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) {
-