From 4b513271ee0b7b1992fc461f5a5002dab13f9bad Mon Sep 17 00:00:00 2001 From: Relakkes Yang Date: Mon, 20 Apr 2026 17:59:42 +0800 Subject: [PATCH] fix: improve windows computer use and desktop rendering --- desktop/src-tauri/Cargo.lock | 2 +- .../src/components/chat/CodeViewer.test.tsx | 27 ++++ desktop/src/components/chat/CodeViewer.tsx | 22 ++- desktop/src/components/chat/ToolCallBlock.tsx | 45 +++++- .../src/components/chat/chatBlocks.test.tsx | 13 ++ .../markdown/MarkdownRenderer.test.tsx | 13 ++ .../components/markdown/MarkdownRenderer.tsx | 2 +- desktop/src/i18n/locales/en.ts | 1 + desktop/src/i18n/locales/zh.ts | 1 + desktop/src/pages/ComputerUseSettings.tsx | 25 ++++ runtime/win_helper.py | 68 +++++++-- src/main.tsx | 2 +- .../__tests__/computer-use-python.test.ts | 75 ++++++++++ src/server/api/computer-use-python.ts | 131 ++++++++++++++++++ src/server/api/computer-use.ts | 74 ++++++---- src/utils/computerUse/common.test.ts | 28 ++++ src/utils/computerUse/common.ts | 45 ++++-- src/utils/computerUse/executor.ts | 66 ++++++--- .../computerUse/preauthorizedConfig.test.ts | 32 +++++ src/utils/computerUse/preauthorizedConfig.ts | 33 +++++ src/utils/computerUse/pythonBridge.ts | 11 +- src/utils/computerUse/setup.ts | 7 +- src/utils/computerUse/wrapper.tsx | 95 ++++++++----- src/vendor/computer-use-mcp/toolCalls.ts | 11 +- 24 files changed, 708 insertions(+), 121 deletions(-) create mode 100644 desktop/src/components/chat/CodeViewer.test.tsx create mode 100644 src/server/__tests__/computer-use-python.test.ts create mode 100644 src/server/api/computer-use-python.ts create mode 100644 src/utils/computerUse/common.test.ts create mode 100644 src/utils/computerUse/preauthorizedConfig.test.ts create mode 100644 src/utils/computerUse/preauthorizedConfig.ts diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 5d4a90c2..956b476b 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -323,7 +323,7 @@ dependencies = [ [[package]] name = "claude-code-desktop" -version = "0.1.2" +version = "0.1.3" dependencies = [ "serde", "serde_json", diff --git a/desktop/src/components/chat/CodeViewer.test.tsx b/desktop/src/components/chat/CodeViewer.test.tsx new file mode 100644 index 00000000..9f654f6b --- /dev/null +++ b/desktop/src/components/chat/CodeViewer.test.tsx @@ -0,0 +1,27 @@ +import { render, screen, waitFor } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { CodeViewer } from './CodeViewer' + +vi.mock('react-shiki', () => ({ + ShikiHighlighter: ({ children }: { children: string }) => ( +
+ {children} +
+ ), +})) + +describe('CodeViewer', () => { + it('keeps the same inner padding for highlighted code content', async () => { + const { container } = render( + , + ) + + await waitFor(() => { + expect(screen.getByTestId('shiki-container')).toBeTruthy() + }) + + const contentWrapper = container.querySelector('[data-code-viewer-content]') as HTMLElement | null + expect(contentWrapper).toBeTruthy() + expect(contentWrapper?.style.padding).toBe('0.5rem 12px') + }) +}) diff --git a/desktop/src/components/chat/CodeViewer.tsx b/desktop/src/components/chat/CodeViewer.tsx index cbfd53bd..4b337cde 100644 --- a/desktop/src/components/chat/CodeViewer.tsx +++ b/desktop/src/components/chat/CodeViewer.tsx @@ -45,6 +45,8 @@ const warmCodeTheme = { ], } +const CODE_AREA_PADDING = '0.5rem 12px' + /** * Wraps ShikiHighlighter with a plain-text fallback so the code area * is never empty while the async WASM / language-grammar load is in-flight, @@ -74,13 +76,13 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language? }, [code, language]) return ( -
+
{/* Plain-text fallback shown until Shiki finishes highlighting */} {!loaded && (
       )}
