From 5df965d0b43e546e60e36ac24270735499d57f22 Mon Sep 17 00:00:00 2001 From: oratis Date: Tue, 2 Jun 2026 01:17:40 +0800 Subject: [PATCH 1/9] fix(desktop): kill startup cat flash, clear titlebar, clickable file cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues from live tauri:dev review: 1. Startup B&W "cat" flash — in dev, Vite injects index.css via JS, so the first paint was unstyled and the BrandMark (no explicit size, fill=currentColor) ballooned to a full-size black silhouette on white. Add render-blocking critical CSS to index.html (dark surface + pinned .mark box) so the first paint matches the app — no flash. 2. Transparent titlebar overlapped content (sidebar brand / chat-header pills / inspector head clipped). Add a 30px top inset on .app-shell so all columns clear the macOS traffic-light/title region (box-sizing is global border-box; the native title strip stays OS-draggable; no app-region drag so chat text stays selectable). 3. File outputs now get a clickable card. ToolCard gains an `onOpen` affordance; Repl passes it for any tool with a `file_path` (Read/Write/ Edit), wired through App → renderScreen → ReplScreen to fp.open — clicking loads the file into the right-side preview panel. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src/App.tsx | 4 +++ apps/desktop/src/components/ToolCard.tsx | 23 ++++++++++++-- apps/desktop/src/index.css | 32 ++++++++++++++++++++ apps/desktop/src/index.html | 38 ++++++++++++++++++++++++ apps/desktop/src/screens/Repl.tsx | 18 ++++++++++- 5 files changed, 111 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 9a10987..454c25d 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -212,6 +212,7 @@ export function App(): JSX.Element { () => setSessionEpoch((k) => k + 1), handleInspector, resumedMessages, + (path) => void fp.open(path), )} {fp.isOpen && ( @@ -257,6 +258,7 @@ function renderScreen( onTurnComplete: () => void, onInspector: (patch: Partial) => void, initialMessages?: Msg[], + onOpenFile?: (path: string) => void, ): JSX.Element { switch (screen) { case 'chat': @@ -267,6 +269,7 @@ function renderScreen( onTurnComplete={onTurnComplete} initialMessages={initialMessages} onInspector={onInspector} + onOpenFile={onOpenFile} /> ); case 'sessions': @@ -292,6 +295,7 @@ function renderScreen( onTurnComplete={onTurnComplete} initialMessages={initialMessages} onInspector={onInspector} + onOpenFile={onOpenFile} /> ); } diff --git a/apps/desktop/src/components/ToolCard.tsx b/apps/desktop/src/components/ToolCard.tsx index f011ed3..52286cc 100644 --- a/apps/desktop/src/components/ToolCard.tsx +++ b/apps/desktop/src/components/ToolCard.tsx @@ -22,14 +22,31 @@ interface ToolCardProps { body?: ReactNode; /** If true, body is a diff (line-by-line; preserves whitespace strictly). */ diff?: boolean; + /** + * If set, the target becomes a clickable "open preview" affordance — used for + * file tools (Read/Write/Edit) to load the file into the right-side panel. + */ + onOpen?: () => void; } -export function ToolCard({ name, target, status, body, diff }: ToolCardProps): JSX.Element { +export function ToolCard({ name, target, status, body, diff, onOpen }: ToolCardProps): JSX.Element { return ( -
+
▸ {name} - {target && {target}} + {target && + (onOpen ? ( + + ) : ( + {target} + ))} {status && {status.label}}
{body !== undefined &&
{body}
} diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index eaa7283..546b513 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -256,6 +256,13 @@ select { grid-template-rows: 1fr; height: 100vh; background: var(--bg-1); + /* Clear the macOS transparent titlebar (titleBarStyle:Transparent + + hiddenTitle) so the traffic lights + native drag region don't overlap the + sidebar brand / chat-header pills / inspector head. box-sizing is + border-box globally, so the grid fits within 100vh − this inset. The native + title region (top ~28px) stays OS-draggable; we deliberately do NOT add + -webkit-app-region:drag to the shell (it would make chat text unselectable). */ + padding-top: 30px; } /* ──────────────────────────────────────────────────────────────────── */ @@ -775,6 +782,31 @@ select { text-overflow: ellipsis; white-space: nowrap; } +/* File-tool cards: the target is a clickable "open preview" affordance. */ +.tool-card .tc-head .tc-open { + border: none; + background: transparent; + font: inherit; + cursor: pointer; + color: var(--brand); + display: inline-flex; + align-items: center; + gap: 4px; + padding: 1px 6px; + border-radius: 5px; + max-width: 100%; +} +.tool-card .tc-head .tc-open:hover { + background: var(--brand-tint); + color: #b4c2ff; +} +.tool-card .tc-head .tc-open .tc-open-caret { + color: var(--text-3); + font-weight: 700; +} +.tool-card .tc-head .tc-open:hover .tc-open-caret { + color: var(--brand); +} .tool-card .tc-head .badge { margin-left: auto; flex-shrink: 0; diff --git a/apps/desktop/src/index.html b/apps/desktop/src/index.html index 0f79bce..0a2dd74 100644 --- a/apps/desktop/src/index.html +++ b/apps/desktop/src/index.html @@ -4,6 +4,44 @@ DeepCode + +
diff --git a/apps/desktop/src/screens/Repl.tsx b/apps/desktop/src/screens/Repl.tsx index 869de67..a833980 100644 --- a/apps/desktop/src/screens/Repl.tsx +++ b/apps/desktop/src/screens/Repl.tsx @@ -64,6 +64,8 @@ interface ReplScreenProps { * model, mode, recent files, or the todo list change. */ onInspector?: (patch: Partial) => void; + /** Open a file (from a tool card's "preview" affordance) in the file panel. */ + onOpenFile?: (path: string) => void; } /** Tools whose file_path we surface in the inspector's Recent files section. */ @@ -207,6 +209,7 @@ export function ReplScreen({ onTurnComplete, initialMessages, onInspector, + onOpenFile, }: ReplScreenProps): JSX.Element { const [messages, setMessages] = useState(() => initialMessages && initialMessages.length > 0 @@ -607,7 +610,14 @@ export function ReplScreen({
{messages.map((m, i) => - renderMessage(m, i, pendingApproval, handleApproval, i === activeAssistantIdx), + renderMessage( + m, + i, + pendingApproval, + handleApproval, + i === activeAssistantIdx, + onOpenFile, + ), )} {busy && !pendingApproval && !pendingQuestion && ( @@ -825,6 +835,7 @@ function renderMessage( pendingApproval: PendingApproval | null, onApproval: (decision: 'allow' | 'deny' | 'always') => void, isActive: boolean, + onOpenFile?: (path: string) => void, ): JSX.Element | null { if (m.role === 'user') { return ( @@ -877,6 +888,11 @@ function renderMessage( t.status === 'running' ? '… running' : t.status === 'ok' ? '✓ done' : '✕ error', }} body={t.resultText ? truncate(t.resultText, 1500) : undefined} + onOpen={ + onOpenFile && typeof t.input?.file_path === 'string' + ? () => onOpenFile(String(t.input.file_path)) + : undefined + } /> {/* Inline approval — appears right under the relevant tool card */} {pendingApproval && pendingApproval.toolName === t.name && t.status === 'running' && ( From a5a08c323b479e8f85df916d31d5e57db04ff6c4 Mon Sep 17 00:00:00 2001 From: oratis Date: Tue, 2 Jun 2026 22:26:41 +0800 Subject: [PATCH 2/9] fix(desktop): actionable error when a file tool receives empty args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When DeepSeek emits a Write/Edit/Read call with no arguments (typically output-token truncation on a large file before the args stream), the tool returned a cryptic "missing file_path". Now it explains the likely cause (ran out of output tokens — raise Effort / smaller file) or lists the keys that did arrive, which both helps the user and gives the model a usable hint. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src/lib/mac-tools.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/lib/mac-tools.ts b/apps/desktop/src/lib/mac-tools.ts index 716d31d..c5b526f 100644 --- a/apps/desktop/src/lib/mac-tools.ts +++ b/apps/desktop/src/lib/mac-tools.ts @@ -41,6 +41,20 @@ function pickBool(input: Record, ...keys: string[]): boolean | return undefined; } +/** + * Diagnostic suffix for "missing required arg" errors. An empty input almost + * always means the model's tool call was cut off at the output-token limit + * before it emitted any arguments (DeepSeek caps output at ~8k) — surface that + * clearly so the user (and the model, which sees this error) can react. + */ +function describeInput(input: Record): string { + const keys = Object.keys(input); + if (keys.length === 0) { + return ' — the call arrived with NO arguments. The model likely ran out of output tokens before emitting them; raise Effort (try Max) or write a smaller file / split into multiple writes.'; + } + return ` (received keys: ${keys.join(', ')})`; +} + // ────────────────────────────────────────────────────────────────────────── // Read // ────────────────────────────────────────────────────────────────────────── @@ -65,7 +79,7 @@ export const MacReadTool: ToolHandler = { try { const filePath = pickStr(input, 'file_path', 'filePath', 'path'); if (!filePath) { - return { content: 'Error: missing file_path', isError: true }; + return { content: `Error: missing file_path${describeInput(input)}`, isError: true }; } const r = (await invoke('tool_read', { filePath, @@ -111,7 +125,7 @@ export const MacWriteTool: ToolHandler = { const filePath = pickStr(input, 'file_path', 'filePath', 'path'); const content = pickStr(input, 'content', 'text', 'body') ?? ''; if (!filePath) { - return { content: 'Error: missing file_path', isError: true }; + return { content: `Error: missing file_path${describeInput(input)}`, isError: true }; } await invoke('tool_write', { filePath, content }); const lines = content.split('\n').length; @@ -154,7 +168,7 @@ export const MacEditTool: ToolHandler = { const replaceAll = pickBool(input, 'replace_all', 'replaceAll') ?? false; if (!filePath || oldStr === undefined || newStr === undefined) { return { - content: 'Error: missing file_path / old_string / new_string', + content: `Error: missing file_path / old_string / new_string${describeInput(input)}`, isError: true, }; } From bd1c713ca2cefc6e605dcf83c2adfd012c3403f6 Mon Sep 17 00:00:00 2001 From: oratis Date: Tue, 2 Jun 2026 22:26:50 +0800 Subject: [PATCH 3/9] feat(desktop): session titles + archive/delete (Claude Code parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidebar sessions showed raw ids and had no management actions. - Titles: title a brand-new session from its first user message (mac-agent sessionSetTitle) so the sidebar shows a human label immediately instead of the id; the Rust read-time derive remains the fallback. - Archive / Delete: new session_delete + session_archive Rust commands (archive moves the .jsonl into sessions/archived/, excluded from the list; delete removes it — both guarded against path-traversal ids) + tauri-api wrappers. Sidebar rows reveal 🗄/🗑 on hover (delete confirms); removing the active session resets the chat via a new onSessionRemoved callback. - Freshness: the sidebar now polls (8s) + reloads on active-session change so titles and new sessions surface without a remount. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src-tauri/src/commands.rs | 39 ++++++++++++++ apps/desktop/src-tauri/src/lib.rs | 6 ++- apps/desktop/src/App.tsx | 8 +++ apps/desktop/src/components/Sidebar.tsx | 70 ++++++++++++++++++++++++- apps/desktop/src/index.css | 33 ++++++++++++ apps/desktop/src/lib/mac-agent.ts | 22 +++++++- apps/desktop/src/lib/tauri-api.ts | 10 ++++ 7 files changed, 183 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs index a85b57f..b21e1dd 100644 --- a/apps/desktop/src-tauri/src/commands.rs +++ b/apps/desktop/src-tauri/src/commands.rs @@ -382,6 +382,45 @@ pub fn list_sessions() -> Result, String> { Ok(out) } +/// Reject session ids that could escape the sessions directory. +fn safe_session_id(id: &str) -> Result<(), String> { + if id.is_empty() || id.contains('/') || id.contains('\\') || id.contains("..") { + return Err(format!("invalid session id: {id}")); + } + Ok(()) +} + +/// Permanently delete a session's JSONL file. +#[tauri::command] +pub fn session_delete(id: String) -> Result<(), String> { + safe_session_id(&id)?; + let Some(home) = dirs::home_dir() else { + return Err("no home directory".into()); + }; + let path = home + .join(".deepcode") + .join("sessions") + .join(format!("{id}.jsonl")); + std::fs::remove_file(&path).map_err(|e| format!("delete {}: {}", path.display(), e)) +} + +/// Archive a session by moving its JSONL into sessions/archived/ — excluded from +/// list_sessions but recoverable from disk. +#[tauri::command] +pub fn session_archive(id: String) -> Result<(), String> { + safe_session_id(&id)?; + let Some(home) = dirs::home_dir() else { + return Err("no home directory".into()); + }; + let dir = home.join(".deepcode").join("sessions"); + let archived = dir.join("archived"); + std::fs::create_dir_all(&archived) + .map_err(|e| format!("mkdir {}: {}", archived.display(), e))?; + let from = dir.join(format!("{id}.jsonl")); + let to = archived.join(format!("{id}.jsonl")); + std::fs::rename(&from, &to).map_err(|e| format!("archive {}: {}", from.display(), e)) +} + /// Path to the `deepcode` CLI so the GUI can drop users into it for advanced /// workflows. Resolves a globally-installed `deepcode` on PATH (npm i -g /// deepcode-cli). Bundling the CLI inside the .app is separate future work. diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 75d7863..331d998 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -17,8 +17,8 @@ mod tools; use commands::{ append_allow_matcher, cli_path, get_app_info, get_settings_path, list_plugins, list_sessions, list_skills, load_keybindings, load_settings_file, open_url, read_credentials, - save_credentials, save_keybindings, save_settings_file, session_append, session_create, - session_read, session_set_title, + save_credentials, save_keybindings, save_settings_file, session_append, session_archive, + session_create, session_delete, session_read, session_set_title, }; use tools::{tool_bash, tool_edit, tool_glob, tool_grep, tool_read, tool_write}; use tauri::Manager; @@ -46,6 +46,8 @@ pub fn run() { session_append, session_read, session_set_title, + session_delete, + session_archive, list_sessions, list_plugins, list_skills, diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 454c25d..960f6d0 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -203,6 +203,14 @@ export function App(): JSX.Element { setActiveSessionId(null); setSessionEpoch((k) => k + 1); }} + onSessionRemoved={() => { + // The active session was archived/deleted — reset to a fresh chat. + clearAgentHistory(); + setResumedMessages(undefined); + setActiveSessionId(null); + setScreen('repl'); + setSessionEpoch((k) => k + 1); + }} />
{renderScreen( diff --git a/apps/desktop/src/components/Sidebar.tsx b/apps/desktop/src/components/Sidebar.tsx index 040ae10..4aa69d6 100644 --- a/apps/desktop/src/components/Sidebar.tsx +++ b/apps/desktop/src/components/Sidebar.tsx @@ -6,7 +6,13 @@ import { useCallback, useEffect, useState } from 'react'; import { projectName } from '../lib/project.js'; -import { listSessions, sessionSetTitle, type SessionMeta } from '../lib/tauri-api.js'; +import { + listSessions, + sessionArchive, + sessionDelete, + sessionSetTitle, + type SessionMeta, +} from '../lib/tauri-api.js'; import { BrandMark } from './BrandMark.js'; interface SidebarProps { @@ -18,6 +24,8 @@ interface SidebarProps { onNewSession: () => void; /** Triggers a re-show of the folder picker so the user can switch projects. */ onSwitchProject: () => void; + /** Called after the active session is archived/deleted so the parent resets. */ + onSessionRemoved?: (id: string) => void; } type Bucket = 'Today' | 'Yesterday' | 'Earlier'; @@ -43,6 +51,7 @@ export function Sidebar({ onPickSession, onNewSession, onSwitchProject, + onSessionRemoved, }: SidebarProps): JSX.Element { const [sessions, setSessions] = useState([]); const [now, setNow] = useState(Math.floor(Date.now() / 1000)); @@ -56,9 +65,17 @@ export function Sidebar({ .catch(() => setSessions([])); }, []); + // Reload on mount + whenever the active session changes, then poll so a + // session's auto-derived title (set on its first message) and freshly-created + // sessions surface without needing a remount. useEffect(() => { reload(); - const t = setInterval(() => setNow(Math.floor(Date.now() / 1000)), 30_000); + }, [reload, activeSessionId]); + useEffect(() => { + const t = setInterval(() => { + setNow(Math.floor(Date.now() / 1000)); + reload(); + }, 8_000); return () => clearInterval(t); }, [reload]); @@ -72,6 +89,29 @@ export function Sidebar({ } } + async function handleArchive(id: string): Promise { + try { + await sessionArchive(id); + if (id === activeSessionId) onSessionRemoved?.(id); + reload(); + } catch { + /* ignore — session stays listed */ + } + } + + async function handleDelete(id: string, label: string): Promise { + if (!window.confirm(`Delete session "${label}"? This permanently removes its history.`)) { + return; + } + try { + await sessionDelete(id); + if (id === activeSessionId) onSessionRemoved?.(id); + reload(); + } catch { + /* ignore — session stays listed */ + } + } + const grouped: Record = { Today: [], Yesterday: [], @@ -204,6 +244,32 @@ export function Sidebar({ {s.title?.trim() ? s.title : shortTitle(s.id)} )} {relTime(s.updated_at_secs, now)} + {editingId !== s.id && ( + + + + + )}
))}
diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index 546b513..d8672ef 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -364,6 +364,39 @@ select { margin-left: auto; flex-shrink: 0; } +/* Hover-revealed per-session actions (archive / delete) — they replace the + relative-time meta on hover so the row doesn't get crowded. */ +.sidebar .item .row-actions { + display: none; + margin-left: auto; + gap: 2px; + flex-shrink: 0; +} +.sidebar .item:hover .row-actions { + display: inline-flex; +} +.sidebar .item:hover .meta { + display: none; +} +.sidebar .item .row-act { + border: none; + background: transparent; + cursor: pointer; + font-size: 12px; + line-height: 1; + padding: 2px 4px; + border-radius: 5px; + opacity: 0.7; + filter: grayscale(0.3); +} +.sidebar .item .row-act:hover { + background: var(--bg-3); + opacity: 1; + filter: none; +} +.sidebar .item .row-act.danger:hover { + background: rgba(255, 84, 112, 0.18); +} /* ──────────────────────────────────────────────────────────────────── */ /* Inspector rail (right column) */ diff --git a/apps/desktop/src/lib/mac-agent.ts b/apps/desktop/src/lib/mac-agent.ts index edc642a..e3ad426 100644 --- a/apps/desktop/src/lib/mac-agent.ts +++ b/apps/desktop/src/lib/mac-agent.ts @@ -15,7 +15,17 @@ import { runAgent } from '@deepcode/core/dist/agent.js'; import { DeepSeekProvider, EFFORT_PARAMS } from '@deepcode/core/dist/providers/deepseek.js'; import type { AgentEvent, Effort, Mode, ToolHandler } from '@deepcode/core/dist/types.js'; import { MAC_TOOLS } from './mac-tools.js'; -import { readCredentials, sessionAppend, sessionCreate } from './tauri-api.js'; +import { readCredentials, sessionAppend, sessionCreate, sessionSetTitle } from './tauri-api.js'; + +/** First non-empty line of the user message, trimmed to a sidebar-friendly length. */ +function sessionTitleFrom(userMessage: string): string { + const firstLine = + userMessage + .split('\n') + .map((l) => l.trim()) + .find((l) => l.length > 0) ?? userMessage.trim(); + return firstLine.slice(0, 60); +} // Local minimal ToolRegistry — same shape as @deepcode/core's, without // the BUILTIN_TOOLS top-level import that drags in fs. @@ -132,6 +142,7 @@ export async function startAgentTurn(args: StartTurnArgs): Promise await invoke('session_set_title', { id, title }); } +/** Permanently delete a session's JSONL file. */ +export async function sessionDelete(id: string): Promise { + await invoke('session_delete', { id }); +} + +/** Archive a session (moved to sessions/archived/, hidden from the list). */ +export async function sessionArchive(id: string): Promise { + await invoke('session_archive', { id }); +} + /** Append one JSON message line to a session's JSONL file. */ export async function sessionAppend(id: string, message: Record): Promise { await invoke('session_append', { id, message }); From d9a79259eea5fd75359da8010de457a26c6c03df Mon Sep 17 00:00:00 2001 From: t Date: Wed, 3 Jun 2026 16:19:40 +0800 Subject: [PATCH 4/9] fix(desktop): regenerate stale icon.icns from icon.svg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dock icon (.icns) was stale — generated from an older render — so it showed washed-out while the PNGs were already the crisp white-cat-on-blue mark. Regenerated icon.icns from icon.svg via `tauri icon` so the dock / Finder icon matches the intended design. (Non-macOS assets that tauri icon emits — Windows/Android/iOS — were discarded; this is a macOS-only app.) Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src-tauri/icons/icon.icns | Bin 78412 -> 78412 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns index fb56d3b4619cdf0ddaad51a63d36b08a2f08e59a..d7c0ea134aa6f445616daeec7b120392dc795b44 100644 GIT binary patch delta 67 zcmV-J0KEUq)fmfVag9v;EUS7O@K&1G59k Z`U Date: Wed, 3 Jun 2026 16:39:02 +0800 Subject: [PATCH 5/9] style(desktop): Claude-Code-style chat messages + density (pass 1/2) Reshape the message stream toward Claude Code: a centered ~760px readable column instead of full-bleed, drop the heavy YO/DC avatar chips, give user turns a soft bubble while assistant/system are plain prose, and tighten the inter-message spacing. Sidebar search + top-bar cleanup land in pass 2. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src/index.css | 71 +++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index d8672ef..a41f2f6 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -718,10 +718,12 @@ select { .chat-stream { flex: 1; overflow-y: auto; - padding: 22px; + padding: 20px 0; display: flex; flex-direction: column; - gap: 16px; + gap: 2px; + /* Claude-Code-style centered, readable column rather than full-bleed. */ + --stream-max: 760px; } .chat-stream::-webkit-scrollbar { width: 8px; @@ -731,54 +733,53 @@ select { border-radius: 4px; } -/* Message rows */ +/* Message rows — Claude-Code style: a centered readable column, no heavy + avatar chips. The role reads from a subtle label + (for the user) a soft + bubble; the assistant is plain prose. */ .msg { - display: flex; - gap: 12px; + display: block; + width: 100%; + max-width: var(--stream-max); + margin: 0 auto; + padding: 10px 22px; } .msg .avatar { - width: 28px; - height: 28px; - border-radius: 8px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - font-weight: 700; -} -.msg.user .avatar { - background: var(--bg-3); - color: var(--text-1); -} -.msg.assistant .avatar { - background: linear-gradient(135deg, var(--brand) 0%, #6b86ff 100%); - color: #fff; -} -.msg.system .avatar { - background: var(--bg-2); - color: var(--text-2); - font-size: 11px; + display: none; } .msg .body { - flex: 1; min-width: 0; } .msg .author { - font-size: 10.5px; + font-size: 11px; color: var(--text-3); - margin-bottom: 4px; - letter-spacing: 0.5px; - text-transform: uppercase; + margin-bottom: 5px; + letter-spacing: 0.3px; font-weight: 600; } .msg .content { - color: var(--text-0); - font-size: 13.5px; - line-height: 1.65; + color: var(--text-1); + font-size: 14px; + line-height: 1.7; white-space: pre-wrap; word-break: break-word; } +/* User turns get a soft bubble; assistant + system are plain prose. */ +.msg.user .body { + background: var(--bg-2); + border: 1px solid var(--line-soft); + border-radius: 12px; + padding: 10px 14px; +} +.msg.user .author { + display: none; +} +.msg.assistant .content { + color: var(--text-0); +} +.msg.system { + padding-top: 4px; + padding-bottom: 4px; +} .msg .content code { background: var(--bg-3); color: #b4c2ff; From e94cd1ea696e41279b2447ee2d2d4f21391a6416 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 3 Jun 2026 17:13:24 +0800 Subject: [PATCH 6/9] fix(desktop): resume + title CLI/headless sessions (no type:"message") MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desktop sessions tag each message line type:"message", but CLI/headless sessions persist bare {role,content} lines with no type field. session_read and derive_session_title both filtered on type=="message", so CLI sessions loaded as an empty chat and showed their id instead of a title. Accept a line as a message when type=="message" OR (type absent AND role is user/assistant). Verified: a CLI session went 0→1 messages with a derived title. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src-tauri/src/commands.rs | 54 +++++++++++++++----------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs index b21e1dd..a839406 100644 --- a/apps/desktop/src-tauri/src/commands.rs +++ b/apps/desktop/src-tauri/src/commands.rs @@ -198,7 +198,14 @@ pub fn session_read(id: String) -> Result, String> { let Ok(v) = serde_json::from_str::(line) else { continue; // tolerate a partial trailing line }; - if v.get("type").and_then(|t| t.as_str()) == Some("message") { + // Desktop sessions tag messages with type:"message"; CLI/headless sessions + // write bare {role, content} lines with no type. Accept both, skip meta. + let t = v.get("type").and_then(|t| t.as_str()); + let is_role_msg = matches!( + v.get("role").and_then(|r| r.as_str()), + Some("user") | Some("assistant") + ); + if t == Some("message") || (t.is_none() && is_role_msg) { out.push(v); } } @@ -250,34 +257,37 @@ fn derive_session_title(path: &std::path::Path) -> Option { let Ok(v) = serde_json::from_str::(line) else { continue; }; - match v.get("type").and_then(|t| t.as_str()) { - // Manual title wins — return immediately. - Some("session_meta") => { - if let Some(t) = v.get("title").and_then(|t| t.as_str()) { - let t = t.trim(); - if !t.is_empty() { - return Some(clean_title(t)); - } + let line_type = v.get("type").and_then(|t| t.as_str()); + // Manual title on the session_meta header wins — return immediately. + if line_type == Some("session_meta") { + if let Some(t) = v.get("title").and_then(|t| t.as_str()) { + let t = t.trim(); + if !t.is_empty() { + return Some(clean_title(t)); } } - Some("message") if v.get("role").and_then(|r| r.as_str()) == Some("user") => { - if from_user.is_none() { - if let Some(content) = v.get("content").and_then(|c| c.as_array()) { - for block in content { - if block.get("type").and_then(|t| t.as_str()) == Some("text") { - if let Some(txt) = block.get("text").and_then(|t| t.as_str()) { - let title = clean_title(txt); - if !title.is_empty() { - from_user = Some(title); - break; - } - } + continue; + } + // First user message → title. Desktop tags type:"message"; CLI/headless + // sessions write bare {role,content} with no type — accept both. + let is_msg = line_type == Some("message") || line_type.is_none(); + if is_msg + && v.get("role").and_then(|r| r.as_str()) == Some("user") + && from_user.is_none() + { + if let Some(content) = v.get("content").and_then(|c| c.as_array()) { + for block in content { + if block.get("type").and_then(|t| t.as_str()) == Some("text") { + if let Some(txt) = block.get("text").and_then(|t| t.as_str()) { + let title = clean_title(txt); + if !title.is_empty() { + from_user = Some(title); + break; } } } } } - _ => {} } } from_user From 10fd5c0c87798167e1b40b12cd2684df3cefe0b2 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 3 Jun 2026 17:28:44 +0800 Subject: [PATCH 7/9] =?UTF-8?q?style(desktop):=20Claude-Code=20layout=20pa?= =?UTF-8?q?ss=202=20=E2=80=94=20composer=20align,=20sidebar=20search,=20co?= =?UTF-8?q?mpact=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Composer now aligns to the centered message column (760px) instead of spanning full width — cohesive chat area like Claude Code. - Sidebar: add a session search/filter box; replace the heavy boxed PROJECT chip with a compact one-line project row (the header breadcrumb carries the path already). - Add a dev-only full-app preview harness (preview-app.html/.tsx) that renders with a mocked Tauri invoke, so layout can be screenshotted + iterated without a rebuild. Not in the prod bundle. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src/components/Sidebar.tsx | 108 ++++++++++-------------- apps/desktop/src/index.css | 97 +++++++++++++++++++++ apps/desktop/src/preview-app.html | 12 +++ apps/desktop/src/preview-app.tsx | 94 +++++++++++++++++++++ 4 files changed, 249 insertions(+), 62 deletions(-) create mode 100644 apps/desktop/src/preview-app.html create mode 100644 apps/desktop/src/preview-app.tsx diff --git a/apps/desktop/src/components/Sidebar.tsx b/apps/desktop/src/components/Sidebar.tsx index 4aa69d6..6377842 100644 --- a/apps/desktop/src/components/Sidebar.tsx +++ b/apps/desktop/src/components/Sidebar.tsx @@ -58,6 +58,7 @@ export function Sidebar({ // Inline rename: which session is being edited + its draft title. const [editingId, setEditingId] = useState(null); const [editValue, setEditValue] = useState(''); + const [query, setQuery] = useState(''); const reload = useCallback(() => { void listSessions() @@ -112,12 +113,18 @@ export function Sidebar({ } } + const q = query.trim().toLowerCase(); + const visible = q + ? sessions.filter( + (s) => (s.title || '').toLowerCase().includes(q) || s.id.toLowerCase().includes(q), + ) + : sessions; const grouped: Record = { Today: [], Yesterday: [], Earlier: [], }; - for (const s of sessions) { + for (const s of visible) { grouped[bucketFor(s.updated_at_secs, now)].push(s); } @@ -128,68 +135,18 @@ export function Sidebar({ DeepCode
- {/* Active project chip */} -
-
+ 📁 + {projectName(projectPath)} +
-
- 📁 - - {projectName(projectPath)} - - -
+ ⇄ +
+ {sessions.length > 0 && ( +
+ + setQuery(e.target.value)} + spellCheck={false} + /> + {query && ( + + )} +
+ )} + + {q && visible.length === 0 && ( +
No sessions match “{query}”.
+ )} + {(['Today', 'Yesterday', 'Earlier'] as const).map((bucket) => { const items = grouped[bucket]; if (items.length === 0) return null; diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index a41f2f6..64f48fa 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -309,6 +309,95 @@ select { .sidebar .new-btn:hover { background: rgba(77, 107, 254, 0.18); } +/* Session search (Claude-Code-style filter) */ +.sidebar .sb-search { + display: flex; + align-items: center; + gap: 6px; + margin: 2px 4px 6px; + padding: 6px 9px; + background: var(--bg-0); + border: 1px solid var(--line-soft); + border-radius: var(--radius-sm); +} +.sidebar .sb-search:focus-within { + border-color: var(--line); +} +.sidebar .sb-search-icon { + color: var(--text-3); + font-size: 13px; + flex-shrink: 0; +} +.sidebar .sb-search input { + flex: 1; + min-width: 0; + background: transparent; + border: 0; + outline: none; + color: var(--text-0); + font: inherit; + font-size: 12px; +} +.sidebar .sb-search input::placeholder { + color: var(--text-3); +} +.sidebar .sb-search-clear { + border: 0; + background: transparent; + color: var(--text-3); + cursor: pointer; + font-size: 14px; + line-height: 1; + padding: 0 2px; +} +.sidebar .sb-search-clear:hover { + color: var(--text-0); +} +.sidebar .sb-search-empty { + color: var(--text-3); + font-size: 11.5px; + padding: 8px; + text-align: center; +} +/* Compact active-project row (replaces the big PROJECT box). */ +.sidebar .sb-project { + display: flex; + align-items: center; + gap: 7px; + margin: 0 4px 10px; + padding: 6px 8px; + border-radius: var(--radius-sm); + color: var(--text-1); + font-size: 12.5px; +} +.sidebar .sb-project:hover { + background: var(--bg-1); +} +.sidebar .sb-project-icon { + font-size: 13px; + flex-shrink: 0; +} +.sidebar .sb-project-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-0); + font-weight: 500; +} +.sidebar .sb-project-switch { + border: 0; + background: transparent; + color: var(--text-3); + cursor: pointer; + font-size: 12px; + padding: 2px; + flex-shrink: 0; +} +.sidebar .sb-project-switch:hover { + color: var(--text-0); +} .sidebar .new-btn kbd { color: var(--text-3); font-family: inherit; @@ -884,6 +973,14 @@ select { padding: 14px 18px 16px; background: var(--bg-1); } +/* Keep the composer aligned with the centered message column (Claude-Code feel) + rather than spanning full width. 760px matches .msg's --stream-max. */ +.composer .box, +.composer .ctx-bar { + max-width: 760px; + width: 100%; + margin-inline: auto; +} .composer .box { border: 1px solid var(--line); border-radius: 12px; diff --git a/apps/desktop/src/preview-app.html b/apps/desktop/src/preview-app.html new file mode 100644 index 0000000..5f727d7 --- /dev/null +++ b/apps/desktop/src/preview-app.html @@ -0,0 +1,12 @@ + + + + + + DeepCode — full-app layout preview (dev only) + + +
+ + + diff --git a/apps/desktop/src/preview-app.tsx b/apps/desktop/src/preview-app.tsx new file mode 100644 index 0000000..52e1355 --- /dev/null +++ b/apps/desktop/src/preview-app.tsx @@ -0,0 +1,94 @@ +// DEV-ONLY full-app layout preview. Renders with a mocked Tauri +// `invoke` so the whole shell (sidebar + chat + composer + inspector) shows in +// a plain browser — lets us screenshot + iterate on the layout without the +// Tauri backend or a rebuild. Not in the prod bundle (build input = index.html). + +import { createRoot } from 'react-dom/client'; +import { App } from './App.js'; +import { installTauriShim } from './lib/window-shim.js'; +import './index.css'; + +const now = Math.floor(Date.now() / 1000); +const MOCK_SESSIONS = [ + { + id: '2026-06-02-aaa111', + path: '', + size_bytes: 900, + updated_at_secs: now - 3600, + title: '制作一个打飞机的小游戏', + }, + { + id: '2026-06-02-bbb222', + path: '', + size_bytes: 700, + updated_at_secs: now - 7200, + title: '写一个超级马里奥的小游戏', + }, + { + id: '2026-06-01-ccc333', + path: '', + size_bytes: 500, + updated_at_secs: now - 90_000, + title: '重构 auth 模块并加单测', + }, + { + id: '2026-05-31-ddd444', + path: '', + size_bytes: 300, + updated_at_secs: now - 180_000, + title: 'hi', + }, +]; +const MOCK_MESSAGES = [ + { type: 'message', role: 'user', content: [{ type: 'text', text: '制作一个打飞机的小游戏' }] }, + { + type: 'message', + role: 'assistant', + content: [ + { + type: 'text', + text: '好的,我来创建一个 HTML5 打飞机射击游戏,包含玩家飞机、敌机、子弹和计分。', + }, + { + type: 'tool_use', + id: 't1', + name: 'Write', + input: { file_path: '/Users/oratis/Projects/DeepCode/test/打飞机.html' }, + }, + ], + }, + { type: 'message', role: 'user', content: [{ type: 'text', text: '加一个 boss 关卡' }] }, +]; + +// Mock the Tauri invoke bridge before the app calls it (no invoke runs at import). +(window as unknown as { __TAURI_INTERNALS__: unknown }).__TAURI_INTERNALS__ = { + invoke: async (cmd: string) => { + switch (cmd) { + case 'load_settings_file': + return { projectPath: '/Users/oratis/Projects/DeepCode/test' }; + case 'read_credentials': + return { api_key: 'sk-mock', base_url: 'https://api.deepseek.com/v1' }; + case 'get_app_info': + return { version: '0.1.6', platform: 'macos', home_dir: '/Users/oratis' }; + case 'get_settings_path': + return '/Users/oratis/.deepcode/settings.json'; + case 'list_sessions': + return MOCK_SESSIONS; + case 'session_read': + return MOCK_MESSAGES; + case 'load_keybindings': + return {}; + case 'list_plugins': + case 'list_skills': + return []; + default: + console.warn('[preview] unmocked invoke:', cmd); + return null; + } + }, + transformCallback: (cb: unknown) => cb, +}; + +installTauriShim(); +const rootEl = document.getElementById('root'); +if (rootEl) createRoot(rootEl).render(); From 84d47f1d5a0d17903fd2d4a832ec093cf51d10d5 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 3 Jun 2026 18:08:53 +0800 Subject: [PATCH 8/9] =?UTF-8?q?feat(desktop):=20right=20rail=20becomes=20a?= =?UTF-8?q?n=20activity=20bar=20=E2=80=94=20distinct=20panels=20per=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inspector rail's four hint icons (▤◐📁ⓘ) all expanded the same Inspector panel, so "clicking anything on the right is just the inspector". Rework it into a VS Code-style activity bar: the 48px rail is always present on the far right and each icon opens its OWN panel to its left, exactly one at a time. - ⓘ Inspector — plan / context / recent files / session (plan badge + context tint) - 📄 Files — the file preview panel (Source / Diff / History) - ⚙ Settings — the Settings shell (main-area screen) App: replace inspectorExpanded/inspectorFocus/expandInspector with inspectorOpen + a filesCollapsed flag; toggleInspector/toggleFiles/openFile enforce single-panel mutual exclusion (opening one closes the other; an active icon closes its panel; Files with no tabs invokes the picker). The rail is now rendered unconditionally as the last grid child; .inspector-open gains the 48px rail track so the inspector squeezes chat beside the rail instead of replacing it. Monochrome SVG icons replace the unicode glyphs. preview-app: mock plugin:dialog|open + correct tool_read ({content}) so the file panel is exercisable in the browser preview. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src/App.tsx | 94 ++++++---- apps/desktop/src/components/InspectorRail.tsx | 163 +++++++++++------- apps/desktop/src/index.css | 2 +- apps/desktop/src/preview-app.tsx | 25 +++ 4 files changed, 180 insertions(+), 104 deletions(-) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 960f6d0..8306447 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -28,11 +28,7 @@ import { SettingsScreen } from './screens/Settings.js'; import { SkillsScreen } from './screens/Skills.js'; import type { ScreenName } from './types/screens.js'; import type { UpdateInfo } from './types/global.js'; -import { - emptyInspectorData, - type InspectorData, - type InspectorSection, -} from './types/inspector.js'; +import { emptyInspectorData, type InspectorData } from './types/inspector.js'; export function App(): JSX.Element { const [hasKey, setHasKey] = useState(null); @@ -44,13 +40,15 @@ export function App(): JSX.Element { // Reconstructed messages for a resumed session; seeded into ReplScreen on its // next remount. Cleared when starting a fresh session. const [resumedMessages, setResumedMessages] = useState(undefined); - // Right inspector: 48 px rail by default, 320 px panel when expanded. - const [inspectorExpanded, setInspectorExpanded] = useState(false); - // Which section to scroll to when expanding via a rail hint icon (null = top). - const [inspectorFocus, setInspectorFocus] = useState(null); + // Right side is an activity bar (48 px rail) that's always present; exactly + // one panel opens to its left at a time (VS Code model). `inspectorOpen` + // tracks the Inspector panel; the file panel tracks its own tabs + a collapse + // flag so it can be hidden without discarding open files. + const [inspectorOpen, setInspectorOpen] = useState(false); const [inspector, setInspector] = useState(() => emptyInspectorData()); - // Right-side file panel (§3.11): opens between chat and the inspector rail. + // Right-side file panel (§3.11): opens to the left of the rail. const fp = useFilePanel(); + const [filesCollapsed, setFilesCollapsed] = useState(false); // Drag the panel's left edge to resize (320–800px, persisted by the hook). const onFilePanelResizeStart = useCallback( @@ -69,13 +67,32 @@ export function App(): JSX.Element { [fp.state.width, fp.setWidth], ); - // Expand the inspector, optionally scrolling to a section. Bumps focus to a - // fresh value each time so re-clicking the same icon re-scrolls. - const expandInspector = useCallback((section?: InspectorSection) => { - setInspectorExpanded(true); - setInspectorFocus(section ?? null); + // Exactly one right panel at a time. Opening one closes the other; clicking + // an active icon closes its panel. + const filesVisible = fp.isOpen && !filesCollapsed; + + const toggleInspector = useCallback(() => { + setFilesCollapsed(true); // a visible inspector hides the file panel + setInspectorOpen((v) => !v); }, []); + const toggleFiles = useCallback(() => { + setInspectorOpen(false); + if (fp.isOpen) setFilesCollapsed((c) => !c); + else void fp.openViaPicker(); // no tabs yet — let the user pick a file + }, [fp.isOpen, fp.openViaPicker]); + + // Open a specific file (chat tool card / inspector recent files): surface the + // file panel and step the inspector aside for it. + const openFile = useCallback( + (path: string) => { + setInspectorOpen(false); + setFilesCollapsed(false); + void fp.open(path); + }, + [fp.open], + ); + // Merge the slice ReplScreen lifts up (usage / model / mode / files / todos). // Stable identity so ReplScreen's sync effect doesn't refire every render. const handleInspector = useCallback((patch: Partial) => { @@ -114,10 +131,10 @@ export function App(): JSX.Element { // Re-registered when that context changes so it never reads stale state. useEffect(() => { return registerShortcut('meta+\\', () => { - if (fp.isOpen && fp.state.view === 'diff') fp.toggleDiffMode(); - else setInspectorExpanded((v) => !v); + if (filesVisible && fp.state.view === 'diff') fp.toggleDiffMode(); + else toggleInspector(); }); - }, [fp.isOpen, fp.state.view, fp.toggleDiffMode]); + }, [filesVisible, fp.state.view, fp.toggleDiffMode, toggleInspector]); async function handlePickProject(path: string): Promise { await saveProjectPath(path); @@ -159,11 +176,11 @@ export function App(): JSX.Element { const usedTokens = inspector.usage.inputTokens + inspector.usage.outputTokens; const contextFill = usedTokens > 0 ? usedTokens / contextWindowFor(inspector.model) : undefined; - // When the file panel is open it inserts a 4th column and the inspector stays - // a 48px rail (§3.11); otherwise the 3-column shell with an optional 320px - // inspector panel. + // The rail is always the last 48px column. A panel (file OR inspector) opens + // to its left, widening the grid so it squeezes chat rather than overlaying. + const inspectorShowing = inspectorOpen && !filesVisible; const shellClass = - 'app-shell' + (fp.isOpen ? ' file-open' : inspectorExpanded ? ' inspector-open' : ''); + 'app-shell' + (filesVisible ? ' file-open' : inspectorShowing ? ' inspector-open' : ''); return (
@@ -220,10 +237,10 @@ export function App(): JSX.Element { () => setSessionEpoch((k) => k + 1), handleInspector, resumedMessages, - (path) => void fp.open(path), + openFile, )} - {fp.isOpen && ( + {filesVisible ? ( {}} onResizeStart={onFilePanelResizeStart} /> - )} - {inspectorExpanded && !fp.isOpen ? ( + ) : inspectorShowing ? ( setInspectorExpanded(false)} - onOpenFile={(path) => void fp.open(path)} - /> - ) : ( - setScreen('settings')} - settingsActive={SETTINGS_FAMILY.includes(screen)} - planCount={planCount} - contextFill={contextFill} + focusSection={null} + onCollapse={() => setInspectorOpen(false)} + onOpenFile={openFile} /> - )} + ) : null} + setScreen('settings')} + />
); } diff --git a/apps/desktop/src/components/InspectorRail.tsx b/apps/desktop/src/components/InspectorRail.tsx index 7399505..c70a93b 100644 --- a/apps/desktop/src/components/InspectorRail.tsx +++ b/apps/desktop/src/components/InspectorRail.tsx @@ -1,97 +1,69 @@ -// Right-column collapsed inspector rail (48 px). -// Design spec screen #3 (line ~1220). +// Right-column activity bar (48 px) — always present on the far right. // -// Per the spec the rail is intentionally minimal: it hints at the inspector's -// contents with four small icons (▤ Plan · ◐ Context · 📁 Recent files · -// ⓘ Session info) and nothing else but the ‹ expand chevron and a ⚙ Settings -// shortcut. Clicking ‹ — or any of the four hint icons — expands the 320 px -// panel (the icon picks which section to scroll to). The settings cog is the -// rail's one piece of navigation; everything else (Permissions / MCP / Plugins -// / Skills / About) lives inside the Settings shell's left nav. - -import type { InspectorSection } from '../types/inspector.js'; +// Each icon opens its OWN distinct right-side panel (VS Code activity-bar +// model), so it's no longer "everything opens the inspector": +// • ⓘ Inspector — plan / context / recent files / session (toggles the panel) +// • ▤ Files — the file preview panel (Source / Diff / History) +// • ⚙ Settings — the Settings shell (a main-area screen, not a right panel) +// The active panel's icon is highlighted; clicking it again closes the panel. interface InspectorRailProps { - /** Plan items pending — shown as a badge on ▤. */ + /** Inspector panel is the visible right panel. */ + inspectorActive: boolean; + /** File preview panel is the visible right panel. */ + filesActive: boolean; + /** On any settings-family screen (highlights the cog). */ + settingsActive: boolean; + /** Plan items pending — badge on the Inspector icon. */ planCount?: number; - /** Context fill 0..1 — drives the ◐ color (mint if < 0.6, warn ≥ 0.8). */ + /** Context fill 0..1 — tints the Inspector icon (warn ≥ 0.8). */ contextFill?: number; - /** Expand the rail into the 320 px panel, optionally focusing a section. */ - onExpand: (section?: InspectorSection) => void; - /** Open the Settings shell. */ + onToggleInspector: () => void; + onToggleFiles: () => void; onSettings: () => void; - /** Highlight the cog when the user is on any settings-family screen. */ - settingsActive: boolean; } export function InspectorRail({ + inspectorActive, + filesActive, + settingsActive, planCount, contextFill, - onExpand, + onToggleInspector, + onToggleFiles, onSettings, - settingsActive, }: InspectorRailProps): JSX.Element { const ctxColor = contextFill === undefined - ? 'var(--text-2)' + ? undefined : contextFill > 0.8 ? 'var(--warn)' : contextFill > 0.6 - ? 'var(--text-1)' - : 'var(--accent)'; + ? 'var(--text-0)' + : undefined; + + const ctxTitle = contextFill === undefined ? '' : ` · context ${Math.round(contextFill * 100)}%`; return ( ); } + +/** Inspector — info/details glyph. */ +function IconInspector(): JSX.Element { + return ( + + ); +} + +/** Files — document with a folded corner + text lines. */ +function IconFiles(): JSX.Element { + return ( + + ); +} + +/** Settings — gear. */ +function IconSettings(): JSX.Element { + return ( + + ); +} diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index 64f48fa..6f4dada 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -555,7 +555,7 @@ select { /* ──────────────────────────────────────────────────────────────────── */ .app-shell.inspector-open { - grid-template-columns: 240px 1fr 320px; + grid-template-columns: 240px 1fr 320px 48px; } .inspector { diff --git a/apps/desktop/src/preview-app.tsx b/apps/desktop/src/preview-app.tsx index 52e1355..e34886e 100644 --- a/apps/desktop/src/preview-app.tsx +++ b/apps/desktop/src/preview-app.tsx @@ -81,6 +81,31 @@ const MOCK_MESSAGES = [ case 'list_plugins': case 'list_skills': return []; + // The file picker (⌘O / Files-with-no-tabs) goes through the dialog plugin. + case 'plugin:dialog|open': + return '/Users/oratis/Projects/DeepCode/test/打飞机.html'; + // toolRead unwraps `.content` (see lib/tauri-api.ts). + case 'tool_read': + return { + content: [ + '', + '', + ' ', + ' ', + ' 打飞机', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'), + }; default: console.warn('[preview] unmocked invoke:', cmd); return null; From a283e879c7f1d1efd3e691f05ae17dd3615b6b3b Mon Sep 17 00:00:00 2001 From: t Date: Wed, 3 Jun 2026 18:17:15 +0800 Subject: [PATCH 9/9] =?UTF-8?q?style(desktop):=20tighten=20the=20top=20?= =?UTF-8?q?=E2=80=94=20content=20sits=20just=20under=20the=20titlebar=20in?= =?UTF-8?q?set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 30px shell padding already clears the macOS traffic lights, so each column's extra top padding (sidebar 18 / chat-header 12 / rail 14 / inspector-head 18) was pure dead space — content sat ~46px down with an empty strip above it. Trim those so the brand / breadcrumb / pills / inspector title align ~34px from the window top, just below the titlebar (Claude-Code-style compact top). The inspector loses its panel top padding and the sticky head's −18px margin trick (top:0 now), keeping its title aligned with the rest. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src/index.css | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index 6f4dada..420a762 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -272,7 +272,9 @@ select { .sidebar { background: var(--bg-0); border-right: 1px solid var(--line); - padding: 18px 14px; + /* Top padding stays small — the shell's 30px titlebar inset already clears + the traffic lights, so extra top padding here is pure dead space. */ + padding: 4px 14px 14px; overflow-y: auto; display: flex; flex-direction: column; @@ -281,7 +283,7 @@ select { display: flex; align-items: center; gap: 10px; - padding: 4px 6px 18px; + padding: 2px 6px 14px; } .sidebar .brand-row .name { font-weight: 700; @@ -494,7 +496,7 @@ select { .inspector-rail { background: var(--bg-1); border-left: 1px solid var(--line); - padding: 14px 0; + padding: 6px 0; gap: 10px; display: flex; flex-direction: column; @@ -561,19 +563,20 @@ select { .inspector { background: var(--bg-0); border-left: 1px solid var(--line); - padding: 18px; + /* No top padding — the sticky head supplies its own; keeps the title aligned + with the brand / chat-header just under the titlebar inset. */ + padding: 0 18px 18px; overflow-y: auto; } .inspector .inspector-head { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 4px; /* Sticky so a section scrolled into view via a rail icon lands just below. */ position: sticky; - top: -18px; - margin: -18px -18px 4px; - padding: 18px 18px 8px; + top: 0; + margin: 0 -18px 4px; + padding: 8px 18px 8px; background: var(--bg-0); z-index: 1; } @@ -782,7 +785,7 @@ select { background: var(--bg-1); } .chat-header { - padding: 12px 22px; + padding: 6px 22px; border-bottom: 1px solid var(--line); display: flex; align-items: center;