From 2c2fa59f9485e5ea2ae5ec4322da12036ccae464 Mon Sep 17 00:00:00 2001 From: oratis Date: Thu, 28 May 2026 18:31:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20M7=20=E2=80=94=20Monaco=20file?= =?UTF-8?q?=20panel=20with=20Source=20/=20Diff=20/=20History=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FilePanel screen is now a real Monaco-based file viewer. · apps/desktop/src/screens/FilePanel.tsx - Source: monaco-editor read-only view, syntax highlighting per file extension (ts/tsx/js/jsx/json/md/py/rs/go/rb/sh/yml/html/ css/sql/toml + plaintext fallback). - Diff: monaco-editor's DiffEditor showing the file vs `git show HEAD:` so the user sees uncommitted changes inline. - History: `git log --pretty=format` queried via tool_bash; renders hash / date / subject table. - Tab close + view switcher (top bar) + per-tab state. - "Open README.md" button on empty state — invokes tool_read. - Custom Monaco theme "deepcode-dark" matching the rest of the UI. · apps/desktop/package.json — added @monaco-editor/react + monaco-editor (monaco itself is ~1.7 MB minified but lazy-loaded on first FilePanel render). Tests: 549 still passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/desktop/package.json | 2 + apps/desktop/src/screens/FilePanel.tsx | 185 +++++++++++++++++++++++-- pnpm-lock.yaml | 60 ++++++++ 3 files changed, 234 insertions(+), 13 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6f0ee36..af2b263 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -21,6 +21,7 @@ "dependencies": { "@deepcode/core": "workspace:*", "@deepcode/shared-ui": "workspace:*", + "@monaco-editor/react": "^4.7.0", "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-fs": "^2.0.0", @@ -31,6 +32,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", + "monaco-editor": "^0.55.1", "react": "^18.3.0", "react-dom": "^18.3.0" }, diff --git a/apps/desktop/src/screens/FilePanel.tsx b/apps/desktop/src/screens/FilePanel.tsx index fe377a8..f41dcc4 100644 --- a/apps/desktop/src/screens/FilePanel.tsx +++ b/apps/desktop/src/screens/FilePanel.tsx @@ -1,31 +1,114 @@ -// File panel — right-side Monaco-based viewer with Source / Diff / History tabs. +// File panel — Monaco-backed file viewer with Source / Diff / History tabs. // Spec: docs/VISUAL_DESIGN.html screens #8 and #11 (M7) -// Milestone: M6-rest skeleton; Monaco wiring lands when the binary dep is installed. -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import Editor, { DiffEditor, type Monaco } from '@monaco-editor/react'; +import { invoke } from '@tauri-apps/api/core'; interface OpenFile { path: string; view: 'source' | 'diff' | 'history'; + content?: string; + /** For diff view — the prior content. */ + baseContent?: string; + error?: string; } +type GitLogEntry = { hash: string; date: string; subject: string }; + export function FilePanel(): JSX.Element { const [files, setFiles] = useState([]); const [active, setActive] = useState(0); + const [history, setHistory] = useState([]); function closeTab(idx: number): void { setFiles((fs) => fs.filter((_, i) => i !== idx)); setActive((a) => Math.max(0, Math.min(a, files.length - 2))); } - function switchView(view: OpenFile['view']): void { + async function switchView(view: OpenFile['view']): Promise { + const current = files[active]; + if (!current) return; + if (view === 'diff' && current.baseContent === undefined) { + try { + const r = (await invoke('tool_bash', { + input: { + command: `git show HEAD:${shellQuote(current.path)} 2>/dev/null || true`, + timeout_ms: 10_000, + }, + })) as { stdout: string }; + setFiles((fs) => + fs.map((f, i) => (i === active ? { ...f, view, baseContent: r.stdout } : f)), + ); + } catch { + setFiles((fs) => fs.map((f, i) => (i === active ? { ...f, view } : f))); + } + return; + } + if (view === 'history') { + try { + const r = (await invoke('tool_bash', { + input: { + command: `git log --pretty=format:'%h%x09%ad%x09%s' --date=short -- ${shellQuote(current.path)} 2>/dev/null | head -50`, + timeout_ms: 10_000, + }, + })) as { stdout: string }; + const entries: GitLogEntry[] = r.stdout + .split('\n') + .filter((line) => line.trim()) + .map((line) => { + const [hash = '', date = '', subject = ''] = line.split('\t'); + return { hash, date, subject }; + }); + setHistory(entries); + } catch { + setHistory([]); + } + } setFiles((fs) => fs.map((f, i) => (i === active ? { ...f, view } : f))); } + // Open a demo file from the cwd + async function openDemo(): Promise { + const path = 'README.md'; + try { + const r = (await invoke('tool_read', { filePath: path })) as { content: string }; + setFiles((fs) => [...fs, { path, view: 'source', content: r.content }]); + setActive(files.length); + } catch (err) { + setFiles((fs) => [ + ...fs, + { path, view: 'source', error: (err as Error).message ?? String(err) }, + ]); + } + } + + // Configure Monaco for our dark theme on first load + function handleMount(_editor: unknown, monaco: Monaco): void { + monaco.editor.defineTheme('deepcode-dark', { + base: 'vs-dark', + inherit: true, + rules: [], + colors: { + 'editor.background': '#0e0e10', + 'editor.foreground': '#f4f4f5', + 'editor.lineHighlightBackground': '#18181b', + 'editorLineNumber.foreground': '#52525b', + 'editor.selectionBackground': '#27272a', + }, + }); + monaco.editor.setTheme('deepcode-dark'); + } + + useEffect(() => { + // History reset on tab switch + setHistory([]); + }, [active]); + if (files.length === 0) { return (
-
File panel · M7
+
File panel

