Harden desktop workflows and computer use handling

This commit is contained in:
Simos Mikelatos
2026-06-19 06:21:13 +00:00
parent 531833bc87
commit 2af3d38afe
13 changed files with 73 additions and 26 deletions

View File

@@ -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: |

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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`,

View File

@@ -141,14 +141,23 @@ async function runAction(type: string, params: Record<string, unknown>): 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);

View File

@@ -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);
}
})();
}

View File

@@ -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,
});

View File

@@ -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';

View File

@@ -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) {
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" onClick={() => void refresh()} disabled={isBusy}>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isBusy}
>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>

View File

@@ -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<boolean | undefined>(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) => {

View File

@@ -24,7 +24,7 @@
"git": "Система контроля версий",
"tasks": "Задачи",
"browser": "Browser",
"computer": "Computer"
"computer": "Компьютер"
},
"status": {
"loading": "Загрузка...",

View File

@@ -24,7 +24,7 @@
"git": "Kaynak Kontrolü",
"tasks": "Görevler",
"browser": "Browser",
"computer": "Computer"
"computer": "Bilgisayar"
},
"status": {
"loading": "Yükleniyor...",

View File

@@ -23,8 +23,8 @@
"files": "文件",
"git": "源代码管理",
"tasks": "任务",
"browser": "Browser",
"computer": "Computer"
"browser": "浏览器",
"computer": "计算机"
},
"status": {
"loading": "加载中...",