From 2af3d38afe035662ef078f7dc6e74271cc7370e5 Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Fri, 19 Jun 2026 06:21:13 +0000 Subject: [PATCH] Harden desktop workflows and computer use handling --- .../workflows/desktop-macos-branch-build.yml | 7 ++++--- .github/workflows/desktop-macos-release.yml | 14 +++++++++----- .../workflows/desktop-windows-branch-build.yml | 1 + scripts/release/prepare-desktop-app.js | 12 ++++++++++++ server/computer-use-agent.ts | 17 +++++++++++++---- server/computer-use-mcp.ts | 5 +++-- .../modules/computer-use/computer-use.routes.ts | 14 ++++++++++++-- .../computer-use/computer-use.service.ts | 1 - .../computer-use/view/ComputerUsePanel.tsx | 12 +++++++++++- .../main-content/view/MainContent.tsx | 8 ++++---- src/i18n/locales/ru/common.json | 2 +- src/i18n/locales/tr/common.json | 2 +- src/i18n/locales/zh-CN/common.json | 4 ++-- 13 files changed, 73 insertions(+), 26 deletions(-) diff --git a/.github/workflows/desktop-macos-branch-build.yml b/.github/workflows/desktop-macos-branch-build.yml index 5222fcfd..2eaa6309 100644 --- a/.github/workflows/desktop-macos-branch-build.yml +++ b/.github/workflows/desktop-macos-branch-build.yml @@ -14,12 +14,13 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 cache: npm @@ -73,7 +74,7 @@ jobs: cat release/SHASUMS256.txt - name: Upload branch build artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: ${{ steps.artifact.outputs.name }} path: | diff --git a/.github/workflows/desktop-macos-release.yml b/.github/workflows/desktop-macos-release.yml index 033b3212..826b3c12 100644 --- a/.github/workflows/desktop-macos-release.yml +++ b/.github/workflows/desktop-macos-release.yml @@ -25,12 +25,13 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 cache: npm @@ -43,14 +44,17 @@ jobs: - name: Resolve release metadata id: release + env: + TAG_INPUT: ${{ inputs.tag }} + RELEASE_NAME_INPUT: ${{ inputs.release_name }} run: | VERSION="$(node -p "require('./package.json').version")" - TAG="${{ inputs.tag }}" + TAG="$TAG_INPUT" if [ -z "$TAG" ]; then TAG="v${VERSION}" fi - RELEASE_NAME="${{ inputs.release_name }}" + RELEASE_NAME="$RELEASE_NAME_INPUT" if [ -z "$RELEASE_NAME" ]; then RELEASE_NAME="CloudCLI Desktop macOS ${TAG}" fi @@ -93,7 +97,7 @@ jobs: cat release/SHASUMS256.txt - name: Publish GitHub release assets - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: tag_name: ${{ steps.release.outputs.tag }} target_commitish: ${{ github.sha }} diff --git a/.github/workflows/desktop-windows-branch-build.yml b/.github/workflows/desktop-windows-branch-build.yml index e30b060d..6f168ba5 100644 --- a/.github/workflows/desktop-windows-branch-build.yml +++ b/.github/workflows/desktop-windows-branch-build.yml @@ -17,6 +17,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v6 diff --git a/scripts/release/prepare-desktop-app.js b/scripts/release/prepare-desktop-app.js index d458b1c7..7e33c96d 100644 --- a/scripts/release/prepare-desktop-app.js +++ b/scripts/release/prepare-desktop-app.js @@ -133,6 +133,18 @@ for (const [name, version] of Object.entries(packageJson.optionalDependencies || } } +for (const name of [ + '@nut-tree-fork/default-clipboard-provider', + '@nut-tree-fork/libnut', + '@nut-tree-fork/provider-interfaces', + '@nut-tree-fork/shared', + 'jimp', + 'node-abort-controller', + 'temp', +]) { + await copyNodeModule(name); +} + await fs.writeFile( path.join(stageDir, 'package.json'), `${JSON.stringify(buildDesktopPackageJson(copiedOptionalDependencies), null, 2)}\n`, diff --git a/server/computer-use-agent.ts b/server/computer-use-agent.ts index e39fd0dc..64905862 100644 --- a/server/computer-use-agent.ts +++ b/server/computer-use-agent.ts @@ -141,14 +141,23 @@ async function runAction(type: string, params: Record): Promise return { ...(await snapshot(target)), position, cursor: position }; } case 'mouse_move': - await executor.moveTo(target, point as Point); + if (!point) { + throw new Error('mouse_move requires a valid point.'); + } + await executor.moveTo(target, point); return { ...(await snapshot(target)), cursor: point }; case 'click': await executor.click(target, (params.button as ClickButton) || 'left', point, params.double === true); return { ...(await snapshot(target)), cursor: point ?? null }; - case 'drag': - await executor.drag(target, asPoint(params.from) as Point, asPoint(params.to) as Point, (params.button as ClickButton) || 'left'); - return { ...(await snapshot(target)), cursor: asPoint(params.to) ?? null }; + case 'drag': { + const from = asPoint(params.from); + const to = asPoint(params.to); + if (!from || !to) { + throw new Error('drag requires valid from and to points.'); + } + await executor.drag(target, from, to, (params.button as ClickButton) || 'left'); + return { ...(await snapshot(target)), cursor: to }; + } case 'type': await executor.type(String(params.text ?? '')); return snapshot(target); diff --git a/server/computer-use-mcp.ts b/server/computer-use-mcp.ts index 807c8c16..0d723e69 100644 --- a/server/computer-use-mcp.ts +++ b/server/computer-use-mcp.ts @@ -376,12 +376,13 @@ process.stdin.on('data', (chunk) => { buffer = buffer.slice(messageEnd); void (async () => { - const request = JSON.parse(rawMessage) as JsonRpcRequest; + let request: JsonRpcRequest | null = null; try { + request = JSON.parse(rawMessage) as JsonRpcRequest; const result = await handleMessage(request); sendResult(request.id, result); } catch (error) { - sendError(request.id, error); + sendError(request?.id ?? null, error); } })(); } diff --git a/server/modules/computer-use/computer-use.routes.ts b/server/modules/computer-use/computer-use.routes.ts index b9745fd3..9d791217 100644 --- a/server/modules/computer-use/computer-use.routes.ts +++ b/server/modules/computer-use/computer-use.routes.ts @@ -125,9 +125,19 @@ router.post('/sessions/:sessionId/screenshot', async (req: AuthenticatedRequest, router.post('/sessions/:sessionId/click', async (req: AuthenticatedRequest, res) => { try { + const x = Number(req.body?.x); + const y = Number(req.body?.y); + if (!Number.isFinite(x) || !Number.isFinite(y)) { + res.status(400).json({ + success: false, + error: 'Valid numeric coordinates are required.', + }); + return; + } + const session = await computerUseService.userClick(requireUser(req), readParam(req.params.sessionId), { - x: Number(req.body?.x), - y: Number(req.body?.y), + x, + y, button: toButton(req.body?.button), double: req.body?.double === true, }); diff --git a/server/modules/computer-use/computer-use.service.ts b/server/modules/computer-use/computer-use.service.ts index 937c7d8c..60892be9 100644 --- a/server/modules/computer-use/computer-use.service.ts +++ b/server/modules/computer-use/computer-use.service.ts @@ -1,4 +1,3 @@ -import { createRequire } from 'node:module'; import { randomBytes, randomUUID } from 'node:crypto'; import { spawn } from 'node:child_process'; import fs from 'node:fs'; diff --git a/src/components/computer-use/view/ComputerUsePanel.tsx b/src/components/computer-use/view/ComputerUsePanel.tsx index 589cb28f..3db62958 100644 --- a/src/components/computer-use/view/ComputerUsePanel.tsx +++ b/src/components/computer-use/view/ComputerUsePanel.tsx @@ -67,6 +67,7 @@ export default function ComputerUsePanel({ isVisible }: ComputerUsePanelProps) { ); const refresh = useCallback(async () => { + setError(null); const [statusResponse, sessionsResponse] = await Promise.all([ authenticatedFetch('/api/computer-use/status'), authenticatedFetch('/api/computer-use/sessions'), @@ -87,6 +88,10 @@ export default function ComputerUsePanel({ isVisible }: ComputerUsePanelProps) { void refresh().catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Computer Use')); }, [isVisible, refresh]); + const handleRefresh = useCallback(() => { + void refresh().catch((err) => setError(err instanceof Error ? err.message : 'Failed to refresh Computer Use')); + }, [refresh]); + // Poll while an active session exists so agent-driven changes show up live. useEffect(() => { if (!isVisible || !selectedSession || selectedSession.status !== 'ready') return; @@ -273,7 +278,12 @@ export default function ComputerUsePanel({ isVisible }: ComputerUsePanelProps) {

- diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx index 839f52f8..d06b2f59 100644 --- a/src/components/main-content/view/MainContent.tsx +++ b/src/components/main-content/view/MainContent.tsx @@ -59,11 +59,11 @@ function MainContent({ const { currentProject, setCurrentProject } = useTaskMaster() as TaskMasterContextValue; const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue; const [browserUseEnabled, setBrowserUseEnabled] = useState(false); - const [computerUseEnabled, setComputerUseEnabled] = useState(false); + const [computerUseEnabled, setComputerUseEnabled] = useState(undefined); const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled); const shouldShowBrowserTab = browserUseEnabled; - const shouldShowComputerTab = computerUseEnabled; + const shouldShowComputerTab = computerUseEnabled === true; const { editingFile, @@ -136,10 +136,10 @@ function MainContent({ }, [loadComputerUseSettings]); useEffect(() => { - if (!shouldShowComputerTab && activeTab === 'computer') { + if (computerUseEnabled === false && activeTab === 'computer') { setActiveTab('chat'); } - }, [shouldShowComputerTab, activeTab, setActiveTab]); + }, [computerUseEnabled, activeTab, setActiveTab]); usePaletteOpsRegister({ openFile: (filePath: string) => { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index fbde3d5f..a1ce1c34 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -24,7 +24,7 @@ "git": "Система контроля версий", "tasks": "Задачи", "browser": "Browser", - "computer": "Computer" + "computer": "Компьютер" }, "status": { "loading": "Загрузка...", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index e33ed02b..4b917063 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -24,7 +24,7 @@ "git": "Kaynak Kontrolü", "tasks": "Görevler", "browser": "Browser", - "computer": "Computer" + "computer": "Bilgisayar" }, "status": { "loading": "Yükleniyor...", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index ac9bd9c1..7295f6b0 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -23,8 +23,8 @@ "files": "文件", "git": "源代码管理", "tasks": "任务", - "browser": "Browser", - "computer": "Computer" + "browser": "浏览器", + "computer": "计算机" }, "status": { "loading": "加载中...",