No file open.

@@ -34,9 +117,9 @@ export function FilePanel(): JSX.Element {

@@ -45,6 +128,8 @@ export function FilePanel(): JSX.Element { } const current = files[active]!; + const lang = guessLanguage(current.path); + return (
{/* Tab bar */} @@ -76,7 +161,7 @@ export function FilePanel(): JSX.Element { {(['source', 'diff', 'history'] as const).map((v) => (
- {/* Body — Monaco lives here in M7. Stub shows the placeholder. */} -
-
- Monaco editor mounts here in M7. {current.view} view of {current.path}. -
+ {/* Body */} +
+ {current.error ? ( +
Error: {current.error}
+ ) : current.view === 'source' ? ( + + ) : current.view === 'diff' ? ( + + ) : ( +
+ {history.length === 0 ? ( +

No git history for this file (or not a git repo).

+ ) : ( + + + + + + + + + + {history.map((e) => ( + + + + + + ))} + +
HashDateSubject
{e.hash}{e.date}{e.subject}
+ )} +
+ )}
); } + +function shellQuote(s: string): string { + return `'${s.replace(/'/g, "'\\''")}'`; +} + +function guessLanguage(path: string): string { + const ext = path.split('.').pop()?.toLowerCase() ?? ''; + return ( + { + ts: 'typescript', + tsx: 'typescript', + js: 'javascript', + jsx: 'javascript', + json: 'json', + md: 'markdown', + py: 'python', + rs: 'rust', + go: 'go', + rb: 'ruby', + sh: 'shell', + bash: 'shell', + yml: 'yaml', + yaml: 'yaml', + html: 'html', + css: 'css', + sql: 'sql', + toml: 'toml', + }[ext] ?? 'plaintext' + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c37c611..75ed09e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@deepcode/shared-ui': specifier: workspace:* version: link:../../packages/shared-ui + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tauri-apps/api': specifier: ^2.0.0 version: 2.11.0 @@ -90,6 +93,9 @@ importers: '@xterm/xterm': specifier: ^6.0.0 version: 6.0.0 + monaco-editor: + specifier: ^0.55.1 + version: 0.55.1 react: specifier: ^18.3.0 version: 18.3.1 @@ -508,6 +514,16 @@ packages: '@cfworker/json-schema': optional: true + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -775,6 +791,9 @@ packages: '@types/react@18.3.29': resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/vscode@1.120.0': resolution: {integrity: sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==} @@ -1095,6 +1114,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1468,6 +1490,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1503,6 +1530,9 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + monaco-editor@0.55.1: + resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1812,6 +1842,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -2315,6 +2348,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.55.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2519,6 +2563,9 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/trusted-types@2.0.7': + optional: true + '@types/vscode@1.120.0': {} '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': @@ -2866,6 +2913,10 @@ snapshots: dlv@1.1.3: {} + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3258,6 +3309,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@14.0.0: {} + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -3285,6 +3338,11 @@ snapshots: dependencies: brace-expansion: 1.1.15 + monaco-editor@0.55.1: + dependencies: + dompurify: 3.2.7 + marked: 14.0.0 + ms@2.1.3: {} mz@2.7.0: @@ -3586,6 +3644,8 @@ snapshots: stackback@0.0.2: {} + state-local@1.0.7: {} + statuses@2.0.2: {} std-env@3.10.0: {}