mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-28 23:35:27 +08:00
Harden desktop workflows and computer use handling
This commit is contained in:
@@ -14,12 +14,13 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -73,7 +74,7 @@ jobs:
|
|||||||
cat release/SHASUMS256.txt
|
cat release/SHASUMS256.txt
|
||||||
|
|
||||||
- name: Upload branch build artifacts
|
- name: Upload branch build artifacts
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.artifact.outputs.name }}
|
name: ${{ steps.artifact.outputs.name }}
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
14
.github/workflows/desktop-macos-release.yml
vendored
14
.github/workflows/desktop-macos-release.yml
vendored
@@ -25,12 +25,13 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -43,14 +44,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Resolve release metadata
|
- name: Resolve release metadata
|
||||||
id: release
|
id: release
|
||||||
|
env:
|
||||||
|
TAG_INPUT: ${{ inputs.tag }}
|
||||||
|
RELEASE_NAME_INPUT: ${{ inputs.release_name }}
|
||||||
run: |
|
run: |
|
||||||
VERSION="$(node -p "require('./package.json').version")"
|
VERSION="$(node -p "require('./package.json').version")"
|
||||||
TAG="${{ inputs.tag }}"
|
TAG="$TAG_INPUT"
|
||||||
if [ -z "$TAG" ]; then
|
if [ -z "$TAG" ]; then
|
||||||
TAG="v${VERSION}"
|
TAG="v${VERSION}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RELEASE_NAME="${{ inputs.release_name }}"
|
RELEASE_NAME="$RELEASE_NAME_INPUT"
|
||||||
if [ -z "$RELEASE_NAME" ]; then
|
if [ -z "$RELEASE_NAME" ]; then
|
||||||
RELEASE_NAME="CloudCLI Desktop macOS ${TAG}"
|
RELEASE_NAME="CloudCLI Desktop macOS ${TAG}"
|
||||||
fi
|
fi
|
||||||
@@ -93,7 +97,7 @@ jobs:
|
|||||||
cat release/SHASUMS256.txt
|
cat release/SHASUMS256.txt
|
||||||
|
|
||||||
- name: Publish GitHub release assets
|
- name: Publish GitHub release assets
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.release.outputs.tag }}
|
tag_name: ${{ steps.release.outputs.tag }}
|
||||||
target_commitish: ${{ github.sha }}
|
target_commitish: ${{ github.sha }}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v6
|
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(
|
await fs.writeFile(
|
||||||
path.join(stageDir, 'package.json'),
|
path.join(stageDir, 'package.json'),
|
||||||
`${JSON.stringify(buildDesktopPackageJson(copiedOptionalDependencies), null, 2)}\n`,
|
`${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 };
|
return { ...(await snapshot(target)), position, cursor: position };
|
||||||
}
|
}
|
||||||
case 'mouse_move':
|
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 };
|
return { ...(await snapshot(target)), cursor: point };
|
||||||
case 'click':
|
case 'click':
|
||||||
await executor.click(target, (params.button as ClickButton) || 'left', point, params.double === true);
|
await executor.click(target, (params.button as ClickButton) || 'left', point, params.double === true);
|
||||||
return { ...(await snapshot(target)), cursor: point ?? null };
|
return { ...(await snapshot(target)), cursor: point ?? null };
|
||||||
case 'drag':
|
case 'drag': {
|
||||||
await executor.drag(target, asPoint(params.from) as Point, asPoint(params.to) as Point, (params.button as ClickButton) || 'left');
|
const from = asPoint(params.from);
|
||||||
return { ...(await snapshot(target)), cursor: asPoint(params.to) ?? null };
|
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':
|
case 'type':
|
||||||
await executor.type(String(params.text ?? ''));
|
await executor.type(String(params.text ?? ''));
|
||||||
return snapshot(target);
|
return snapshot(target);
|
||||||
|
|||||||
@@ -376,12 +376,13 @@ process.stdin.on('data', (chunk) => {
|
|||||||
buffer = buffer.slice(messageEnd);
|
buffer = buffer.slice(messageEnd);
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const request = JSON.parse(rawMessage) as JsonRpcRequest;
|
let request: JsonRpcRequest | null = null;
|
||||||
try {
|
try {
|
||||||
|
request = JSON.parse(rawMessage) as JsonRpcRequest;
|
||||||
const result = await handleMessage(request);
|
const result = await handleMessage(request);
|
||||||
sendResult(request.id, result);
|
sendResult(request.id, result);
|
||||||
} catch (error) {
|
} 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) => {
|
router.post('/sessions/:sessionId/click', async (req: AuthenticatedRequest, res) => {
|
||||||
try {
|
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), {
|
const session = await computerUseService.userClick(requireUser(req), readParam(req.params.sessionId), {
|
||||||
x: Number(req.body?.x),
|
x,
|
||||||
y: Number(req.body?.y),
|
y,
|
||||||
button: toButton(req.body?.button),
|
button: toButton(req.body?.button),
|
||||||
double: req.body?.double === true,
|
double: req.body?.double === true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { createRequire } from 'node:module';
|
|
||||||
import { randomBytes, randomUUID } from 'node:crypto';
|
import { randomBytes, randomUUID } from 'node:crypto';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default function ComputerUsePanel({ isVisible }: ComputerUsePanelProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
|
setError(null);
|
||||||
const [statusResponse, sessionsResponse] = await Promise.all([
|
const [statusResponse, sessionsResponse] = await Promise.all([
|
||||||
authenticatedFetch('/api/computer-use/status'),
|
authenticatedFetch('/api/computer-use/status'),
|
||||||
authenticatedFetch('/api/computer-use/sessions'),
|
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'));
|
void refresh().catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Computer Use'));
|
||||||
}, [isVisible, refresh]);
|
}, [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.
|
// Poll while an active session exists so agent-driven changes show up live.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVisible || !selectedSession || selectedSession.status !== 'ready') return;
|
if (!isVisible || !selectedSession || selectedSession.status !== 'ready') return;
|
||||||
@@ -273,7 +278,12 @@ export default function ComputerUsePanel({ isVisible }: ComputerUsePanelProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<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" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ function MainContent({
|
|||||||
const { currentProject, setCurrentProject } = useTaskMaster() as TaskMasterContextValue;
|
const { currentProject, setCurrentProject } = useTaskMaster() as TaskMasterContextValue;
|
||||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue;
|
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue;
|
||||||
const [browserUseEnabled, setBrowserUseEnabled] = useState(false);
|
const [browserUseEnabled, setBrowserUseEnabled] = useState(false);
|
||||||
const [computerUseEnabled, setComputerUseEnabled] = useState(false);
|
const [computerUseEnabled, setComputerUseEnabled] = useState<boolean | undefined>(undefined);
|
||||||
|
|
||||||
const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
|
const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
|
||||||
const shouldShowBrowserTab = browserUseEnabled;
|
const shouldShowBrowserTab = browserUseEnabled;
|
||||||
const shouldShowComputerTab = computerUseEnabled;
|
const shouldShowComputerTab = computerUseEnabled === true;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
editingFile,
|
editingFile,
|
||||||
@@ -136,10 +136,10 @@ function MainContent({
|
|||||||
}, [loadComputerUseSettings]);
|
}, [loadComputerUseSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldShowComputerTab && activeTab === 'computer') {
|
if (computerUseEnabled === false && activeTab === 'computer') {
|
||||||
setActiveTab('chat');
|
setActiveTab('chat');
|
||||||
}
|
}
|
||||||
}, [shouldShowComputerTab, activeTab, setActiveTab]);
|
}, [computerUseEnabled, activeTab, setActiveTab]);
|
||||||
|
|
||||||
usePaletteOpsRegister({
|
usePaletteOpsRegister({
|
||||||
openFile: (filePath: string) => {
|
openFile: (filePath: string) => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"git": "Система контроля версий",
|
"git": "Система контроля версий",
|
||||||
"tasks": "Задачи",
|
"tasks": "Задачи",
|
||||||
"browser": "Browser",
|
"browser": "Browser",
|
||||||
"computer": "Computer"
|
"computer": "Компьютер"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"git": "Kaynak Kontrolü",
|
"git": "Kaynak Kontrolü",
|
||||||
"tasks": "Görevler",
|
"tasks": "Görevler",
|
||||||
"browser": "Browser",
|
"browser": "Browser",
|
||||||
"computer": "Computer"
|
"computer": "Bilgisayar"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Yükleniyor...",
|
"loading": "Yükleniyor...",
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
"files": "文件",
|
"files": "文件",
|
||||||
"git": "源代码管理",
|
"git": "源代码管理",
|
||||||
"tasks": "任务",
|
"tasks": "任务",
|
||||||
"browser": "Browser",
|
"browser": "浏览器",
|
||||||
"computer": "Computer"
|
"computer": "计算机"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
|
|||||||
Reference in New Issue
Block a user