From a1dc041dbd74d1d63ed8a2c888f284d1acc85358 Mon Sep 17 00:00:00 2001 From: "ASKA C." Date: Sun, 13 Sep 2026 01:54:03 +0800 Subject: [PATCH 1/7] Align Desktop 0.5.0 with Core agent onboarding --- .github/workflows/desktop-package.yml | 122 ++++++++++++++++++++++++++ desktop/README.md | 83 +++++++++--------- desktop/agent-menu.cjs | 38 +------- desktop/diagnostics-window.cjs | 3 +- desktop/diagnostics.cjs | 1 - desktop/main.cjs | 18 ++-- desktop/package-lock.json | 4 +- desktop/package.json | 2 +- desktop/smoke.cjs | 25 ++---- desktop/stage-windows.cjs | 21 ++++- desktop/test/agent-menu.test.cjs | 53 +++-------- desktop/test/core-release-inspect.cjs | 31 +++++++ desktop/test/diagnostics.test.cjs | 7 +- 13 files changed, 249 insertions(+), 159 deletions(-) create mode 100644 .github/workflows/desktop-package.yml create mode 100644 desktop/test/core-release-inspect.cjs diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml new file mode 100644 index 0000000..5396bdc --- /dev/null +++ b/.github/workflows/desktop-package.yml @@ -0,0 +1,122 @@ +name: Desktop package + +on: + workflow_dispatch: + push: + branches: ['release/desktop-*'] + +permissions: + contents: read + +jobs: + macos-arm64: + runs-on: macos-15 + timeout-minutes: 35 + env: + CORE_REF: v2.12.0 + CORE_COMMIT: 2d2606cb6f8cac62ed7b936a332f9595edffc34a + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + STANDTERM_DISABLE_AGENTINFO_CURRENT: '1' + PYTHONDONTWRITEBYTECODE: '1' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + architecture: arm64 + - name: Verify native tools and prepare isolated dependencies + run: | + test "$(uname -m)" = arm64 + node -e "require('node:assert/strict').equal(process.arch, 'arm64')" + python -c "import platform; assert platform.machine() == 'arm64'" + test "$(git rev-parse "$CORE_REF^{commit}")" = "$CORE_COMMIT" + python -m venv tools/.venv_macos + tools/.venv_macos/bin/python -m pip install -r requirements.txt + tools/.venv_macos/bin/python -m pip freeze > tools/requirements_macos.txt + npm ci --prefix desktop --no-audit --no-fund + mkdir -p desktop/dist/evidence + - name: Test Desktop source + run: npm test --prefix desktop > desktop/dist/evidence/desktop-unit.log 2>&1 + - name: Stage formal Core and build native DMG + run: | + stage=$(node desktop/stage-windows.cjs --macos --core-ref "$CORE_REF") + echo "DESKTOP_STAGE=$stage" >> "$GITHUB_ENV" + node desktop/test/core-release-inspect.cjs "$stage" "$CORE_REF" > desktop/dist/evidence/core-release.json + npm ci --prefix "$stage" --no-audit --no-fund + npm run make:mac --prefix "$stage" + - name: Inspect the app and mounted DMG + run: | + app="$DESKTOP_STAGE/out.noindex/mac-arm64/StandTermDesktop.app" + test -d "$app" + test "$(lipo -archs "$app/Contents/MacOS/StandTermDesktop")" = arm64 + codesign --verify --deep --strict "$app" + node desktop/test/package-inspect.cjs "$app/Contents/Resources" "$DESKTOP_STAGE" "$DESKTOP_STAGE/node_modules/@electron/asar" > desktop/dist/evidence/package-inspection.log + dmg=("$DESKTOP_STAGE"/out.noindex/*.dmg) + test "${#dmg[@]}" = 1 + mountpoint=$(mktemp -d "$RUNNER_TEMP/standterm-dmg.XXXXXX") + hdiutil attach "${dmg[0]}" -readonly -nobrowse -mountpoint "$mountpoint" + trap 'hdiutil detach "$mountpoint"' EXIT + node desktop/test/package-inspect.cjs "$mountpoint/StandTermDesktop.app/Contents/Resources" "$DESKTOP_STAGE" "$DESKTOP_STAGE/node_modules/@electron/asar" >> desktop/dist/evidence/package-inspection.log + echo "DESKTOP_APP=$app" >> "$GITHUB_ENV" + - name: Verify native shell initialization + run: tools/.venv_macos/bin/python desktop/test/macos_shell_smoke.py > desktop/dist/evidence/macos-shell.log 2>&1 + - name: Exercise packaged application + id: smoke + continue-on-error: true + timeout-minutes: 5 + run: | + runtime=$(mktemp -d "$RUNNER_TEMP/standterm-core.XXXXXX") + cp -R "$DESKTOP_APP/Contents/Resources/bundle/core/." "$runtime/" + export STANDTERM_DESKTOP_TEST_ROOT="$runtime" + export STANDTERM_DESKTOP_PYTHON="$GITHUB_WORKSPACE/tools/.venv_macos/bin/python" + export STANDTERM_AGENT_RUNTIME_DIR="$runtime/agent-state" + export STANDTERM_SESSION_RECOVERY_STORE="$runtime/recovery.json" + "$DESKTOP_APP/Contents/MacOS/StandTermDesktop" --backend=macos --desktop-smoke > desktop/dist/evidence/packaged-smoke.log 2>&1 + - name: Exercise first-run setup and reuse + id: setup + continue-on-error: true + timeout-minutes: 10 + run: desktop/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron desktop/test/macos-setup-integration.cjs "$GITHUB_WORKSPACE/tools/.venv_macos/bin/python" "$DESKTOP_STAGE" > desktop/dist/evidence/setup-integration.log 2>&1 + - name: Record build identity and verification results + if: always() && env.DESKTOP_APP != '' + env: + SMOKE_RESULT: ${{ steps.smoke.outcome }} + SETUP_RESULT: ${{ steps.setup.outcome }} + run: | + cp tools/requirements_macos.txt desktop/dist/evidence/tested-runtime-dependencies.txt + cp "$DESKTOP_STAGE/bundle/manifest.json" desktop/dist/evidence/core-manifest.json + cp "$DESKTOP_STAGE/release-identity.json" desktop/dist/evidence/release-identity.json + tools/.venv_macos/bin/python - <<'PY' + import json, os, platform + from pathlib import Path + info = { + 'sourceCommit': os.environ['GITHUB_SHA'], + 'coreCommit': os.environ['CORE_COMMIT'], + 'platform': platform.platform(), 'architecture': platform.machine(), + 'python': platform.python_version(), + 'runUrl': f"https://github.com/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}", + 'packagedSmoke': os.environ['SMOKE_RESULT'], + 'firstRunSetup': os.environ['SETUP_RESULT'], + 'signature': 'Ad hoc; no Developer ID or notarization', + 'manualAcceptance': 'Finder, Gatekeeper, IME and upgrade not exercised', + } + Path('desktop/dist/evidence/build-metadata.json').write_text(json.dumps(info, indent=2) + '\n') + PY + cp "$DESKTOP_STAGE"/out.noindex/*.dmg desktop/dist/evidence/ + cd desktop/dist/evidence + shasum -a 256 *.dmg > SHA256SUMS.txt + - uses: actions/upload-artifact@v4 + if: always() + with: + name: StandTerm-Desktop-macos-arm64 + path: desktop/dist/evidence/ + if-no-files-found: error + retention-days: 14 + - name: Require native interaction checks + if: steps.smoke.outcome != 'success' || steps.setup.outcome != 'success' + run: exit 1 diff --git a/desktop/README.md b/desktop/README.md index 291b1fa..7ce8f1e 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -15,18 +15,18 @@ not a Gatekeeper-qualified public release. No signing account or private key is needed for a local build. Intel/Rosetta acceptance is not implied. New build filenames pair the independent Desktop and bundled Core versions: -`StandTerm-Desktop-0.4.5-2.12.0-mac-arm64.dmg` and -`StandTerm-Desktop-0.4.5-2.12.0-win32-x64-Setup.exe` for future matching builds. +`StandTerm-Desktop-0.5.0-2.12.0-mac-arm64.dmg` and +`StandTerm-Desktop-0.5.0-2.12.0-win32-x64-Setup.exe` for future matching builds. These are naming examples, not published download links. Core 2.12.0 is a source-only release; existing 0.4.3 downloads are unchanged. Delivery archives and checksum sidecars retain the paired label; a future matching Desktop tag -would be `desktop-v0.4.5-2.12.0`. +would be `desktop-v0.5.0-2.12.0`. Staging writes `release-identity.json` from the staged package/lock versions and the manifest-hashed `core_version.py`. The builder revalidates this identity and fails on missing or inconsistent inputs. Build from the printed stage, not the source Desktop directory. Package and lock versions remain Desktop-only SemVer -(`0.4.5`), so the combined label cannot change installer upgrade ordering. The +(`0.5.0`), so the combined label cannot change installer upgrade ordering. The Core qualifier is preserved, and About/Diagnostics continue to show separate versions. A label alone is not a source or Core bundle identity. @@ -68,7 +68,7 @@ Build on an Apple Silicon Mac with Node 22.12+ and the checkout's macOS venv: ```sh cd desktop npm ci -npm run stage:mac +npm run stage:mac -- --core-ref v2.12.0 # Change to the absolute stage directory printed above, then: npm ci npm run make:mac @@ -80,6 +80,11 @@ documents and Git state, and generates the native icon with macOS `sips` and development app copies stay out of Spotlight results. The current build uses electron-builder 26's [macOS signing options](https://www.electron.build/v26/docs/mac/). +The Desktop package workflow builds on a native `macos-15` arm64 runner. It +compares the complete staged Core payload to the formal tag, inspects the app +and mounted DMG, and records native smoke results with the artifact. Automated +checks do not replace Finder, Gatekeeper, IME or installer-upgrade acceptance. + For isolated verification, run `npm test`, `npm run smoke:capture`, the Python bootstrap/runtime/backend tests, and `test/browser-storage-smoke.cjs` with the checkout Electron executable. Set separate `STANDTERM_AGENT_RUNTIME_DIR` and @@ -287,21 +292,23 @@ Permanent disposal of recovery files is a separate, explicit user action. Build tools need Windows Node.js 22.12+ and Git. From the repository: ```powershell -node desktop/stage-windows.cjs +node desktop/stage-windows.cjs --core-ref v2.12.0 # Change to the exact desktop/dist/windows-build-* path printed above. cd npm ci npm run make:win ``` -The staging script creates a new directory each time, using a runtime filter over -`git ls-files` plus explicit desktop-shell/shared-lease file lists. Runtime contents include -the tracked working-tree changes, so this is an evaluation snapshot, not a claim -that uncommitted changes are already a GitHub release. Build staging never copies +The staging script creates a new directory each time. `--core-ref` selects Core +files and bytes from that Git revision; the Desktop shell comes from the current +checkout. Verify a release stage with `node desktop/test/core-release-inspect.cjs + v2.12.0`, then inspect the actual package with `test/package-inspect.cjs`. +Without `--core-ref`, staging uses tracked working-tree Core files for development +snapshots; these are not a claim that changes are already a release. Build staging never copies the development venv or `node_modules`. The new directory gets Windows build dependencies; the source checkout's Linux/WSLg `node_modules` is untouched. -The `StandTerm-Desktop-0.4.5-2.12.0-win32-x64-Setup.exe` is under `out/`; the unpacked +The `StandTerm-Desktop-0.5.0-2.12.0-win32-x64-Setup.exe` is under `out/`; the unpacked application is under `out/win-unpacked/`. Packaging uses [electron-builder's assisted NSIS target](https://www.electron.build/nsis.html), with pinned build dependencies and scoped custom installer hooks. Squirrel @@ -574,24 +581,25 @@ display are covered by tests. ## Agent prompts and bundled Core -The top-level **Agent** menu is the first-use entrypoint; no prior StandTerm -skill installation is required. Choose **Copy skill installation prompt** and -paste it into your agent. For subsequent sessions, use **Copy usage prompt** or -**Copy file-transfer prompt** and describe the intended task and terminal. -**Getting started...** explains token minting and the distinction between setup -and terminal authority. **Copy connection info (JSON)** and **Copy agentinfo URL** -remain available for clients that already know the protocol. Existing Diagnostics -copy actions are preserved. - -These actions only copy text or show help. They do not install skills, execute -helpers, mint tokens or approve transfers. Each prompt contains the exact live -endpoint and instance ID, never credentials. Agents verify `/agentinfo` identity -and use its `launch_dir`, `python_path`, `scripts` and `skills` paths. Installation -prompts ask agents to preserve references and customized skills; agents without -persistent skill support can read the documents for the current session instead. -Backend paths belong to the active macOS, Windows or WSL environment. If access fails or -an older Core lacks discovery metadata, request the correct environment or a -Core update; do not guess a different endpoint or download arbitrary helpers. +Use Core's **Agent Info for Current Tab** to copy an agent prompt. The Desktop +**Agent** menu retains **Show / hide Agent Panel**, **Pause Agent for current +terminal**, and **Getting started...**. Desktop does not generate another set of +AgentInfo URLs or prompts. + +1. Select the tab where the agent runs. In Agent Panel, enable access and choose + permissions for each tab it may operate. +2. For an SSH agent, start **Agent Tunnel** on its SSH tab. Agent Info appears + only after setup succeeds. For a local agent, mint a token in Agent Panel. +3. Open **Agent Info for Current Tab**, choose **Copy Prompt**, and paste it into + the agent with the intended task. Follow the environment identified there. + +Skills do not need to be installed first. The prompt leads to the active Core's +bundled skills and helpers. For persistent skill installation, ask the agent to +follow the `skills[*].install_prompt_path` instructions reported by Agent Info, +preserving references and any customized skills. Copying a prompt does not grant +terminal access or approve transfers. The advanced Git Core mode needs a Core +version that supports current-tab Agent Info; an older checkout does not gain +these controls from the Desktop shell. Core staging includes the published runtime, static assets, launch/install scripts, README, public documentation, skill prompts/references and support @@ -611,7 +619,7 @@ managed Core bundle SHA-256 identity when available. The same Core details are in Diagnostics. Core reports its version from `core_version.py`, independently of the Electron package version. Source checkouts have no managed build identity; older backends that omit version metadata show Unknown, never an inferred Git -tag. The current source pairing is Desktop 0.4.5 / Core 2.12.0. The Core source +tag. The current source pairing is Desktop 0.5.0 / Core 2.12.0. The Core source release does not publish or qualify Desktop installers. The Agent menu and expanded Core payload postdate the published 0.4.1 installer and the earlier macOS 0.4.2 candidate; they require a @@ -636,17 +644,10 @@ snapshot. It has a separate temporary session, no backend cookies, no scripts, no Node/preload bridge and no network access. Paths may identify your OS account; review the page before sharing. Previous-run logs remain in the log folder. -**Copy backend URL** copies only the current origin. **Copy agent connection -info** copies a tokenless JSON object with `base_url`, `agentinfo_url`, -`instance_id` and `backend_mode`, identified by the `standterm_agent_connection` -schema. Both actions are in the main Diagnostics menu and the diagnostic -window's View menu. Paste the latter into the agent conversation to identify -this exact instance when Core and Desktop run together; do not guess port 5000 -or use a shared current-instance pointer. The object is bootstrap information, -not a credential or a handoff file. The agent must fetch its exact Agent Info -URL; existing minting, terminal selection and approval requirements still apply. -Copying does not connect an agent, mint a token or add clipboard access to the -renderer. These source changes postdate the original 0.4.0 evaluation installer. +**Copy backend URL** copies the current Core origin for diagnostics. It is +available in the main Diagnostics menu and the diagnostic window's View menu. +For agent setup, use Core's **Agent Info for Current Tab** and **Copy Prompt**; +Diagnostics does not provide a separate AgentInfo copy action. **Diagnostics > Developer Tools...** requires explicit confirmation each time. Use the Console or Sources snippets for trusted frontend JavaScript diagnostics diff --git a/desktop/agent-menu.cjs b/desktop/agent-menu.cjs index bdbf942..649f4f6 100644 --- a/desktop/agent-menu.cjs +++ b/desktop/agent-menu.cjs @@ -1,45 +1,9 @@ 'use strict'; -const { agentConnectionInfo } = require('./diagnostics.cjs'); - -const INTRO = 'Read the agentinfo_url in the connection JSON below and verify instance_id before proceeding. ' - + 'Use launch_dir, python_path, scripts and skill/skills from that exact instance; do not guess ports or runtime paths. ' - + 'Read the bundled skill entrypoint and only the references needed for this task. ' - + 'Paths belong to the backend environment (macOS, Windows or WSL): if access or the endpoint is unavailable, report the limitation ' - + 'and ask for the correct environment or fresh connection information instead of scanning for another instance. '; -const BOUNDARY = 'Never expose handoff secrets or credentials. Skill installation is not permission to operate terminals. ' - + 'Preserve token minting, terminal modes, human-input boundaries and fresh per-copy browser approval. ' - + 'If no terminal is enabled, ask the user to enable External Agent and mint its token in the intended tab. ' - + 'Do not execute terminal commands until the user specifies the task and target.'; -const PROMPTS = { - usage: 'Help me use this StandTerm instance for my stated task. ' + INTRO - + 'Use the skill boot_prompt_path for routine operation; do not install or overwrite local skills in this workflow. ', - install: 'Help me install or update the bundled StandTerm skills for this agent. ' + INTRO - + 'Read each available skill install_prompt_path (skill_prompt.txt), including external-agent, file-transfer and privileged-HITL guidance. ' - + 'Use this agent\'s supported skill installation mechanism, preserving references and relative paths. ' - + 'Compare existing installations first and ask before overwriting customized content. ' - + 'If persistent skills are unsupported, read the documents for this session and report that nothing was installed. ' - + 'Use helpers from the active Core with its reported Python, rather than copying them into the skill installation. ', - transfer: 'Help me prepare a StandTerm file transfer. ' + INTRO - + 'Read the external-agent and file-transfer skills. Ask for any missing source, destination and file details. ' - + 'Prefer the typed backend copy helper; do not automate the human Files UI or approve the copy yourself. ' - + 'Do not fall back to terminal-stream rescue without a new explicit instruction. ', -}; - -function agentMenu({ origin, mode, instanceId, copyText, showHelp, uiItems = [] }) { - const info = agentConnectionInfo({ origin, mode, instanceId }); - const json = JSON.stringify(info, null, 2); - const prompt = kind => PROMPTS[kind] + BOUNDARY + '\n\n' + json; +function agentMenu({ showHelp, uiItems = [] }) { return { id: 'agent-menu', label: 'Agent', submenu: [ ...uiItems, { id: 'agent-help', label: 'Getting started...', click: showHelp }, - { type: 'separator' }, - { id: 'agent-copy-usage', label: 'Copy usage prompt', click: () => copyText(prompt('usage')) }, - { id: 'agent-copy-install', label: 'Copy skill installation prompt', click: () => copyText(prompt('install')) }, - { id: 'agent-copy-transfer', label: 'Copy file-transfer prompt', click: () => copyText(prompt('transfer')) }, - { type: 'separator' }, - { id: 'agent-copy-connection', label: 'Copy connection info (JSON)', click: () => copyText(json) }, - { id: 'agent-copy-agentinfo', label: 'Copy agentinfo URL', click: () => copyText(info.agentinfo_url) }, ] }; } diff --git a/desktop/diagnostics-window.cjs b/desktop/diagnostics-window.cjs index 8f10ce6..fc9c619 100644 --- a/desktop/diagnostics-window.cjs +++ b/desktop/diagnostics-window.cjs @@ -24,7 +24,7 @@ function statusHtml(rows, events) {
${escape(events.map(event => JSON.stringify(event)).join('\n') || 'No events yet.')}
`; } -function createStatusWindow(owner, snapshot, { copyUrl, copyConnectionInfo }) { +function createStatusWindow(owner, snapshot, { copyUrl }) { const { BrowserWindow, Menu, session } = require('electron'); let win; let opening; @@ -46,7 +46,6 @@ function createStatusWindow(owner, snapshot, { copyUrl, copyConnectionInfo }) { win.setMenu(Menu.buildFromTemplate([{ label: 'View', submenu: [ { label: 'Refresh', accelerator: 'CommandOrControl+R', click: () => refresh().catch(() => {}) }, { label: 'Copy backend URL', click: copyUrl }, - { label: 'Copy agent connection info', click: copyConnectionInfo }, { role: 'close' }, ] }])); const owned = win; diff --git a/desktop/diagnostics.cjs b/desktop/diagnostics.cjs index 77ec1c0..21296ce 100644 --- a/desktop/diagnostics.cjs +++ b/desktop/diagnostics.cjs @@ -65,7 +65,6 @@ function diagnosticsMenu({ origin, mode, instanceId, version, coreVersion, logge { id: 'diagnostics-backend', label: `Backend: ${mode === 'wsl' ? 'WSL' : 'Native'}`, enabled: false }, { id: 'diagnostics-origin', label: `URL: ${origin}`, enabled: false }, { id: 'diagnostics-copy-origin', label: 'Copy backend URL', click: () => copyText(info.base_url) }, - { id: 'diagnostics-copy-agent', label: 'Copy agent connection info', click: () => copyText(JSON.stringify(info, null, 2)) }, { label: persistent ? 'Web settings: saved per origin (same as Core)' : 'Web settings: temporary test profile', enabled: false }, { type: 'separator' }, { id: 'diagnostics-logs', label: 'Open diagnostics log folder', click: openLogs }, diff --git a/desktop/main.cjs b/desktop/main.cjs index c87ff33..4e9f634 100644 --- a/desktop/main.cjs +++ b/desktop/main.cjs @@ -377,7 +377,6 @@ async function start() { ['Security', 'Core sandbox on; Node/preload off; isolated Desktop toolbar; external preview network blocked'], ], events: diagnostics.snapshot() }), { copyUrl: () => clipboard.writeText(connectionInfo.base_url), - copyConnectionInfo: () => clipboard.writeText(JSON.stringify(connectionInfo, null, 2)), }); Menu.setApplicationMenu(Menu.buildFromTemplate([ { id: 'standterm', label: 'StandTerm', submenu: [ @@ -400,17 +399,18 @@ async function start() { { label: 'Quit StandTerm', accelerator: 'CommandOrControl+Q', click: () => app.quit() }, ] }, { id: 'edit', role: 'editMenu' }, - agentMenu({ origin: handoff.origin, mode, instanceId: handoff.instance_id, + agentMenu({ uiItems: [commands.item('agentPanel'), commands.item('pauseAgent'), { type: 'separator' }], - copyText: text => clipboard.writeText(text), showHelp: () => dialog.showMessageBox(win, { type: 'info', title: 'StandTerm Agent', message: 'Give your agent a StandTerm prompt', - detail: 'First time: choose Agent > Copy skill installation prompt and paste it into your agent. ' - + 'For an existing setup, use Copy usage prompt or Copy file-transfer prompt and describe your task.\n\n' - + 'Prompts identify this instance without credentials. The agent reads bundled Core documents and helpers; ' - + 'it needs access to the backend environment (macOS, Windows or WSL).\n\n' - + 'When ready, enable External Agent in the intended terminal tab and mint a token. ' - + 'Copying a prompt does not install anything, grant terminal access or approve transfers.', + detail: 'Select the tab where your agent runs. Use Agent Panel to enable access and choose permissions ' + + 'on each tab it may operate.\n\n' + + 'For an SSH agent, start Agent Tunnel on its SSH tab. Agent Info for Current Tab appears after setup succeeds. ' + + 'For a local agent, mint a token in Agent Panel.\n\n' + + 'Open Agent Info for Current Tab, choose Copy Prompt, and paste it into your agent with the intended task. ' + + 'Follow the environment shown in that dialog.\n\n' + + 'Skills do not need to be installed first. The prompt leads to the bundled skills and helpers; ' + + 'Agent Info also provides installation instructions when persistent skills are wanted.', buttons: ['OK'], noLink: true, }), }), diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 27a171d..9dc162e 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "standterm-desktop-evaluation", - "version": "0.4.5", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "standterm-desktop-evaluation", - "version": "0.4.5", + "version": "0.5.0", "license": "MIT", "devDependencies": { "electron": "44.2.0", diff --git a/desktop/package.json b/desktop/package.json index 978fd15..3fb6ca5 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "standterm-desktop-evaluation", - "version": "0.4.5", + "version": "0.5.0", "private": true, "productName": "StandTerm Desktop", "author": "ASKA C.", diff --git a/desktop/smoke.cjs b/desktop/smoke.cjs index 1721459..a987c48 100644 --- a/desktop/smoke.cjs +++ b/desktop/smoke.cjs @@ -31,25 +31,15 @@ async function run(win, origin, contents = win.webContents, browserAccess) { clipboard.writeText = value => copied.push(value); try { menu.getMenuItemById('diagnostics-copy-origin').click(); - menu.getMenuItemById('diagnostics-copy-agent').click(); - menu.getMenuItemById('agent-copy-connection').click(); - menu.getMenuItemById('agent-copy-install').click(); - menu.getMenuItemById('agent-copy-usage').click(); - menu.getMenuItemById('agent-copy-transfer').click(); } finally { clipboard.writeText = originalCopy; } - assert.equal(copied[0], origin); - const connection = JSON.parse(copied[1]); - assert.equal(copied[2], copied[1]); - for (const prompt of copied.slice(3)) { - assert.deepEqual(JSON.parse(prompt.slice(prompt.indexOf('{'))), connection); - assert.ok(prompt.includes('verify instance_id')); + assert.deepEqual(copied, [origin]); + for (const id of ['diagnostics-copy-agent', 'agent-copy-connection', 'agent-copy-agentinfo', + 'agent-copy-install', 'agent-copy-usage', 'agent-copy-transfer']) { + assert.equal(menu.getMenuItemById(id), null); } - assert.equal(connection.base_url, origin); - assert.equal(connection.agentinfo_url, origin + '/agentinfo'); - assert.ok(connection.instance_id); const agentinfo = await contents.executeJavaScript('fetch("/agentinfo").then(response => response.json())'); - assert.equal(agentinfo.instance_id, connection.instance_id); - assert.equal(agentinfo.agentinfo_url, connection.agentinfo_url); + assert.equal(typeof agentinfo.instance_id, 'string'); + assert.equal(agentinfo.agentinfo_url, origin + '/agentinfo'); for (const name of ['standterm-external-agent', 'standterm-file-transfer', 'standterm-privileged-hitl']) { const skill = agentinfo.skills[name]; assert.equal(skill.available, true, `Missing ${name} in the running Core`); @@ -60,9 +50,6 @@ async function run(win, origin, contents = win.webContents, browserAccess) { } assert.deepEqual(agentinfo.skill, agentinfo.skills['standterm-external-agent']); for (const name of ['agent_scp', 'agent_rsfile', 'agent_mcp']) assert.equal(typeof agentinfo.scripts[name], 'string'); - assert.ok(['windows', 'wsl', 'macos'].includes(connection.backend_mode)); - assert.deepEqual(Object.keys(connection).sort(), - ['schema', 'schema_version', 'base_url', 'agentinfo_url', 'instance_id', 'backend_mode'].sort()); assert.equal(contents.isDevToolsOpened(), false); menu.getMenuItemById('diagnostics-status').click(); const status = BrowserWindow.getAllWindows().find(candidate => candidate !== win); diff --git a/desktop/stage-windows.cjs b/desktop/stage-windows.cjs index 66152c0..ff73109 100644 --- a/desktop/stage-windows.cjs +++ b/desktop/stage-windows.cjs @@ -12,9 +12,15 @@ const { releaseIdentity } = require('./release-identity.cjs'); const platform = process.argv.includes('--macos') ? 'macos' : 'windows'; if (platform === 'macos' && process.platform !== 'darwin') throw new Error('Stage macOS on a native Mac.'); const root = path.resolve(__dirname, '..'); -const tracked = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8' }).split('\0').filter(Boolean); +const coreRefIndex = process.argv.indexOf('--core-ref'); +const coreRef = coreRefIndex < 0 ? null : process.argv[coreRefIndex + 1]; +if (coreRefIndex >= 0 && (!coreRef || coreRef.startsWith('--'))) throw new Error('--core-ref requires a Git revision.'); +const coreCommit = coreRef && execFileSync('git', ['rev-parse', '--verify', '--end-of-options', `${coreRef}^{commit}`], + { cwd: root, encoding: 'utf8' }).trim(); +const tracked = execFileSync('git', coreCommit ? ['ls-tree', '-r', '--name-only', '-z', coreCommit] : ['ls-files', '-z'], + { cwd: root, encoding: 'utf8' }).split('\0').filter(Boolean); const coreFiles = selectCoreFiles(tracked); -validateCoreFiles(root, coreFiles); +if (!coreCommit) validateCoreFiles(root, coreFiles); const shellFiles = [ 'package.json', 'package-lock.json', 'electron-builder.cjs', 'installer.nsh', 'main.cjs', 'policy.cjs', 'capture.cjs', 'capture-file.cjs', 'recorder.html', 'recorder.js', 'setup.cjs', 'macos-python.cjs', @@ -66,8 +72,15 @@ if (platform === 'macos') { } const files = {}; for (const file of coreFiles.sort()) { - copy(path.join(root, file), path.join(stage, 'bundle', 'core', file)); - files[file] = createHash('sha256').update(fs.readFileSync(path.join(root, file))).digest('hex'); + const destination = path.join(stage, 'bundle', 'core', file); + if (coreCommit) { + const data = execFileSync('git', ['show', `${coreCommit}:${file}`], { cwd: root, maxBuffer: 16 * 1024 * 1024 }); + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.writeFileSync(destination, data, { flag: 'wx' }); + } else { + copy(path.join(root, file), destination); + } + files[file] = createHash('sha256').update(fs.readFileSync(destination)).digest('hex'); } const id = createHash('sha256').update(JSON.stringify(files)).digest('hex'); validateCoreFiles(path.join(stage, 'bundle', 'core'), Object.keys(files)); diff --git a/desktop/test/agent-menu.test.cjs b/desktop/test/agent-menu.test.cjs index 844ab1b..4d4f09f 100644 --- a/desktop/test/agent-menu.test.cjs +++ b/desktop/test/agent-menu.test.cjs @@ -3,44 +3,19 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const { agentMenu } = require('../agent-menu.cjs'); -const { agentConnectionInfo } = require('../diagnostics.cjs'); -for (const mode of ['windows', 'wsl', 'macos']) { - test(`Agent menu offers opt-in prompts for the exact ${mode} instance`, () => { - const copied = []; - let help = 0; - const options = { origin: 'http://127.0.0.1:64487', mode, instanceId: 'instance-test', - token: 'SECRET-TEST', terminal: 'PRIVATE-TEST', copyText: text => copied.push(text), showHelp: () => help++ }; - const menu = agentMenu(options); - assert.equal(menu.label, 'Agent'); - assert.deepEqual(copied, []); - const click = id => menu.submenu.find(item => item.id === id).click(); - click('agent-help'); - assert.equal(help, 1); - assert.deepEqual(copied, []); - for (const kind of ['usage', 'install', 'transfer']) { - click(`agent-copy-${kind}`); - const value = copied.at(-1); - assert.deepEqual(JSON.parse(value.slice(value.indexOf('{'))), agentConnectionInfo(options)); - for (const required of ['verify instance_id', 'python_path', 'launch_dir', 'macOS, Windows or WSL', - 'do not guess ports', 'mint', 'approval', 'Do not execute terminal commands']) assert.ok(value.includes(required)); - assert.ok(!/SECRET-TEST|PRIVATE-TEST|:5000/.test(value)); - } - assert.ok(copied[0].includes('do not install or overwrite')); - assert.ok(copied[1].includes('ask before overwriting customized content')); - assert.ok(copied[1].includes('nothing was installed')); - assert.ok(copied[2].includes('Do not fall back to terminal-stream rescue')); - click('agent-copy-connection'); - assert.deepEqual(JSON.parse(copied.at(-1)), agentConnectionInfo(options)); - click('agent-copy-agentinfo'); - assert.equal(copied.at(-1), options.origin + '/agentinfo'); - }); -} - -test('Agent menu rejects credential-bearing origins and invalid identities', () => { - const options = { origin: 'http://127.0.0.1:64487', mode: 'wsl', instanceId: 'instance-test' }; - for (const invalid of [{ origin: options.origin + '/?token=secret' }, { origin: 'https://example.com' }, - { instanceId: 'id\ncontrol' }, { mode: 'unknown' }]) { - assert.throws(() => agentMenu({ ...options, ...invalid })); - } +test('Agent menu delegates terminal authority to Core and offers guidance', () => { + const calls = []; + const uiItems = [ + { id: 'ui-agentPanel', click: () => calls.push('panel') }, + { id: 'ui-pauseAgent', click: () => calls.push('pause') }, + { type: 'separator' }, + ]; + const menu = agentMenu({ uiItems, showHelp: () => calls.push('help') }); + assert.equal(menu.label, 'Agent'); + assert.deepEqual(calls, []); + assert.deepEqual(menu.submenu.map(item => item.id).filter(Boolean), + ['ui-agentPanel', 'ui-pauseAgent', 'agent-help']); + for (const item of menu.submenu) if (item.click) item.click(); + assert.deepEqual(calls, ['panel', 'pause', 'help']); }); diff --git a/desktop/test/core-release-inspect.cjs b/desktop/test/core-release-inspect.cjs new file mode 100644 index 0000000..0e4f1cd --- /dev/null +++ b/desktop/test/core-release-inspect.cjs @@ -0,0 +1,31 @@ +'use strict'; + +// Compare a staged Core payload with an independently selected public release. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createHash } = require('node:crypto'); +const { execFileSync } = require('node:child_process'); +const { coreFiles, validateCoreFiles } = require('../core-files.cjs'); +const { stagedReleaseIdentity } = require('../release-identity.cjs'); +const [stageArg, coreRef] = process.argv.slice(2); +if (!stageArg || !coreRef) throw new Error('Pass the build stage and expected Core release tag.'); +const root = path.resolve(__dirname, '../..'); +const git = args => execFileSync('git', args, { cwd: root, maxBuffer: 16 * 1024 * 1024 }); +const commit = git(['rev-parse', '--verify', '--end-of-options', `${coreRef}^{commit}`]).toString().trim(); +const selected = coreFiles(git(['ls-tree', '-r', '--name-only', '-z', commit]).toString().split('\0').filter(Boolean)); +const stage = path.resolve(stageArg); +const directory = path.join(stage, 'bundle', 'core'); +const manifest = JSON.parse(fs.readFileSync(path.join(stage, 'bundle', 'manifest.json'), 'utf8')); +assert.deepEqual(Object.keys(manifest.files).sort(), selected); +validateCoreFiles(directory, selected); +const hash = data => createHash('sha256').update(data).digest('hex'); +for (const file of selected) { + const expected = git(['show', `${commit}:${file}`]); + assert.deepEqual(fs.readFileSync(path.join(directory, file)), expected, file); + assert.equal(manifest.files[file], hash(expected), file); +} +assert.equal(manifest.id, hash(JSON.stringify(manifest.files))); +const identity = stagedReleaseIdentity(stage); +assert.equal(coreRef, `v${identity.coreVersion}`); +console.log(JSON.stringify({ coreRef, coreCommit: commit, coreFiles: selected.length, ...identity }, null, 2)); diff --git a/desktop/test/diagnostics.test.cjs b/desktop/test/diagnostics.test.cjs index bcafe39..a105178 100644 --- a/desktop/test/diagnostics.test.cjs +++ b/desktop/test/diagnostics.test.cjs @@ -76,7 +76,7 @@ test('diagnostics menu shows the actual owned endpoint without an access URL', t } }); -test('connection copy uses the exact instance and never includes credentials or page payloads', t => { +test('backend URL copy preserves origin validation without another AgentInfo entry', t => { const { logger } = fixture(t); const options = { origin: 'http://127.0.0.1:64487', mode: 'wsl', instanceId: 'desktop-test-1', token: 'secret', session_token: 'secret', launcher_token: 'secret', terminal: 'private content' }; @@ -88,9 +88,8 @@ test('connection copy uses the exact instance and never includes credentials or const menu = diagnosticsMenu({ ...options, version: '0.4.0', logger, copyText: value => copied.push(value) }); assert.deepEqual(copied, []); menu.submenu.find(item => item.id === 'diagnostics-copy-origin').click(); - menu.submenu.find(item => item.id === 'diagnostics-copy-agent').click(); - assert.equal(copied[0], options.origin); - assert.deepEqual(JSON.parse(copied[1]), info); + assert.deepEqual(copied, [options.origin]); + assert.equal(menu.submenu.find(item => item.id === 'diagnostics-copy-agent'), undefined); for (const invalid of [{ origin: options.origin + '/?token=secret' }, { instanceId: '' }, { instanceId: 'id\nsecret' }, { mode: 'unknown' }]) { assert.throws(() => agentConnectionInfo({ ...options, ...invalid })); From 4d1abb293de13eee577a66c5df3706508d32e857 Mon Sep 17 00:00:00 2001 From: "ASKA C." Date: Sun, 13 Sep 2026 02:01:26 +0800 Subject: [PATCH 2/7] Launch Mac setup checks through Electron's CLI --- .github/workflows/desktop-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index 5396bdc..4517bb1 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -81,7 +81,7 @@ jobs: id: setup continue-on-error: true timeout-minutes: 10 - run: desktop/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron desktop/test/macos-setup-integration.cjs "$GITHUB_WORKSPACE/tools/.venv_macos/bin/python" "$DESKTOP_STAGE" > desktop/dist/evidence/setup-integration.log 2>&1 + run: node desktop/node_modules/electron/cli.js desktop/test/macos-setup-integration.cjs "$GITHUB_WORKSPACE/tools/.venv_macos/bin/python" "$DESKTOP_STAGE" > desktop/dist/evidence/setup-integration.log 2>&1 - name: Record build identity and verification results if: always() && env.DESKTOP_APP != '' env: From 095f0c676d71c5a994fdea4650fef180b063dd1a Mon Sep 17 00:00:00 2001 From: "ASKA C." Date: Sun, 13 Sep 2026 02:17:32 +0800 Subject: [PATCH 3/7] Verify Desktop packages on native Windows CI --- .github/workflows/desktop-package.yml | 109 ++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 6 deletions(-) diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index 4517bb1..cd23a47 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -8,16 +8,17 @@ on: permissions: contents: read +env: + CORE_REF: v2.12.0 + CORE_COMMIT: 2d2606cb6f8cac62ed7b936a332f9595edffc34a + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + STANDTERM_DISABLE_AGENTINFO_CURRENT: '1' + PYTHONDONTWRITEBYTECODE: '1' + jobs: macos-arm64: runs-on: macos-15 timeout-minutes: 35 - env: - CORE_REF: v2.12.0 - CORE_COMMIT: 2d2606cb6f8cac62ed7b936a332f9595edffc34a - CSC_IDENTITY_AUTO_DISCOVERY: 'false' - STANDTERM_DISABLE_AGENTINFO_CURRENT: '1' - PYTHONDONTWRITEBYTECODE: '1' steps: - uses: actions/checkout@v4 with: @@ -120,3 +121,99 @@ jobs: - name: Require native interaction checks if: steps.smoke.outcome != 'success' || steps.setup.outcome != 'success' run: exit 1 + + windows-x64: + runs-on: windows-2025 + timeout-minutes: 35 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + architecture: x64 + - name: Prepare native tools and isolated dependencies + run: | + node -e "require('node:assert/strict').equal(process.arch, 'x64')" + python -c "import struct,sys; assert sys.platform == 'win32' and struct.calcsize('P') == 8" + if ((git rev-parse "$env:CORE_REF^{commit}") -ne $env:CORE_COMMIT) { throw 'Unexpected Core release commit' } + python -m venv tools/.venv_win + tools/.venv_win/Scripts/python.exe -m pip install -r requirements.txt + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + tools/.venv_win/Scripts/python.exe -m pip freeze > tools/requirement_winenv.txt + npm ci --prefix desktop --no-audit --no-fund + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + New-Item -ItemType Directory desktop/dist/evidence + - name: Test Desktop source + run: npm test --prefix desktop *> desktop/dist/evidence/desktop-unit.log + - name: Stage formal Core and build native installer + run: | + $stage = node desktop/stage-windows.cjs --core-ref $env:CORE_REF + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + "DESKTOP_STAGE=$stage" >> $env:GITHUB_ENV + node desktop/test/core-release-inspect.cjs $stage $env:CORE_REF > desktop/dist/evidence/core-release.json + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm ci --prefix $stage --no-audit --no-fund + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run make:win --prefix $stage + - name: Inspect the Windows application + run: | + node desktop/test/package-inspect.cjs "$env:DESKTOP_STAGE/out/win-unpacked/resources" $env:DESKTOP_STAGE "$env:DESKTOP_STAGE/node_modules/@electron/asar" > desktop/dist/evidence/package-inspection.log + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + "DESKTOP_APP=$env:DESKTOP_STAGE/out/win-unpacked/StandTermDesktop.exe" >> $env:GITHUB_ENV + - name: Exercise packaged application + id: smoke + continue-on-error: true + timeout-minutes: 5 + run: | + $runtime = Join-Path $env:RUNNER_TEMP ('standterm-core-' + [guid]::NewGuid()) + New-Item -ItemType Directory $runtime + Copy-Item "$env:DESKTOP_STAGE/out/win-unpacked/resources/bundle/core/*" $runtime -Recurse + $env:STANDTERM_DESKTOP_TEST_ROOT = $runtime + $env:STANDTERM_DESKTOP_PYTHON = "$env:GITHUB_WORKSPACE/tools/.venv_win/Scripts/python.exe" + $env:STANDTERM_AGENT_RUNTIME_DIR = "$runtime/agent-state" + $env:STANDTERM_SESSION_RECOVERY_STORE = "$runtime/recovery.json" + & $env:DESKTOP_APP --backend=windows --desktop-smoke *> desktop/dist/evidence/packaged-smoke.log + exit $LASTEXITCODE + - name: Exercise first-run preparation and reuse + id: setup + continue-on-error: true + timeout-minutes: 10 + run: node desktop/test/bootstrap-integration.cjs "$env:GITHUB_WORKSPACE/tools/.venv_win/Scripts/python.exe" "$env:DESKTOP_STAGE/out/win-unpacked/resources/bundle" *> desktop/dist/evidence/setup-integration.log + - name: Record build identity and verification results + if: always() && env.DESKTOP_APP != '' + env: + SMOKE_RESULT: ${{ steps.smoke.outcome }} + SETUP_RESULT: ${{ steps.setup.outcome }} + run: | + Copy-Item tools/requirement_winenv.txt desktop/dist/evidence/tested-runtime-dependencies.txt + Copy-Item "$env:DESKTOP_STAGE/bundle/manifest.json" desktop/dist/evidence/core-manifest.json + Copy-Item "$env:DESKTOP_STAGE/release-identity.json" desktop/dist/evidence/release-identity.json + $info = [ordered]@{ + sourceCommit = $env:GITHUB_SHA; coreCommit = $env:CORE_COMMIT + platform = 'win32'; architecture = 'x64' + runUrl = "https://github.com/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID" + packagedSmoke = $env:SMOKE_RESULT; firstRunSetup = $env:SETUP_RESULT + signature = (Get-AuthenticodeSignature $env:DESKTOP_APP).Status.ToString() + fileVersion = (Get-Item $env:DESKTOP_APP).VersionInfo.FileVersion + manualAcceptance = 'Installer upgrade, real IME and clipboard not exercised' + } + $info | ConvertTo-Json > desktop/dist/evidence/build-metadata.json + Copy-Item "$env:DESKTOP_STAGE/out/*Setup.exe" desktop/dist/evidence/ + Get-ChildItem desktop/dist/evidence/*Setup.exe | ForEach-Object { + (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower() + ' ' + $_.Name + } > desktop/dist/evidence/SHA256SUMS.txt + - uses: actions/upload-artifact@v4 + if: always() + with: + name: StandTerm-Desktop-windows-x64 + path: desktop/dist/evidence/ + if-no-files-found: error + retention-days: 14 + - name: Require native interaction checks + if: steps.smoke.outcome != 'success' || steps.setup.outcome != 'success' + run: exit 1 From 22841da2efea91df2a52608046f4553795ad41d1 Mon Sep 17 00:00:00 2001 From: "ASKA C." Date: Sun, 13 Sep 2026 02:22:06 +0800 Subject: [PATCH 4/7] Document Desktop 0.5.0 evaluation delivery --- README.md | 16 ++++++++-------- desktop/README.md | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index db703e4..a586f0d 100644 --- a/README.md +++ b/README.md @@ -13,29 +13,29 @@ the [browser-based Core quick start](#quick-start). It adds SSH Agent Tunnel with shared skills and helpers, Agent Panel permission sync, Agent Info for the current tab, and SSH host fingerprint management. IME input-line anchoring remains an [experimental PoC](docs/ime_anchor_poc.md). -This release does not publish new Desktop installers: the downloads below remain -Desktop 0.4.3 evaluation builds with their original bundled Core snapshot. +The separate Desktop 0.5.0 evaluation below bundles the formal Core 2.12.0 source. ![StandTerm Desktop with terminal rendering tests, local and SSH tabs, and a floating PowerShell terminal](standterm_desktop.png) -*Desktop development preview. The toolbar shown above is newer than the published -Desktop 0.4.3 installers.* +*Desktop preview. See the release notes for the current controls and validation.* ## Desktop Downloads (Evaluation) -[StandTerm Desktop 0.4.3](https://github.com/askac/standterm/releases/tag/desktop-v0.4.3) +[StandTerm Desktop 0.5.0 / Core 2.12.0](https://github.com/askac/standterm/releases/tag/desktop-v0.5.0-2.12.0) is available as an evaluation pre-release, not a production-qualified release. | Platform | Download | Required before installation | | --- | --- | --- | -| Windows x64, including Windows + WSL | [Windows installer (.exe)](https://github.com/askac/standterm/releases/download/desktop-v0.4.3/StandTerm-Desktop-0.4.3-win32-x64-Setup.exe) | Python 3.10+ with venv/ensurepip in each selected environment; WSL mode also needs an existing WSL distribution. | -| macOS Apple Silicon | [macOS installer (.dmg)](https://github.com/askac/standterm/releases/download/desktop-v0.4.3/StandTerm-Desktop-0.4.3-mac-arm64.dmg) | Native arm64 Python 3.10+ with venv/ensurepip. Intel/Rosetta is not qualified. | +| Windows x64, including Windows + WSL | [Windows installer (.exe)](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.0/StandTerm-Desktop-0.5.0-2.12.0-win32-x64-Setup.exe) | Python 3.10+ with venv/ensurepip in each selected environment; WSL mode also needs an existing WSL distribution. | +| macOS Apple Silicon | [macOS installer (.dmg)](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.0/StandTerm-Desktop-0.5.0-2.12.0-mac-arm64.dmg) | Native arm64 Python 3.10+ with venv/ensurepip. Intel/Rosetta is not qualified. | Packages include Electron and Core. **Git, Node.js and npm are not required**; Python and its virtual environment are not bundled. +The optional advanced Git Core source requires Git in the selected backend +environment. Bundled Core recovery remains available without Git. 1. Download the package for your platform and check the release's - [SHA256SUMS](https://github.com/askac/standterm/releases/download/desktop-v0.4.3/SHA256SUMS). + [SHA256SUMS](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.0/SHA256SUMS). 2. On Windows, run the installer and choose **Windows only**, **Windows + WSL** or **WSL only**. Native Windows mode needs 64-bit Windows Python; installing Windows Python does not satisfy WSL mode. On macOS, copy the app to a diff --git a/desktop/README.md b/desktop/README.md index 7ce8f1e..23b2e09 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -16,11 +16,10 @@ needed for a local build. Intel/Rosetta acceptance is not implied. New build filenames pair the independent Desktop and bundled Core versions: `StandTerm-Desktop-0.5.0-2.12.0-mac-arm64.dmg` and -`StandTerm-Desktop-0.5.0-2.12.0-win32-x64-Setup.exe` for future matching builds. -These are naming examples, not published download links. Core 2.12.0 is a -source-only release; existing 0.4.3 downloads are unchanged. Delivery archives -and checksum sidecars retain the paired label; a future matching Desktop tag -would be `desktop-v0.5.0-2.12.0`. +`StandTerm-Desktop-0.5.0-2.12.0-win32-x64-Setup.exe`. +The [Desktop 0.5.0 evaluation release](https://github.com/askac/standterm/releases/tag/desktop-v0.5.0-2.12.0) +contains paired packages, checksums and validation evidence. Core 2.12.0 remains +a separate source release; older Desktop releases retain their original files. Staging writes `release-identity.json` from the staged package/lock versions and the manifest-hashed `core_version.py`. The builder revalidates this identity and @@ -80,7 +79,8 @@ documents and Git state, and generates the native icon with macOS `sips` and development app copies stay out of Spotlight results. The current build uses electron-builder 26's [macOS signing options](https://www.electron.build/v26/docs/mac/). -The Desktop package workflow builds on a native `macos-15` arm64 runner. It +The Desktop package workflow builds on native `macos-15` arm64 and +`windows-2025` x64 runners. It compares the complete staged Core payload to the formal tag, inspects the app and mounted DMG, and records native smoke results with the artifact. Automated checks do not replace Finder, Gatekeeper, IME or installer-upgrade acceptance. From 7e12ebb6b54010fdd3cbd7541937a37a43ed0cfc Mon Sep 17 00:00:00 2001 From: "ASKA C." Date: Sun, 13 Sep 2026 02:26:45 +0800 Subject: [PATCH 5/7] Wait for packaged GUI smoke process completion --- .github/workflows/desktop-package.yml | 4 ++-- desktop/test/packaged-smoke-runner.cjs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 desktop/test/packaged-smoke-runner.cjs diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index cd23a47..c9e7936 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -77,7 +77,7 @@ jobs: export STANDTERM_DESKTOP_PYTHON="$GITHUB_WORKSPACE/tools/.venv_macos/bin/python" export STANDTERM_AGENT_RUNTIME_DIR="$runtime/agent-state" export STANDTERM_SESSION_RECOVERY_STORE="$runtime/recovery.json" - "$DESKTOP_APP/Contents/MacOS/StandTermDesktop" --backend=macos --desktop-smoke > desktop/dist/evidence/packaged-smoke.log 2>&1 + node desktop/test/packaged-smoke-runner.cjs "$DESKTOP_APP/Contents/MacOS/StandTermDesktop" macos > desktop/dist/evidence/packaged-smoke.log 2>&1 - name: Exercise first-run setup and reuse id: setup continue-on-error: true @@ -177,7 +177,7 @@ jobs: $env:STANDTERM_DESKTOP_PYTHON = "$env:GITHUB_WORKSPACE/tools/.venv_win/Scripts/python.exe" $env:STANDTERM_AGENT_RUNTIME_DIR = "$runtime/agent-state" $env:STANDTERM_SESSION_RECOVERY_STORE = "$runtime/recovery.json" - & $env:DESKTOP_APP --backend=windows --desktop-smoke *> desktop/dist/evidence/packaged-smoke.log + node desktop/test/packaged-smoke-runner.cjs $env:DESKTOP_APP windows *> desktop/dist/evidence/packaged-smoke.log exit $LASTEXITCODE - name: Exercise first-run preparation and reuse id: setup diff --git a/desktop/test/packaged-smoke-runner.cjs b/desktop/test/packaged-smoke-runner.cjs new file mode 100644 index 0000000..480b02d --- /dev/null +++ b/desktop/test/packaged-smoke-runner.cjs @@ -0,0 +1,16 @@ +'use strict'; + +// Wait for GUI executables explicitly; shell launch success is not a smoke result. +const { spawnSync } = require('node:child_process'); +const [executable, mode] = process.argv.slice(2); +if (!executable || !['windows', 'wsl', 'macos'].includes(mode)) { + throw new Error('Pass the packaged executable and backend mode.'); +} +const result = spawnSync(executable, [`--backend=${mode}`, '--desktop-smoke'], { + env: process.env, encoding: 'utf8', timeout: 180000, maxBuffer: 4 * 1024 * 1024, +}); +process.stdout.write(result.stdout || ''); +process.stderr.write(result.stderr || ''); +console.log(JSON.stringify({ type: 'desktop_smoke_process_result', exitCode: result.status, + signal: result.signal, errorCode: result.error?.code || null })); +process.exit(result.error ? 1 : result.status ?? 1); From fb5f3227be3d2cee0af72259a5a8ef3757985dbb Mon Sep 17 00:00:00 2001 From: "ASKA C." Date: Sun, 13 Sep 2026 02:45:29 +0800 Subject: [PATCH 6/7] Record state when Desktop smoke checks time out --- desktop/smoke.cjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/desktop/smoke.cjs b/desktop/smoke.cjs index a987c48..89e7128 100644 --- a/desktop/smoke.cjs +++ b/desktop/smoke.cjs @@ -10,6 +10,14 @@ async function waitFor(contents, predicate) { if (await contents.executeJavaScript(predicate)) return; await new Promise(resolve => setTimeout(resolve, 100)); } + const state = await contents.executeJavaScript(`({ + path: location.pathname, focused: document.hasFocus(), visibility: document.visibilityState, + activeTag: document.activeElement?.tagName, activeClass: document.activeElement?.className, + socket: window.terminalTest?.getSocketState(), + connection: window.terminalTest?.getConnectionDiagnostics(), + terminal: window.terminalTest?.getActiveAgentState()?.connected + })`).catch(() => null); + console.error('Desktop smoke wait state:', JSON.stringify(state)); throw new Error('Desktop smoke timed out waiting for terminal state.'); } From 49b4ff7a7f4a49daa83a723b4440733400d351cd Mon Sep 17 00:00:00 2001 From: "ASKA C." Date: Sun, 13 Sep 2026 02:54:31 +0800 Subject: [PATCH 7/7] Restore Windows rendering and bundle Core 2.12.1 --- .github/workflows/desktop-package.yml | 4 ++-- README.md | 10 +++++----- desktop/README.md | 26 ++++++++++++++++---------- desktop/main.cjs | 8 +++++++- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index c9e7936..ba6a713 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -9,8 +9,8 @@ permissions: contents: read env: - CORE_REF: v2.12.0 - CORE_COMMIT: 2d2606cb6f8cac62ed7b936a332f9595edffc34a + CORE_REF: v2.12.1 + CORE_COMMIT: 1838d76f7c49850345ab7306c3ffdcb663f81cfd CSC_IDENTITY_AUTO_DISCOVERY: 'false' STANDTERM_DISABLE_AGENTINFO_CURRENT: '1' PYTHONDONTWRITEBYTECODE: '1' diff --git a/README.md b/README.md index 58b7a4f..10d5b48 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It fixes Windows local shell startup when an executable path contains spaces. Core 2.12 adds SSH Agent Tunnel with shared skills and helpers, Agent Panel permission sync, Agent Info for the current tab, and SSH host fingerprint management. IME input-line anchoring remains an [experimental PoC](docs/ime_anchor_poc.md). -The separate Desktop 0.5.0 evaluation below bundles the formal Core 2.12.0 source. +The separate Desktop 0.5.0 evaluation below bundles the formal Core 2.12.1 source. ![StandTerm Desktop with terminal rendering tests, local and SSH tabs, and a floating PowerShell terminal](standterm_desktop.png) @@ -22,13 +22,13 @@ The separate Desktop 0.5.0 evaluation below bundles the formal Core 2.12.0 sourc ## Desktop Downloads (Evaluation) -[StandTerm Desktop 0.5.0 / Core 2.12.0](https://github.com/askac/standterm/releases/tag/desktop-v0.5.0-2.12.0) +[StandTerm Desktop 0.5.0 / Core 2.12.1](https://github.com/askac/standterm/releases/tag/desktop-v0.5.0-2.12.1) is available as an evaluation pre-release, not a production-qualified release. | Platform | Download | Required before installation | | --- | --- | --- | -| Windows x64, including Windows + WSL | [Windows installer (.exe)](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.0/StandTerm-Desktop-0.5.0-2.12.0-win32-x64-Setup.exe) | Python 3.10+ with venv/ensurepip in each selected environment; WSL mode also needs an existing WSL distribution. | -| macOS Apple Silicon | [macOS installer (.dmg)](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.0/StandTerm-Desktop-0.5.0-2.12.0-mac-arm64.dmg) | Native arm64 Python 3.10+ with venv/ensurepip. Intel/Rosetta is not qualified. | +| Windows x64, including Windows + WSL | [Windows installer (.exe)](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.1/StandTerm-Desktop-0.5.0-2.12.1-win32-x64-Setup.exe) | Python 3.10+ with venv/ensurepip in each selected environment; WSL mode also needs an existing WSL distribution. | +| macOS Apple Silicon | [macOS installer (.dmg)](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.1/StandTerm-Desktop-0.5.0-2.12.1-mac-arm64.dmg) | Native arm64 Python 3.10+ with venv/ensurepip. Intel/Rosetta is not qualified. | Packages include Electron and Core. **Git, Node.js and npm are not required**; Python and its virtual environment are not bundled. @@ -36,7 +36,7 @@ The optional advanced Git Core source requires Git in the selected backend environment. Bundled Core recovery remains available without Git. 1. Download the package for your platform and check the release's - [SHA256SUMS](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.0/SHA256SUMS). + [SHA256SUMS](https://github.com/askac/standterm/releases/download/desktop-v0.5.0-2.12.1/SHA256SUMS). 2. On Windows, run the installer and choose **Windows only**, **Windows + WSL** or **WSL only**. Native Windows mode needs 64-bit Windows Python; installing Windows Python does not satisfy WSL mode. On macOS, copy the app to a diff --git a/desktop/README.md b/desktop/README.md index 23b2e09..e1c6c8b 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -15,10 +15,10 @@ not a Gatekeeper-qualified public release. No signing account or private key is needed for a local build. Intel/Rosetta acceptance is not implied. New build filenames pair the independent Desktop and bundled Core versions: -`StandTerm-Desktop-0.5.0-2.12.0-mac-arm64.dmg` and -`StandTerm-Desktop-0.5.0-2.12.0-win32-x64-Setup.exe`. -The [Desktop 0.5.0 evaluation release](https://github.com/askac/standterm/releases/tag/desktop-v0.5.0-2.12.0) -contains paired packages, checksums and validation evidence. Core 2.12.0 remains +`StandTerm-Desktop-0.5.0-2.12.1-mac-arm64.dmg` and +`StandTerm-Desktop-0.5.0-2.12.1-win32-x64-Setup.exe`. +The [Desktop 0.5.0 evaluation release](https://github.com/askac/standterm/releases/tag/desktop-v0.5.0-2.12.1) +contains paired packages, checksums and validation evidence. Core 2.12.1 remains a separate source release; older Desktop releases retain their original files. Staging writes `release-identity.json` from the staged package/lock versions and @@ -33,7 +33,7 @@ The 0.4.5 evaluation source includes updated Core terminal reads, transcript splitting, token-tab countdowns, asynchronous launcher status polling and the experimental IME anchor, plus guarded Desktop clipboard controls. Earlier local 0.4.5 candidates contain Core `2.11.0-dev`; they must not be renamed or presented -as containing Core 2.12.0. Rebuild both platforms and verify their manifests +as containing Core 2.12.1. Rebuild both platforms and verify their manifests before publishing paired installers. Real macOS IME candidate placement and native clipboard/upgrade acceptance remain separate manual checks. @@ -43,6 +43,12 @@ This also keeps frames updating for its owning window; it is not a capture permission grant. Core's own preferences, action focus checks and the existing stop-on-hide/minimize/fullscreen/navigation behavior remain unchanged. +On Windows, Desktop disables Chromium's native window occlusion calculation to +keep the Core view receiving input and producing captures after activation. +Covered windows, other virtual desktops or a locked session may consequently +use more CPU/GPU. Explicit focus checks and hide/minimize capture stops remain +enabled. Recheck this workaround when upgrading Electron. + Native macOS mode is selected automatically, or explicitly with `--backend=macos`. Local shells start as login shells so their usual profiles (for example `~/.zprofile` for zsh) supply MacPorts/Homebrew and user command paths even when @@ -67,7 +73,7 @@ Build on an Apple Silicon Mac with Node 22.12+ and the checkout's macOS venv: ```sh cd desktop npm ci -npm run stage:mac -- --core-ref v2.12.0 +npm run stage:mac -- --core-ref v2.12.1 # Change to the absolute stage directory printed above, then: npm ci npm run make:mac @@ -292,7 +298,7 @@ Permanent disposal of recovery files is a separate, explicit user action. Build tools need Windows Node.js 22.12+ and Git. From the repository: ```powershell -node desktop/stage-windows.cjs --core-ref v2.12.0 +node desktop/stage-windows.cjs --core-ref v2.12.1 # Change to the exact desktop/dist/windows-build-* path printed above. cd npm ci @@ -302,13 +308,13 @@ npm run make:win The staging script creates a new directory each time. `--core-ref` selects Core files and bytes from that Git revision; the Desktop shell comes from the current checkout. Verify a release stage with `node desktop/test/core-release-inspect.cjs - v2.12.0`, then inspect the actual package with `test/package-inspect.cjs`. + v2.12.1`, then inspect the actual package with `test/package-inspect.cjs`. Without `--core-ref`, staging uses tracked working-tree Core files for development snapshots; these are not a claim that changes are already a release. Build staging never copies the development venv or `node_modules`. The new directory gets Windows build dependencies; the source checkout's Linux/WSLg `node_modules` is untouched. -The `StandTerm-Desktop-0.5.0-2.12.0-win32-x64-Setup.exe` is under `out/`; the unpacked +The `StandTerm-Desktop-0.5.0-2.12.1-win32-x64-Setup.exe` is under `out/`; the unpacked application is under `out/win-unpacked/`. Packaging uses [electron-builder's assisted NSIS target](https://www.electron.build/nsis.html), with pinned build dependencies and scoped custom installer hooks. Squirrel @@ -619,7 +625,7 @@ managed Core bundle SHA-256 identity when available. The same Core details are in Diagnostics. Core reports its version from `core_version.py`, independently of the Electron package version. Source checkouts have no managed build identity; older backends that omit version metadata show Unknown, never an inferred Git -tag. The current source pairing is Desktop 0.5.0 / Core 2.12.0. The Core source +tag. The current source pairing is Desktop 0.5.0 / Core 2.12.1. The Core source release does not publish or qualify Desktop installers. The Agent menu and expanded Core payload postdate the published 0.4.1 installer and the earlier macOS 0.4.2 candidate; they require a diff --git a/desktop/main.cjs b/desktop/main.cjs index 4e9f634..091b61b 100644 --- a/desktop/main.cjs +++ b/desktop/main.cjs @@ -58,7 +58,13 @@ try { mode = desktopMode(process.argv); } catch (error) { return; } app.setName('StandTerm Desktop'); -if (process.platform === 'win32') app.setAppUserModelId(`${APP_ID}.${mode}`); +if (process.platform === 'win32') { + app.setAppUserModelId(`${APP_ID}.${mode}`); + // Native occlusion can leave a restored Core view unable to receive input or capture. + // Keep explicit focus, hide and minimize guards; reassess after Electron upgrades. + const disabledFeatures = app.commandLine.getSwitchValue('disable-features'); + app.commandLine.appendSwitch('disable-features', [disabledFeatures, 'CalculateNativeWinOcclusion'].filter(Boolean).join(',')); +} app.enableSandbox(); const captureSmoke = process.argv.includes('--desktop-capture-smoke'); const smoke = process.argv.includes('--desktop-smoke') || captureSmoke;