mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-19 15:32:05 +08:00
Harden desktop workflows and computer use handling
This commit is contained in:
@@ -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: |
|
||||
|
||||
14
.github/workflows/desktop-macos-release.yml
vendored
14
.github/workflows/desktop-macos-release.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"git": "Система контроля версий",
|
||||
"tasks": "Задачи",
|
||||
"browser": "Browser",
|
||||
"computer": "Computer"
|
||||
"computer": "Компьютер"
|
||||
},
|
||||
"status": {
|
||||
"loading": "Загрузка...",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"git": "Kaynak Kontrolü",
|
||||
"tasks": "Görevler",
|
||||
"browser": "Browser",
|
||||
"computer": "Computer"
|
||||
"computer": "Bilgisayar"
|
||||
},
|
||||
"status": {
|
||||
"loading": "Yükleniyor...",
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
"files": "文件",
|
||||
"git": "源代码管理",
|
||||
"tasks": "任务",
|
||||
"browser": "Browser",
|
||||
"computer": "Computer"
|
||||
"browser": "浏览器",
|
||||
"computer": "计算机"
|
||||
},
|
||||
"status": {
|
||||
"loading": "加载中...",
|
||||
|
||||
Reference in New Issue
Block a user