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}
+
)}
-
+
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 && (