From f4c68942a52e4a344ad9ef49095ac981aa49552d Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Fri, 19 Jun 2026 14:20:23 +0000 Subject: [PATCH] fix: repair desktop launcher local view --- .github/workflows/release.yml | 25 ++++++++++------ electron/computerAgent.js | 29 ++++++++++++++++++- electron/launcher/launcher.js | 25 +++++++++++----- electron/main.js | 5 ++++ .../computer-semantics.service.ts | 24 ++++++++++----- .../helpers/macos/CloudCLISemantics.swift | 6 ++-- .../view/subcomponents/MainContentTitle.tsx | 4 +-- 7 files changed, 88 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dffa10f7..32ace264 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,9 @@ on: permissions: contents: read +# This workflow publishes releases with write credentials, so actions are pinned +# to immutable commit SHAs. The trailing comments keep the original major tag +# visible for maintenance context. jobs: build-macos-semantic-helper: strategy: @@ -28,8 +31,10 @@ jobs: target_dir: darwin-x64 runs-on: ${{ matrix.runs_on }} steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 - name: Build macOS semantic helper @@ -42,7 +47,7 @@ jobs: run: | mkdir -p "semantic-helper-artifact/${{ matrix.target_dir }}" cp "server/modules/computer-use/semantics/bin/${{ matrix.target_dir }}/CloudCLISemantics" "semantic-helper-artifact/${{ matrix.target_dir }}/" - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: semantic-helper-${{ matrix.target_dir }} path: semantic-helper-artifact/* @@ -59,8 +64,10 @@ jobs: target_dir: win32-arm64 runs-on: ${{ matrix.runs_on }} steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 - name: Build Windows semantic helper @@ -75,7 +82,7 @@ jobs: run: | mkdir -p "semantic-helper-artifact/${{ matrix.target_dir }}" cp "server/modules/computer-use/semantics/bin/${{ matrix.target_dir }}/CloudCLISemantics.exe" "semantic-helper-artifact/${{ matrix.target_dir }}/" - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: semantic-helper-${{ matrix.target_dir }} path: semantic-helper-artifact/* @@ -90,12 +97,12 @@ jobs: contents: write id-token: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 token: ${{ secrets.RELEASE_PAT }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 registry-url: https://registry.npmjs.org @@ -107,7 +114,7 @@ jobs: - run: npm ci - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 with: pattern: semantic-helper-* path: server/modules/computer-use/semantics/bin diff --git a/electron/computerAgent.js b/electron/computerAgent.js index d832ca84..b28cea6e 100644 --- a/electron/computerAgent.js +++ b/electron/computerAgent.js @@ -52,6 +52,8 @@ export class ComputerAgentController { this.connectedUrls = new Set(); this.currentTargets = []; this.stdoutBuffer = ''; + this.lastEvent = null; + this.lastError = null; } getSettings() { @@ -65,6 +67,9 @@ export class ComputerAgentController { running: Boolean(this.child), connectedCount: this.connectedUrls.size, targetCount: this.currentTargets.length, + targetUrls: [...this.currentTargets], + lastEvent: this.lastEvent, + lastError: this.lastError, }; } @@ -105,6 +110,7 @@ export class ComputerAgentController { if (!this.settings.enabled || wsTargets.length === 0) { this.stop(); this.currentTargets = []; + this.lastEvent = this.settings.enabled ? 'no-targets' : 'disabled'; return; } @@ -113,6 +119,8 @@ export class ComputerAgentController { } this.currentTargets = wsTargets; + this.lastEvent = 'restarting'; + this.lastError = null; this.restart(wsTargets); } @@ -140,6 +148,8 @@ export class ComputerAgentController { this.child.once('error', (error) => { console.error('[ComputerAgent] failed to start:', error.message); + this.lastEvent = 'start-error'; + this.lastError = error.message; this.child = null; this.onChange?.(); }); @@ -147,12 +157,16 @@ export class ComputerAgentController { this.child.stdout?.on('data', (chunk) => this.handleStdout(String(chunk))); this.child.stderr?.on('data', (chunk) => { for (const line of String(chunk).split(/\r?\n/)) { - if (line.trim()) console.error('[ComputerAgent]', line); + if (line.trim()) { + this.lastError = line.trim(); + console.error('[ComputerAgent]', line); + } } }); this.child.once('exit', (code) => { console.log(`[ComputerAgent] exited (code ${code ?? 'null'})`); + this.lastEvent = `exit:${code ?? 'null'}`; this.child = null; this.connectedUrls = new Set(); this.onChange?.(); @@ -185,10 +199,23 @@ export class ComputerAgentController { switch (payload.type) { case 'connected': this.connectedUrls.add(payload.url); + this.lastEvent = 'connected'; + this.lastError = null; this.onChange?.(); break; case 'disconnected': this.connectedUrls.delete(payload.url); + this.lastEvent = 'disconnected'; + this.onChange?.(); + break; + case 'starting': + this.lastEvent = 'starting'; + this.lastError = null; + this.onChange?.(); + break; + case 'error': + this.lastEvent = 'error'; + this.lastError = payload.message || 'Computer agent error.'; this.onChange?.(); break; case 'consent-request': { diff --git a/electron/launcher/launcher.js b/electron/launcher/launcher.js index f4b02d87..11bf6226 100644 --- a/electron/launcher/launcher.js +++ b/electron/launcher/launcher.js @@ -181,19 +181,18 @@ window.__MOCK_STATE__ = { function computerUseStatus(state) { var computerUse = state && state.computerUse ? state.computerUse : {}; + var connectedCount = computerUse.connectedCount || 0; + var environmentLabel = connectedCount + ' environment' + (connectedCount === 1 ? '' : 's'); if (!computerUse.enabled) { return { label: 'Disabled', tone: 'idle', detail: 'CloudCLI cannot use this computer.' }; } - if (!computerUse.targetCount) { - return { label: 'Not linked', tone: 'warn', detail: 'No running cloud environment found for this account.' }; - } - if (!computerUse.connectedCount) { - return { label: 'Connecting', tone: 'warn', detail: 'Trying to link to ' + computerUse.targetCount + ' running cloud environment' + (computerUse.targetCount === 1 ? '' : 's') + '.' }; + if (!connectedCount) { + return { label: 'Not connected', tone: 'warn', detail: 'No environment connected.' }; } if (computerUse.consentMode === 'auto') { - return { label: 'Linked', tone: 'warn', detail: 'Unattended access is on for ' + computerUse.connectedCount + ' cloud environment' + (computerUse.connectedCount === 1 ? '' : 's') + '.' }; + return { label: 'Connected', tone: 'warn', detail: environmentLabel + ' connected. Unattended access is on.' }; } - return { label: 'Linked', tone: 'ok', detail: 'Approval prompts are ready for ' + computerUse.connectedCount + ' cloud environment' + (computerUse.connectedCount === 1 ? '' : 's') + '.' }; + return { label: 'Connected', tone: 'ok', detail: environmentLabel + ' connected.' }; } var CC = { @@ -342,6 +341,11 @@ window.__MOCK_STATE__ = { case 'set-theme-mode': return CC.run('Saved', function () { return bridge.updateSetting('themeMode', node.value); }); case 'set-computer-mode': + CC.state.computerUse = { + ...((CC.state && CC.state.computerUse) || {}), + enabled: true, + consentMode: node.value === 'auto' ? 'auto' : 'ask', + }; return CC.run('Saved', function () { return bridge.updateComputerUse({ enabled: true, @@ -349,6 +353,10 @@ window.__MOCK_STATE__ = { }); }); case 'set-computer-enabled': + CC.state.computerUse = { + ...((CC.state && CC.state.computerUse) || {}), + enabled: !!node.value, + }; return CC.run('Saved', function () { var current = (CC.state && CC.state.computerUse) || { consentMode: 'ask' }; return bridge.updateComputerUse({ @@ -731,10 +739,11 @@ window.__MOCK_STATE__ = { } function localPane(state) { + var computerStatus = CC.computerUseStatus(state); return '