-      
+
renderPreview(toolName, obj, result, t), [obj, result, toolName, t]) const details = useMemo(() => renderDetails(toolName, obj, t), [obj, toolName, t]) @@ -73,11 +78,17 @@ export function ToolCallBlock({ toolName, input, result, compact = false }: Prop )} {result && outputSummary && ( - + {outputSummary} )} - {result?.isError && ( + {result?.isError && ( error )} {expandable && ( @@ -175,12 +186,30 @@ function renderDetails(toolName: string, obj: Record, t?: (key: ) } -function getToolResultSummary(toolName: string, content: unknown, t?: (key: TranslationKey, params?: Record) => string): string { - if (toolName === 'Bash') return '' - +function getToolResultSummary( + toolName: string, + content: unknown, + isError: boolean, + t?: (key: TranslationKey, params?: Record) => string, +): string { const text = extractTextContent(content) if (!text) return '' + if (isError) { + const firstLine = text + .split('\n') + .map((line) => stripAnsi(line).replace(/\s+/g, ' ').trim()) + .find(Boolean) + + if (!firstLine) { + return t?.('tool.error') ?? 'Error' + } + + return firstLine.length <= 72 ? firstLine : `${firstLine.slice(0, 72)}…` + } + + if (toolName === 'Bash') return '' + const lineCount = text.split('\n').length if (lineCount > 1) { return t?.('tool.linesOutput', { count: lineCount }) ?? `${lineCount} lines output` @@ -192,6 +221,10 @@ function getToolResultSummary(toolName: string, content: unknown, t?: (key: Tran return `${compact.slice(0, 36)}…` } +function stripAnsi(value: string): string { + return value.replace(/\x1B\[[0-9;]*m/g, '') +} + function getToolSummary(toolName: string, obj: Record, t?: (key: TranslationKey, params?: Record) => string): string { switch (toolName) { case 'Bash': diff --git a/desktop/src/components/chat/chatBlocks.test.tsx b/desktop/src/components/chat/chatBlocks.test.tsx index 45f00d68..270144e2 100644 --- a/desktop/src/components/chat/chatBlocks.test.tsx +++ b/desktop/src/components/chat/chatBlocks.test.tsx @@ -62,6 +62,19 @@ describe('chat blocks', () => { expect(container.textContent).not.toContain('file-a') }) + it('shows a collapsed error summary for failed bash commands', () => { + const { container } = render( + , + ) + + expect(container.textContent).toContain('Bash') + expect(container.textContent).toContain('fatal: unrecognized argument: --no-stat') + }) + it('expands tool errors so full Computer Use gate messages are readable', () => { const { container } = render( { expect(screen.getByText('Body copy.')).toBeInTheDocument() }) + it('uses semantic code colors for inline code so both themes stay readable', () => { + const { container } = render( + , + ) + + const root = container.firstChild as HTMLDivElement + expect(root).toBeInTheDocument() + expect(root.className).toContain('prose-code:text-[var(--color-code-fg)]') + expect(root.className).toContain('prose-code:bg-[var(--color-code-bg)]') + expect(root.className).not.toContain('prose-code:text-[var(--color-primary-fixed)]') + expect(screen.getByText('claude-sonnet-4-6')).toBeInTheDocument() + }) + it('renders mermaid fenced blocks with the Mermaid renderer', () => { render(B\n```'} />) diff --git a/desktop/src/components/markdown/MarkdownRenderer.tsx b/desktop/src/components/markdown/MarkdownRenderer.tsx index 78a004d2..62b4ccc4 100644 --- a/desktop/src/components/markdown/MarkdownRenderer.tsx +++ b/desktop/src/components/markdown/MarkdownRenderer.tsx @@ -109,7 +109,7 @@ const BASE_PROSE_CLASSES = `markdown-prose prose prose-sm max-w-none text-[var(- prose-headings:text-[var(--color-text-primary)] prose-headings:font-semibold prose-p:my-2 prose-p:leading-relaxed prose-p:break-words - prose-code:text-[13px] prose-code:text-[var(--color-primary-fixed)] prose-code:font-[var(--font-mono)] prose-code:bg-[var(--color-surface-container-high)] prose-code:border prose-code:border-[var(--color-border)] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:hidden prose-code:after:hidden + prose-code:text-[13px] prose-code:text-[var(--color-code-fg)] prose-code:font-[var(--font-mono)] prose-code:bg-[var(--color-code-bg)] prose-code:border prose-code:border-[var(--color-border)] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:hidden prose-code:after:hidden prose-pre:!bg-transparent prose-pre:!p-0 prose-pre:!shadow-none prose-a:text-[var(--color-text-accent)] prose-a:no-underline hover:prose-a:underline prose-strong:text-[var(--color-text-primary)] diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 0a213f5b..6131a5ed 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -272,6 +272,7 @@ export const en = { 'settings.computerUse.setupSuccess': 'Environment setup complete!', 'settings.computerUse.setupFail': 'Setup failed', 'settings.computerUse.allReady': 'All checks passed. Computer Use is ready.', + 'settings.computerUse.downloadPython': 'Download Python 3', 'settings.computerUse.recheckBtn': 'Recheck Status', 'settings.computerUse.requirementsLabel': 'Required packages', 'settings.computerUse.appsTitle': 'Authorized Apps', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 7ade00a8..1fbb596d 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -274,6 +274,7 @@ export const zh: Record = { 'settings.computerUse.setupSuccess': '环境安装完成!', 'settings.computerUse.setupFail': '安装失败', 'settings.computerUse.allReady': '所有检查通过,Computer Use 已就绪。', + 'settings.computerUse.downloadPython': '下载 Python 3', 'settings.computerUse.recheckBtn': '重新检测', 'settings.computerUse.requirementsLabel': '需要的 Python 包', 'settings.computerUse.appsTitle': '已授权应用', diff --git a/desktop/src/pages/ComputerUseSettings.tsx b/desktop/src/pages/ComputerUseSettings.tsx index 9ff7b4fe..beb26552 100644 --- a/desktop/src/pages/ComputerUseSettings.tsx +++ b/desktop/src/pages/ComputerUseSettings.tsx @@ -3,6 +3,10 @@ import { computerUseApi, type ComputerUseStatus, type SetupResult, type Installe import { useTranslation } from '../i18n' type CheckState = 'loading' | 'ready' | 'error' +const PYTHON_DOWNLOAD_URLS: Record = { + darwin: 'https://www.python.org/downloads/macos/', + win32: 'https://www.python.org/downloads/windows/', +} function StatusIcon({ ok }: { ok: boolean | null }) { if (ok === null) { @@ -31,6 +35,15 @@ async function openSystemSettings(pane: 'Privacy_ScreenCapture' | 'Privacy_Acces await computerUseApi.openSettings(pane) } +async function openExternalUrl(url: string) { + try { + const { open } = await import('@tauri-apps/plugin-shell') + await open(url) + } catch { + window.open(url, '_blank', 'noopener,noreferrer') + } +} + export function ComputerUseSettings() { const t = useTranslation() const [status, setStatus] = useState(null) @@ -153,6 +166,9 @@ export function ComputerUseSettings() { const accessibilityNeedsAttention = status?.permissions.accessibility === false const screenRecordingNeedsAttention = status?.permissions.screenRecording === false const screenRecordingReady = status ? status.permissions.screenRecording !== false : null + const pythonDownloadUrl = status + ? PYTHON_DOWNLOAD_URLS[status.platform] ?? 'https://www.python.org/downloads/' + : 'https://www.python.org/downloads/' // Filter apps by search query const filteredApps = useMemo(() => { @@ -297,6 +313,15 @@ export function ComputerUseSettings() { {/* Action buttons */}
+ {!status.python.installed && ( + + )} {!envReady && status.python.installed && (