Local servers

Manage Local CloudCLI on this machine. No account required.

' + '
Local server
' + CC.esc(CC.localUrl(state) || 'Starts on demand') + '
' + '
' + - '
Computer Use
' + CC.esc(computerUseStatus(state).detail) + '
' + CC.esc(computerUseStatus(state).label) + '
' + + '
Computer Use
' + CC.esc(computerStatus.detail) + '
' + CC.esc(computerStatus.label) + '
' + '
'; } diff --git a/electron/main.js b/electron/main.js index 08e2209f..2e647ae3 100644 --- a/electron/main.js +++ b/electron/main.js @@ -278,6 +278,11 @@ function getDiagnosticsText() { cloudConnected: Boolean(cloudAccount?.apiKey), cloudEmail: cloudAccount?.email || null, cloudEnvironmentCount: cloud.getEnvironments().length, + cloudRunningEnvironmentCount: getRunningEnvironmentUrls().length, + cloudAuthState: cloud.getAuthState(), + computerUse: computerAgent?.getState() || null, + computerUseSettingsPath: getComputerUseSettingsPath(), + cloudAccountPath: getStorePath(), controlPlaneUrl: CLOUDCLI_CONTROL_PLANE_URL, }, null, 2); } diff --git a/server/modules/computer-use/computer-semantics.service.ts b/server/modules/computer-use/computer-semantics.service.ts index d6a8eb05..b43f3df4 100644 --- a/server/modules/computer-use/computer-semantics.service.ts +++ b/server/modules/computer-use/computer-semantics.service.ts @@ -24,6 +24,14 @@ function readString(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } +function requireApp(input: Record): string { + const app = readString(input.app); + if (!app) { + throw new Error('app is required.'); + } + return app; +} + function readNumber(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) ? value : undefined; } @@ -102,7 +110,7 @@ function getHelperAdapter(): SemanticAdapter | null { function shouldFallbackFromHelper(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); - return /not implemented|unavailable|not found|does not exist/i.test(message); + return /not implemented|unavailable|not found|does not exist|timed out|not running|exited with code|failed to start/i.test(message); } async function withHelperState( @@ -347,7 +355,7 @@ export const computerSemanticsService = { return getAppState(sessionId, readString(input.app)); case 'click': case 'click_element': { - const app = readString(input.app); + const app = requireApp(input); const helperState = await withHelperState(sessionId, (adapter) => adapter.clickElement({ ...input, sessionId, app })); if (helperState) { return helperState; @@ -366,7 +374,7 @@ export const computerSemanticsService = { return getAppState(sessionId, app); } case 'drag': { - const app = readString(input.app); + const app = requireApp(input); const helperState = await withHelperState(sessionId, (adapter) => adapter.drag({ ...input, sessionId, app })); if (helperState) { return helperState; @@ -384,7 +392,7 @@ export const computerSemanticsService = { } case 'scroll': case 'scroll_element': { - const app = readString(input.app); + const app = requireApp(input); const helperState = await withHelperState(sessionId, (adapter) => adapter.scrollElement({ ...input, sessionId, app })); if (helperState) { return helperState; @@ -398,7 +406,7 @@ export const computerSemanticsService = { return getAppState(sessionId, app); } case 'type_text': { - const app = readString(input.app); + const app = requireApp(input); const helperState = await withHelperState(sessionId, (adapter) => adapter.typeText({ ...input, sessionId, app })); if (helperState) { return helperState; @@ -407,7 +415,7 @@ export const computerSemanticsService = { return getAppState(sessionId, app); } case 'press_key': { - const app = readString(input.app); + const app = requireApp(input); const helperState = await withHelperState(sessionId, (adapter) => adapter.pressKey({ ...input, sessionId, app })); if (helperState) { return helperState; @@ -416,7 +424,7 @@ export const computerSemanticsService = { return getAppState(sessionId, app); } case 'set_value': { - const app = readString(input.app); + const app = requireApp(input); const helperState = await withHelperState(sessionId, (adapter) => adapter.setValue({ ...input, sessionId, app })); if (helperState) { return helperState; @@ -432,7 +440,7 @@ export const computerSemanticsService = { return getAppState(sessionId, app); } case 'perform_secondary_action': { - const app = readString(input.app); + const app = requireApp(input); const helperState = await withHelperState(sessionId, (adapter) => adapter.performSecondaryAction({ ...input, sessionId, app })); if (helperState) { return helperState; diff --git a/server/modules/computer-use/semantics/helpers/macos/CloudCLISemantics.swift b/server/modules/computer-use/semantics/helpers/macos/CloudCLISemantics.swift index be55aabb..780c5f0c 100644 --- a/server/modules/computer-use/semantics/helpers/macos/CloudCLISemantics.swift +++ b/server/modules/computer-use/semantics/helpers/macos/CloudCLISemantics.swift @@ -74,8 +74,10 @@ func bounds(_ element: AXUIElement) -> [String: Double]? { var point = CGPoint.zero var size = CGSize.zero - guard AXValueGetValue(positionValue as! AXValue, .cgPoint, &point), - AXValueGetValue(sizeValue as! AXValue, .cgSize, &size) + guard let positionAxValue = positionValue as? AXValue, + let sizeAxValue = sizeValue as? AXValue, + AXValueGetValue(positionAxValue, .cgPoint, &point), + AXValueGetValue(sizeAxValue, .cgSize, &size) else { return nil } return [ diff --git a/src/components/main-content/view/subcomponents/MainContentTitle.tsx b/src/components/main-content/view/subcomponents/MainContentTitle.tsx index 2ac2c273..d492de78 100644 --- a/src/components/main-content/view/subcomponents/MainContentTitle.tsx +++ b/src/components/main-content/view/subcomponents/MainContentTitle.tsx @@ -29,11 +29,11 @@ function getTabTitle(activeTab: AppTab, shouldShowTasksTab: boolean, t: (key: st } if (activeTab === 'browser') { - return 'Browser'; + return t('tabs.browser'); } if (activeTab === 'computer') { - return 'Computer Use'; + return t('tabs.computer'); } return 'Project';