diff --git a/Agent.md b/Agent.md index 53ceeb02..93e83ccc 100644 --- a/Agent.md +++ b/Agent.md @@ -86,7 +86,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request - Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端" - Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout) - Unit tests `npm test` (89: 45 daemon_client + 20 conn-manager + 8 integration + 6 build-config + 7 gui-state + 3 preload-api); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` - - React migration Batch 0 + Batch 1 (design `~/.emrg/designs/gui-react-migration-design.md`): `emrg/gui/renderer/` scaffold — Vite 8 + React 19 + TypeScript strict + Vitest + self-developed SnapshotStore (useSyncExternalStore); standalone build → `renderer/dist/index.html` + `assets/*` (CSP `script-src 'self'`-safe, no inline); `window.emrg` bridge (52 invoke + onEvent, API-surface guard test) + main.js + daemon protocol untouched; vanilla renderer stays live until Batch 5 switch (D3); Batch 1 ported commands.js → `lib/commands.ts` + copywriting.js → `lib/copywriting.ts` (pure logic, injectable t); Batch 1 remainder: i18n.js full dictionary (373 keys zh/en parity, auto-generated `lib/i18n-dicts.ts` + detectLocale/getLocale/setLocale/t) + markdown.js → `lib/markdown.ts` (marked/DOMPurify/hljs injectable, fence-closed heuristic + block-projection streaming); Batch 2: chat.js → `lib/transcript.ts` (pure per-session state machine — delta/done/toolStart/toolEnd/merge-group/clearTyping, sid buckets P3, 15 tests) + `components/TranscriptView.tsx` (React chat area: streaming plain-text → markdown-on-done via injectable renderer, tool rows running→done/failed with elapsed + expandable output, merge-group bar summary + collapse/user-expanded, remote label, 7 tests); Batch 3: Sidebar/ResultPanel/WorkspaceView/FileTree components; Batch 4: dialog components (help/memory/skills/rename/confirm/rewind/open-session/new-session/welcome/github-device) + `lib/dialog` reducer + dialogLists/rewind/openSession pure logic; Batch 5: `lib/daemonBridge.ts` event bridge (multi-subscriber onEvent → transcript/app stores, sid-routed #977, queue-injection #655) + `DaemonBridgeProvider` (mount-once/dispose, tRef) + Shell chat-loop wiring (live daemon data: transcript/composer/sidebar/result-panel, #1016-#1018) + `DialogHost` (command routing → dialogs: /help /memory /skills /rewind /rename /delete /sessions /open /resume + direct /clear /compact /version /image; sidebar new-chat/open-chat buttons, #1019) + workspace view switching (Sidebar `#side-nav` rail: sessions/projects/tasks/rants/settings with active highlight; Shell `activeView` state per vanilla `switchView` toggle semantics — panel hides chat chrome, sessions restores it; #1020) + SettingsDialog wiring (`components/SettingsPanel.tsx`: five tabs model/github/appearance/language/about — model CRUD + set-default via getSettings/saveSettings, github connect/disconnect status, theme data-theme apply, i18n setLocale, about version/evolution-count from Shell appState; #1021) + final switch part 1 (#1023): main.js `loadFile` → `renderer/dist/index.html` (both initial + render-process-gone reload) — production GUI now runs the React app; vanilla renderer files (renderer/js, css, index.html) + vm.runInContext smoke tests retained for revert, removed in follow-up cleanup (vanilla renderer/js + css + index.html and the vm.runInContext legacy tests deleted — React vitest 411 + GUI 89 cover the migrated logic); gui `npm start` pre-builds dist via `prestart` (fresh-clone dev flow) + - React migration Batch 0 + Batch 1 (design `~/.emrg/designs/gui-react-migration-design.md`): `emrg/gui/renderer/` scaffold — Vite 8 + React 19 + TypeScript strict + Vitest + self-developed SnapshotStore (useSyncExternalStore); standalone build → `renderer/dist/index.html` + `assets/*` (CSP `script-src 'self'`-safe, no inline); `window.emrg` bridge (52 invoke + onEvent, API-surface guard test) + main.js + daemon protocol untouched; vanilla renderer stays live until Batch 5 switch (D3); Batch 1 ported commands.js → `lib/commands.ts` + copywriting.js → `lib/copywriting.ts` (pure logic, injectable t); Batch 1 remainder: i18n.js full dictionary (373 keys zh/en parity, auto-generated `lib/i18n-dicts.ts` + detectLocale/getLocale/setLocale/t) + markdown.js → `lib/markdown.ts` (marked/DOMPurify/hljs injectable, fence-closed heuristic + block-projection streaming); Batch 2: chat.js → `lib/transcript.ts` (pure per-session state machine — delta/done/toolStart/toolEnd/merge-group/clearTyping, sid buckets P3, 15 tests) + `components/TranscriptView.tsx` (React chat area: streaming plain-text → markdown-on-done via injectable renderer, tool rows running→done/failed with elapsed + expandable output, merge-group bar summary + collapse/user-expanded, remote label, 7 tests); Batch 3: Sidebar/ResultPanel/WorkspaceView/FileTree components; Batch 4: dialog components (help/memory/skills/rename/confirm/rewind/open-session/new-session/welcome/github-device) + `lib/dialog` reducer + dialogLists/rewind/openSession pure logic; Batch 5: `lib/daemonBridge.ts` event bridge (multi-subscriber onEvent → transcript/app stores, sid-routed #977, queue-injection #655) + `DaemonBridgeProvider` (mount-once/dispose, tRef) + Shell chat-loop wiring (live daemon data: transcript/composer/sidebar/result-panel, #1016-#1018) + `DialogHost` (command routing → dialogs: /help /memory /skills /rewind /rename /delete /sessions /open /resume + direct /clear /compact /version /image; sidebar new-chat/open-chat buttons, #1019) + workspace view switching (Sidebar `#side-nav` rail: sessions/projects/tasks/rants/settings with active highlight; Shell `activeView` state per vanilla `switchView` toggle semantics — panel hides chat chrome, sessions restores it; #1020) + SettingsDialog wiring (`components/SettingsPanel.tsx`: five tabs model/github/appearance/language/about — model CRUD + set-default via getSettings/saveSettings, github connect/disconnect status, theme data-theme apply, i18n setLocale, about version/evolution-count from Shell appState; #1021) + final switch part 1 (#1023): main.js `loadFile` → `renderer/dist/index.html` (both initial + render-process-gone reload) — production GUI now runs the React app; vanilla renderer files (renderer/js, css, index.html) + vm.runInContext smoke tests retained for revert, removed in follow-up cleanup (vanilla renderer/js + css + index.html and the vm.runInContext legacy tests deleted — React vitest 411 + GUI 89 cover the migrated logic); gui `npm start` pre-builds dist via `prestart` (fresh-clone dev flow) + post-switch production wiring: `lib/vendorMarkdown.ts` restores real vendor marked/DOMPurify/hljs into the bundle (chat markdown rendering + ResultPanel `window.hljs` code highlight — previously degraded to escaped plain text) and re-imports the vanilla `css/` suite (tokens/base/components/layout/animations, deleted with the vanilla renderer — React components reference `var(--token)` vars and vanilla class names; one pre-existing stray brace in components.css fixed for lightningcss) - **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711) - Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/.md` → fallback - Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`) @@ -121,7 +121,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg Python: `uv run pytest tests/ -v` (1107) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (89: 45 daemon_client + 20 conn-manager + 8 integration + 6 build-config + 7 gui-state + 3 preload-api) — syntax: `node --check main.js preload.js daemon_client.js` -Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (411: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 15 transcript + 7 TranscriptView + 15 history + 22 composer + 14 Composer + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 8 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 12 daemonBridge + 6 DaemonBridgeProvider + 16 Shell + 15 DialogHost + 13 SettingsPanel) + `npm run build` → `renderer/dist/` +Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (415: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 15 transcript + 7 TranscriptView + 15 history + 22 composer + 14 Composer + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 8 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 12 daemonBridge + 6 DaemonBridgeProvider + 16 Shell + 15 DialogHost + 13 SettingsPanel + 4 vendorMarkdown) + `npm run build` → `renderer/dist/` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) Git-over-https 兜底: `python scripts/sync-master-from-api.py [--repo owner/name] [--ref master]` — 受限网络下 github.com:443 不可达而 api.github.com 可达时,用 Git Data API 的 verification payload + signature 字节级重建上游 commit(含 web-flow GPG 签名 squash merge,reconstruct_commit 经 hermetic 测试验证 sha 一致)并推进本地 refs;内容对象缺失时 fail-loud 提示改用 git fetch(10+ 周期实证的恢复路径) diff --git a/emrg/gui/renderer/css/animations.css b/emrg/gui/renderer/css/animations.css new file mode 100644 index 00000000..07e9fd81 --- /dev/null +++ b/emrg/gui/renderer/css/animations.css @@ -0,0 +1,91 @@ +/* animations.css — 动效:快速 150ms / 中 250ms / 慢 350ms,克制、无弹跳无循环 + * 原则:只动 transform/opacity(性能友好);消息上浮+淡入;光标脉动;对话框淡入缩放。 + */ + +/* 消息出现:轻微上浮 + 淡入 */ +@keyframes msg-in { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 流式末尾柔和光标脉动 */ +@keyframes cursor-pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.25; + } +} + +/* 工具行旋转指示 */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* 对话框进入 */ +@keyframes dialog-in { + from { + opacity: 0; + transform: scale(0.96) translateY(8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* 遮罩淡入 */ +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* 横幅滑入 */ +@keyframes banner-in { + from { + opacity: 0; + transform: translateX(-50%) translateY(-8px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +/* 下拉菜单进入 */ +@keyframes menu-in { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 侧边栏 ✦ 温和闪烁(演化发生时) */ +@keyframes star-pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } +} +.brand-star.pulse { + animation: star-pulse 1.2s var(--ease) 2; +} diff --git a/emrg/gui/renderer/css/base.css b/emrg/gui/renderer/css/base.css new file mode 100644 index 00000000..d9c8158d --- /dev/null +++ b/emrg/gui/renderer/css/base.css @@ -0,0 +1,108 @@ +/* base.css — reset / 字体 / 滚动条 / 通用基础 */ + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body { + height: 100%; + overflow: hidden; + font-family: var(--font-sans); + font-size: var(--fs-body); + line-height: var(--lh-body); + background: var(--bg); + color: var(--text-1); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +/* 主题切换过渡(仅作用于整体背景/文字,避免闪烁) */ +body.theme-transition, +body.theme-transition * { + transition: + background-color var(--dur-slow) var(--ease), + border-color var(--dur-slow) var(--ease), + color var(--dur-slow) var(--ease) !important; +} + +button { + font-family: inherit; + font-size: var(--fs-secondary); + border: none; + background: none; + cursor: pointer; + color: inherit; + border-radius: var(--radius-btn); + transition: + background-color var(--dur-fast) var(--ease), + color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease), + transform var(--dur-fast) var(--ease); +} +button:active { + transform: scale(0.97); +} +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +textarea, +input, +select { + font-family: inherit; + font-size: var(--fs-body); + color: var(--text-1); + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-input); + outline: none; + transition: + border-color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease), + background-color var(--dur-fast) var(--ease); +} +textarea:focus, +input:focus, +select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} + +::placeholder { + color: var(--text-3); +} + +/* 滚动条(WebKit) */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-hover); +} +::-webkit-scrollbar-track { + background: transparent; +} + +/* 代码块等宽字体 */ +code, +pre, +.tool-output { + font-family: var(--font-mono); +} + +.hidden { + display: none !important; +} + +[hidden] { + display: none !important; +} diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css new file mode 100644 index 00000000..e3e2b66d --- /dev/null +++ b/emrg/gui/renderer/css/components.css @@ -0,0 +1,1437 @@ +/* components.css — 组件:对话项 / 消息气泡 / 输入按钮 / 工具友好行 / 对话框 / 空状态 / 模型切换器 */ + +/* ── 侧边栏对话项 ─────────────────────── */ +.conv-item { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: 9px var(--sp-3); + border-radius: 10px; + cursor: pointer; + color: var(--text-2); + font-size: var(--fs-secondary); + margin-bottom: 1px; + transition: + background-color var(--dur-fast) var(--ease), + color var(--dur-fast) var(--ease); +} +.conv-item:hover { + background: var(--bg-soft); + color: var(--text-1); +} +.conv-item.active { + background: var(--accent-soft); + color: var(--accent); + font-weight: 500; +} +.conv-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.conv-item.placeholder { + color: var(--text-3); + cursor: default; + justify-content: center; +} + +/* ── 消息 ─────────────────────────────── */ +.msg { + max-width: 100%; +} +.msg.user { + align-self: flex-end; + /* rant 21:28:49:flex 容器中 auto margin 优先于 align-self——.session-view > * 的 + margin-left/right:auto(居中 760px)盖掉了 align-self:flex-end → 用户气泡被居中 + 而非右对齐。margin-left:auto + margin-right:0(特异性 0,2,0 覆盖 0,1,0)恢复右对齐。 */ + margin-left: auto; + margin-right: 0; + background: var(--accent-soft); + color: var(--text-1); + border-radius: var(--radius-bubble); + border-bottom-right-radius: 6px; + padding: 10px 16px; + max-width: 72%; + white-space: pre-wrap; + word-break: break-word; + animation: msg-in var(--dur-med) var(--ease); +} +.msg.assistant { + display: flex; + flex-direction: column; + gap: var(--sp-1); +} +.msg-assistant-mark { + color: var(--accent); + font-weight: 600; + user-select: none; +} +.msg-body { + color: var(--text-1); + font-size: var(--fs-body); + line-height: var(--lh-body); + word-break: break-word; +} +.msg-body.typing::after { + content: "▍"; + color: var(--accent); + animation: cursor-pulse 1s ease-in-out infinite; +} +.msg.system { + text-align: center; + font-size: var(--fs-secondary); + color: var(--text-3); + padding: var(--sp-1) 0; + user-select: none; +} +.msg.remote-label { + font-size: var(--fs-aux); + color: var(--text-3); +} +/* 历史消息(rant 14:15:12:只读展示,视觉弱化区分于实时消息) */ +.msg.user.history { + opacity: 0.72; + animation: none; +} +.history-load-bar { + text-align: center; + font-size: var(--fs-secondary); + color: var(--accent); + padding: var(--sp-2) 0; + cursor: pointer; + user-select: none; +} + +/* Markdown 内容排版 */ +.msg-body p { + margin: 0 0 0.6em; +} +.msg-body p:last-child { + margin-bottom: 0; +} +.msg-body h1, .msg-body h2, .msg-body h3, .msg-body h4 { + margin: 0.8em 0 0.4em; + font-weight: 600; + line-height: 1.4; +} +.msg-body h1 { font-size: 1.35em; } +.msg-body h2 { font-size: 1.2em; } +.msg-body h3 { font-size: 1.1em; } +.msg-body ul, .msg-body ol { + margin: 0.4em 0; + padding-left: 1.5em; +} +.msg-body li { + margin: 0.2em 0; +} +.msg-body code { + background: var(--bg-soft); + border-radius: 4px; + padding: 0.1em 0.35em; + font-size: 0.88em; +} +.msg-body pre { + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: var(--radius-card); + padding: var(--sp-3); + overflow-x: auto; + margin: 0.5em 0; +} +.msg-body pre code { + background: none; + padding: 0; + font-size: 0.85em; + line-height: 1.55; +} +/* 代码块容器(设计 §3.3:圆角浅底 + 复制按钮) */ +.code-block { + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: var(--radius-card); + margin: 0.5em 0; + overflow: hidden; +} +.code-block .code-head { + display: flex; + justify-content: flex-end; + padding: 4px 8px; + background: color-mix(in srgb, var(--bg-panel) 60%, transparent); + border-bottom: 1px solid var(--border); +} +.code-copy { + background: none; + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-2); + font-size: var(--fs-aux); + padding: 2px 10px; + min-height: 24px; + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); +} +.code-copy:hover { background: var(--bg-panel); color: var(--text-1); } +.code-block pre { + border: none; + border-radius: 0; + margin: 0; + padding: var(--sp-3); +} +.msg-body blockquote { + border-left: 3px solid var(--border); + color: var(--text-2); + margin: 0.5em 0; + padding-left: var(--sp-3); +} +.msg-body a { + color: var(--accent); + text-decoration: none; +} +.msg-body a:hover { + text-decoration: underline; +} +.msg-body table { + border-collapse: collapse; + margin: 0.5em 0; + font-size: 0.92em; +} +.msg-body th, .msg-body td { + border: 1px solid var(--border); + padding: 6px 10px; + text-align: left; +} +.msg-body img { + max-width: 100%; + border-radius: var(--radius-card); +} +.msg-body hr { + border: none; + border-top: 1px solid var(--border); + margin: 1em 0; +} + +/* ── 工具友好状态行 ───────────────────── */ +.tool-row { + display: flex; + align-items: center; + gap: var(--sp-2); + font-size: var(--fs-small); + color: var(--text-2); + padding: 2px 2px; + border-radius: 8px; + cursor: pointer; + user-select: none; + transition: + background-color var(--dur-fast) var(--ease), + color var(--dur-fast) var(--ease); +} +.tool-row:hover { + background: var(--bg-soft); +} +.tool-row .tool-spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 1.5px solid var(--amber-soft); + border-top-color: var(--amber); + border-radius: 50%; + animation: spin 0.8s linear infinite; + flex-shrink: 0; +} +/* rant 21:08:工具完成后 spinner 不再转圈(JS 亦移除元素,CSS 兜底防闪烁) */ +.tool-row:not(.running) .tool-spinner { + display: none; +} +.tool-row.running { + color: var(--amber); +} +.tool-row.done { + color: var(--text-2); +} +.tool-row.done .tool-check { + color: var(--accent); + font-weight: 700; +} +.tool-row.failed { + color: var(--red); +} +.tool-row .tool-chevron { + font-size: 10px; + color: var(--text-3); + margin-left: auto; + transition: transform var(--dur-med) var(--ease); +} +.tool-row .tool-intent { + font-size: var(--fs-secondary); + color: var(--text-2); + margin-left: var(--sp-2); + font-style: italic; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.tool-row.expanded .tool-chevron { + transform: rotate(180deg); +} +.tool-output { + font-size: var(--fs-secondary); + line-height: 1.6; + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: 10px; + padding: var(--sp-3); + margin: 2px 0 6px; + max-height: 320px; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-word; +} +.tool-expand-btn { + font-size: var(--fs-aux); + color: var(--accent); + padding: 2px 6px; + margin: 0 0 6px 2px; +} + +/* rant 21:28:49:工具耗时(成功时 label 后 · 3.2s) */ +.tool-time { + font-size: var(--fs-aux); + color: var(--text-3); + margin-left: 4px; +} + +/* ── 连续工具合并组(方案 A:⌄ N 个工具执行 · Ts) ─────── */ +.tool-group { + display: flex; + flex-direction: column; +} +.tool-group-bar { + display: flex; + align-items: center; + gap: var(--sp-1); + font-size: var(--fs-small); + color: var(--text-3); + padding: 2px 4px; + border-radius: 6px; + cursor: pointer; + user-select: none; + transition: background-color var(--dur-fast) var(--ease); +} +.tool-group-bar:hover { + background: var(--bg-soft); +} +.tool-group-bar .tool-group-chev { + font-size: 10px; + transition: transform var(--dur-med) var(--ease); +} +.tool-group.collapsed .tool-group-rows { + display: none; +} +.tool-group:not(.collapsed) .tool-group-bar .tool-group-chev { + transform: rotate(180deg); +} + +/* ── 空状态欢迎屏 ─────────────────────── */ +#empty-state { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--sp-4); + padding: var(--sp-6); + text-align: center; +} +.empty-hello { + font-size: 26px; + font-weight: 600; + color: var(--text-1); + display: flex; + align-items: center; + gap: var(--sp-2); +} +.empty-hello .brand-star { + color: var(--accent); + font-size: 26px; +} +.empty-sub { + color: var(--text-2); + font-size: var(--fs-secondary); + max-width: 420px; +} +.example-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--sp-3); + max-width: 520px; + width: 100%; +} +.example-card { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-card); + padding: var(--sp-4); + font-size: var(--fs-secondary); + color: var(--text-2); + text-align: left; + box-shadow: var(--shadow-sm); + transition: + border-color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease), + transform var(--dur-fast) var(--ease); +} +.example-card:hover { + border-color: var(--accent); + box-shadow: var(--shadow-md); + transform: translateY(-1px); + color: var(--text-1); +} + +/* ── 按钮 ─────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--sp-2); + padding: 8px var(--sp-4); + border-radius: var(--radius-btn); + font-size: var(--fs-secondary); + min-height: 36px; + transition: + background-color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease), + transform var(--dur-fast) var(--ease); +} +.btn-primary { + background: var(--accent); + color: #fff; + font-weight: 500; + box-shadow: var(--shadow-sm); +} +.btn-primary:hover { + background: var(--accent-strong); + box-shadow: var(--shadow-md); +} +.btn-ghost { + background: var(--bg-soft); + color: var(--text-2); +} +.btn-ghost:hover { + background: var(--border); + color: var(--text-1); +} +.btn-danger { + background: var(--red); + color: #fff; +} +.btn-danger:hover { + background: #d0343a; +} + +/* 发送 / 停止按钮(圆形) */ +.send-btn, +.stop-btn { + width: 38px; + height: 38px; + min-width: 38px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; + transition: + background-color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease), + transform var(--dur-fast) var(--ease); +} +.send-btn { + background: var(--accent); + color: #fff; + box-shadow: var(--shadow-sm); +} +.send-btn:hover:not(:disabled) { + background: var(--accent-strong); + box-shadow: var(--shadow-md); +} +.stop-btn { + background: var(--red); + color: #fff; +} +.stop-btn:hover { + background: #d0343a; +} + +/* ── 模型切换器 ───────────────────────── */ +.model-switcher { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 999px; + font-size: var(--fs-secondary); + color: var(--text-2); + background: transparent; + border: 1px solid transparent; + cursor: pointer; + transition: + background-color var(--dur-fast) var(--ease), + border-color var(--dur-fast) var(--ease); +} +.model-switcher:hover { + background: var(--bg-soft); + border-color: var(--border); +} +.model-switcher.highlight { + background: var(--accent-soft); + color: var(--accent); + border-color: var(--accent); +} +.model-menu { + position: absolute; + bottom: 52px; + left: var(--sp-5); + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-lg); + padding: var(--sp-2); + min-width: 240px; + z-index: 30; + animation: menu-in var(--dur-fast) var(--ease); +} +.model-menu-item { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: 8px 10px; + border-radius: 8px; + cursor: pointer; + font-size: var(--fs-secondary); + color: var(--text-1); +} +.model-menu-item:hover { + background: var(--bg-soft); +} +/* 键盘导航高亮(↑↓) */ +.model-menu-item.active { + background: var(--accent-soft); + color: var(--accent); +} +.model-menu-item .model-check { + color: var(--accent); + font-weight: 700; +} +.model-menu-item .model-vision { + font-size: var(--fs-aux); + color: var(--text-3); +} +.model-menu-empty { + padding: var(--sp-3); + font-size: var(--fs-secondary); + color: var(--text-3); + text-align: center; +} +.model-menu-empty a { + color: var(--accent); + cursor: pointer; +} + +/* 回到底部悬浮按钮(上滑阅读时出现,不打扰) */ +.back-to-bottom { + position: absolute; + bottom: 96px; + left: 50%; + transform: translateX(-50%); + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--bg-panel); + border: 1px solid var(--border); + box-shadow: var(--shadow-md); + color: var(--text-2); + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + z-index: 15; + transition: + opacity var(--dur-med) var(--ease), + background-color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease); +} +.back-to-bottom:hover { + background: var(--accent); + color: #fff; + box-shadow: var(--shadow-lg); +} + +/* ── 连接状态圆点 ─────────────────────── */ +.status-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--text-3); + flex-shrink: 0; + transition: background-color var(--dur-med) var(--ease); +} +.status-dot.green { + background: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} +.status-dot.red { + background: var(--red); + box-shadow: 0 0 0 3px var(--red-soft); +} +.status-dot.gray { + background: var(--text-3); +} + +/* ── 对话框 ───────────────────────────── */ +dialog { + border: none; + padding: 0; + background: transparent; + border-radius: var(--radius-dialog); +} +dialog::backdrop { + background: rgba(0, 0, 0, 0.35); + backdrop-filter: blur(2px); + animation: fade-in var(--dur-slow) var(--ease); +} +.dialog-card { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-dialog); + box-shadow: var(--shadow-lg); + padding: var(--sp-5); + min-width: 400px; + max-width: 520px; + animation: dialog-in var(--dur-slow) var(--ease); +} +.dialog-card h2 { + font-size: var(--fs-title); + font-weight: 600; + margin-bottom: var(--sp-4); + color: var(--text-1); +} +.dialog-card label { + display: block; + font-size: var(--fs-secondary); + color: var(--text-2); + margin-bottom: var(--sp-3); +} +.dialog-card label > input, +.dialog-card label > select, +.dialog-card label > textarea { + width: 100%; + margin-top: var(--sp-1); + padding: 8px 10px; +} +/* rant 2026-08-07T13:32Z: rant dialog textarea was unstyled — default box was + tiny. Full width + comfortable min-height + vertical resize + inherited font. */ +.dialog-card label > textarea { + min-height: 120px; + resize: vertical; + font: inherit; + line-height: 1.5; +} +.dialog-card .hint { + font-size: var(--fs-aux); + color: var(--text-3); + margin-top: 4px; +} +.dialog-actions { + display: flex; + justify-content: flex-end; + gap: var(--sp-2); + margin-top: var(--sp-5); +} +.dir-row { + display: flex; + gap: var(--sp-2); + margin-top: var(--sp-1); +} +.dir-row input { + flex: 1; + padding: 8px 10px; +} + +/* 设置分组 */ +#settings-body-model .settings-group > label { + display: flex; + flex-direction: column; + gap: var(--sp-1); /* 4px */ + margin-bottom: var(--sp-2); /* 8px */ +} +#settings-body-model .settings-group > label input { + width: 100%; +} +.settings-group { + margin-bottom: var(--sp-4); +} +.settings-group-title { + font-size: var(--fs-secondary); + font-weight: 600; + color: var(--text-1); + margin-bottom: var(--sp-2); +} + +/* 多模型管理列表 */ +#model-list { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 4px; +} +.model-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg); + transition: + border-color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease); +} +.model-item.default { + border-color: var(--accent); + background: var(--accent-soft); +} +.model-radio { + width: 18px; + height: 18px; + min-width: 18px; + border-radius: 50%; + border: 2px solid var(--text-3); + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + color: var(--accent); + padding: 0; + background: transparent; + transition: border-color var(--dur-fast) var(--ease); +} +.model-radio.checked { + border-color: var(--accent); +} +.model-radio:hover { + border-color: var(--accent); +} +.model-name { + font-size: var(--fs-secondary); + font-weight: 500; + color: var(--text-1); + display: flex; + align-items: center; + gap: 6px; +} +.model-badge { + font-size: 10px; + background: var(--accent); + color: #fff; + border-radius: 999px; + padding: 1px 8px; + font-weight: 600; +} +.model-id { + font-size: var(--fs-aux); + color: var(--text-3); + font-family: var(--font-mono); +} +.model-vision { + font-size: var(--fs-aux); + color: var(--text-2); +} +.model-actions { + margin-left: auto; + display: flex; + gap: 4px; + flex-shrink: 0; +} +.model-action-btn { + font-size: var(--fs-aux); + color: var(--text-2); + padding: 4px 8px; + border-radius: 6px; +} +.model-action-btn:hover { + background: var(--bg-soft); + color: var(--text-1); +} +.model-action-btn.danger:hover { + background: var(--red-soft); + color: var(--red); +} +.model-list-empty { + font-size: var(--fs-aux); + color: var(--text-3); + text-align: center; + padding: var(--sp-2); +} + +/* 行内模型表单 */ +.model-form { + margin-top: 8px; + padding: var(--sp-3); + border: 1px dashed var(--border); + border-radius: 10px; + background: var(--bg-soft); +} +.model-form-row { + display: flex; + gap: var(--sp-3); +} +.model-form-label { + flex: 1; + margin-bottom: 0; +} +.model-form-label input { + width: 100%; + margin-top: 2px; + padding: 6px 10px; + font-size: var(--fs-secondary); +} +.model-form-footer { + display: flex; + align-items: center; + margin-top: var(--sp-2); +} +.model-form-vision { + display: flex; + align-items: center; + gap: 6px; + font-size: var(--fs-secondary); + color: var(--text-2); + margin-bottom: 0; + cursor: pointer; +} +.model-form-vision input { + accent-color: var(--accent); +} + +/* 定时任务管理(rant 2026-08-12T18:23:15 P3:GUI 任务/自定义类型 CRUD) */ +/* 无 max-height/overflow:任务列表随内容完全展开,不出现内部垂直滚动条(rant 2026-08-24T21:21:58) */ +.task-list { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 4px; +} +.task-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-soft); +} +.task-name { + font-weight: 600; + flex-shrink: 0; + max-width: 40%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.task-hint { + font-size: var(--fs-aux); + color: var(--text-3); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.task-badge { + font-size: var(--fs-aux); + color: var(--text-3); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0 4px; + flex-shrink: 0; +} +/* rant 21:36:01:Rant 面板 5 列 + 状态徽标三态配色(已完成绿 / 进行中琥珀 / 待处理灰) */ +.rant-head { + display: flex; + gap: 8px; + padding: 2px 8px 6px; + font-size: var(--fs-aux); + font-weight: 600; + color: var(--text-3); +} +.rant-head .rant-col-time, +.rant-row .rant-col-time { + flex: 0 0 150px; +} +.rant-head .rant-col-project, +.rant-row .rant-col-project { + flex: 0 0 90px; +} +.rant-head .rant-col-status, +.rant-row .rant-col-status { + flex: 0 0 76px; +} +.rant-head .rant-col-progress, +.rant-row .rant-col-progress { + flex: 1.2; + min-width: 0; +} +.rant-head .rant-col-content, +.rant-row .rant-col-content { + flex: 2; + min-width: 0; +} +.rant-head span, +.rant-row .rant-col-time, +.rant-row .rant-col-project, +.rant-row .rant-col-progress, +.rant-row .rant-col-content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.rant-row .rant-col-time, +.rant-row .rant-col-project, +.rant-row .rant-col-progress, +.rant-row .rant-col-content { + font-size: var(--fs-aux); + color: var(--text-2); +} +.task-badge.badge-done { + color: #16a34a; + border-color: #16a34a; + background: color-mix(in srgb, #16a34a 10%, transparent); +} +.task-badge.badge-warn { + color: #d97706; + border-color: #d97706; + background: color-mix(in srgb, #d97706 10%, transparent); +} +.task-badge.badge-muted { + color: var(--text-3); + border-color: var(--border); + background: transparent; +} +.rant-detail { + padding: 6px 8px; + border-top: 1px solid var(--border); + font-size: var(--fs-secondary); +} +.rant-meta { + font-size: var(--fs-aux); + color: var(--text-3); + margin-bottom: 4px; +} +.rant-md { + margin-top: 4px; +} +.rant-progress { + margin-top: 8px; + padding-top: 6px; + border-top: 1px dashed var(--border); + font-size: var(--fs-aux); + color: var(--text-3); + word-break: break-all; +} +/* rant 10:41:43:rant 详情 markdown —— 【】标题预处理后的 h4 层次 + 嵌套列表缩进可见 */ +.rant-md h4 { + margin: 10px 0 4px; + font-size: 13px; + color: var(--text-1); +} +.rant-md li > ul, +.rant-md li > ol { + margin-left: 1em; +} +.task-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} +.task-empty { + font-size: var(--fs-aux); + color: var(--text-3); + text-align: center; + padding: var(--sp-2); +} + +.task-form { + margin-top: 8px; + padding: var(--sp-3); + border: 1px dashed var(--border); + border-radius: 10px; + background: var(--bg-soft); +} +.task-form select, +.task-form textarea { + width: 100%; + margin-top: 2px; + padding: 6px 10px; + font-size: var(--fs-secondary); +} + +/* rant 09:23:10:任务 running 徽标(常驻运行的任务从源头减少误点) */ +.task-running-badge { + background: var(--bg-soft); + color: var(--text-3); + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + margin-left: 4px; +} + +/* rant 10:36:39:任务状态徽标(待运行/待调度)+ 下次运行倒计时 */ +.task-pending-badge, +.task-idle-badge { + background: var(--bg-soft); + color: var(--text-3); + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + margin-left: 4px; +} +.task-pending-badge { + color: var(--accent); + background: var(--accent-soft, var(--bg-soft)); +} +.task-next-run { + font-size: var(--fs-aux); + color: var(--text-3); + flex-shrink: 0; + white-space: nowrap; + font-variant-numeric: tabular-nums; /* 倒计时走秒不抖动 */ +} + +/* rant 2026-08-18T10:45:52:任务行"上次执行"元信息(运行时间/摘要/降频标识) */ +/* rant 2026-08-20T10:34:40:主表一行 —— 去掉 flex-basis:100%(曾强制"上次运行"独占一行), + 改为 flex: 0 1 auto 与 actions 同行;窄窗口下靠自身 flex-wrap 内部合理换行 */ +.task-meta { + flex: 0 1 auto; + display: flex; + align-items: center; + gap: 6px; + font-size: var(--fs-aux); + color: var(--text-3); + min-width: 0; + flex-wrap: wrap; +} +.task-meta-item { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.task-meta-summary { + max-width: 60%; + color: var(--text-2); +} +.task-saturation-badge { + background: var(--warn-soft, var(--bg-soft)); + color: var(--warn, var(--text-3)); + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + border: 1px solid var(--warn-soft, var(--border)); +} +/* rant 2026-08-18T21:32:32:任务卡点击展开的最近运行子表(时间/干了什么/降频) */ +.task-run-detail { + flex-basis: 100%; + display: flex; + flex-direction: column; + gap: 2px; + margin-top: 4px; + padding-top: 4px; + border-top: 1px dashed var(--border); + min-width: 0; +} +.task-run-detail.hidden { + display: none; +} +.task-run-head, +.task-run-row { + display: grid; + grid-template-columns: 72px 1fr 92px 1fr; + gap: 6px; + font-size: var(--fs-aux); + min-width: 0; + align-items: center; +} +.task-run-head { + color: var(--text-3); + font-weight: 600; +} +.task-run-time { + color: var(--text-3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.task-run-done { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-2); +} +.task-run-reason { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-3); +} +/* rant 2026-08-20T22:59:16 #3:work/reason cell 多行截断(限制行数 + 省略号), + 点击 cell 在本条记录下方展开完整内容(Markdown 渲染),再点收起 */ +.task-run-cell { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + white-space: normal; + cursor: pointer; + line-height: 1.35; +} +.task-run-cell:hover { + color: var(--accent, var(--text-2)); +} +.task-run-expand { + flex-basis: 100%; + grid-column: 1 / -1; + font-size: var(--fs-aux); + color: var(--text-2); + background: var(--bg-soft, transparent); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 8px; + margin: 2px 0; + overflow-wrap: anywhere; +} +.task-run-expand.hidden { + display: none; +} +.task-run-expand :first-child { + margin-top: 0; +} +.task-run-expand :last-child { + margin-bottom: 0; +} +.task-run-flag { + display: flex; + gap: 4px; + align-items: center; + min-width: 0; +} +.task-run-badge-warn { + background: var(--warn-soft, var(--bg-soft)); + color: var(--warn, var(--text-3)); + border-color: var(--warn-soft, var(--border)); +} +.task-run-badge-idle { + color: var(--text-3); +} +.task-run-empty { + flex-basis: 100%; + font-size: var(--fs-aux); + color: var(--text-3); + padding: 4px 0; +} + +/* rant 09:17:45:提示词编辑器 Monaco 挂载容器(长提示词 300px 起步) */ +.monaco-host { + width: 100%; + min-height: 300px; + margin-top: 2px; + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + background: var(--bg-1); +} +.task-form .hidden, +.monaco-host.hidden { + display: none; +} + +.theme-options { + display: flex; + gap: var(--sp-2); +} +.theme-option { + flex: 1; + padding: 8px; + border: 1px solid var(--border); + border-radius: var(--radius-btn); + font-size: var(--fs-secondary); + text-align: center; + color: var(--text-2); + transition: + border-color var(--dur-fast) var(--ease), + background-color var(--dur-fast) var(--ease), + color var(--dur-fast) var(--ease); +} +.theme-option:hover { + border-color: var(--accent); +} +.theme-option.active { + border-color: var(--accent); + background: var(--accent-soft); + color: var(--accent); + font-weight: 500; +} + +/* 首启向导 */ +.welcome-brand { + text-align: center; + margin-bottom: var(--sp-4); +} +.welcome-brand .brand-star { + font-size: 34px; + color: var(--accent); +} +.welcome-brand h1 { + font-size: var(--fs-title-lg); + font-weight: 600; + margin-top: var(--sp-2); +} +.welcome-brand p { + color: var(--text-2); + font-size: var(--fs-secondary); + margin-top: var(--sp-1); +} + +/* ── 关于(演化计数) ──────────────────── */ +.about-line { + font-size: var(--fs-secondary); + color: var(--text-2); + margin-bottom: var(--sp-2); +} + +/* ── 右键菜单(对话列表:重命名/删除) ─────────── */ +.ctx-menu { + position: fixed; + z-index: 9999; + min-width: 140px; + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-lg); + padding: 4px; + animation: fade-in var(--dur-fast) var(--ease); +} +.ctx-item { + display: block; + width: 100%; + text-align: left; + padding: 8px 12px; + border: none; + background: none; + border-radius: 8px; + font-size: var(--fs-secondary); + color: var(--text-1); + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease); +} +.ctx-item:hover { background-color: var(--bg-soft); } +/* 键盘导航高亮(↑↓) */ +.ctx-item.active { background-color: var(--accent-soft); color: var(--accent); } +.ctx-item.danger { color: var(--danger, #e5484d); } +.ctx-item.danger:hover { background-color: color-mix(in srgb, var(--danger, #e5484d) 10%, transparent); } +.ctx-item.danger.active { background-color: color-mix(in srgb, var(--danger, #e5484d) 10%, transparent); color: var(--danger, #e5484d); } + +/* ── / 指令补全菜单(rant 19:44 P1) ─────────────── */ +.cmd-menu { + position: absolute; + bottom: calc(100% + 8px); + left: 0; + right: 0; + max-height: 240px; + overflow-y: auto; + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-lg); + padding: 4px; + z-index: 100; + animation: fade-in var(--dur-fast) var(--ease); +} +.cmd-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + text-align: left; + padding: 8px 12px; + border: none; + background: none; + border-radius: 8px; + font-size: var(--fs-secondary); + color: var(--text-1); + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease); +} +.cmd-menu-item:hover { background-color: var(--bg-soft); } +.cmd-menu-item.selected { background-color: var(--accent-soft); color: var(--accent); } +.cmd-menu-name { font-family: var(--font-mono, monospace); font-weight: 600; } +.cmd-menu-hint { color: var(--text-2); font-size: var(--fs-aux); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.cmd-menu-item.selected .cmd-menu-hint { color: var(--accent); } + +/* / 指令帮助对话框(/help) */ +.help-list { max-height: 300px; overflow-y: auto; } +.help-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 7px 4px; + border-bottom: 1px solid var(--border); +} +.help-row:last-child { border-bottom: none; } +.help-cmd { font-family: var(--font-mono, monospace); font-weight: 600; color: var(--accent); } +.help-hint { color: var(--text-2); font-size: var(--fs-secondary); } + +/* / 指令 P3:记忆详情 / 技能列表 */ +.memory-detail { + margin-top: var(--sp-3, 12px); + padding: var(--sp-3, 12px); + background: var(--bg-soft); + border-radius: var(--radius-sm, 8px); + max-height: 220px; + overflow-y: auto; +} +.memory-detail-title { font-weight: 600; margin-bottom: 6px; } +.memory-detail-body { + font-family: var(--font-mono, monospace); + font-size: var(--fs-aux, 12px); + white-space: pre-wrap; + word-break: break-word; + margin: 0; +} + +/* ── 工作模式切换器(Ask/Auto,WorkBuddy P2) ─────────────── */ +.mode-switcher { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 2px; + border-radius: 999px; + background: var(--bg-soft); + border: 1px solid var(--border); + margin-left: 8px; +} +.mode-btn { + border: none; + background: transparent; + color: var(--text-3); + font-size: 11px; + font-weight: 600; + padding: 3px 10px; + border-radius: 999px; + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); +} +.mode-btn.active { + background: var(--bg-panel); + color: var(--accent); + box-shadow: 0 1px 3px rgba(0,0,0,0.12); +} +.mode-btn:hover:not(.active) { + color: var(--text-1); +} + +/* ── 通用 toast(rant 2026-08-15T09:20:27:面板操作反馈全局可见) ── */ +.toast { + position: fixed; + top: 16px; + right: 20px; + z-index: 1100; + max-width: calc(100vw - 40px); + padding: 10px 16px; + border-radius: var(--radius-btn); + background: var(--bg-panel); + border: 1px solid var(--border); + box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.18)); + animation: toast-in 0.22s var(--ease); + pointer-events: none; +} +.toast-msg { + font-size: var(--fs-secondary); + color: var(--text-1); + white-space: pre-wrap; +} +.toast-success { border-left: 4px solid var(--ok, #2e9e5b); } +.toast-error { border-left: 4px solid var(--danger, #d64545); } +.toast-info { border-left: 4px solid var(--accent, #4a7dff); } + +/* ── 进化完成 toast(WorkBuddy P3)────────────────── */ +.evolution-toast { + position: fixed; + right: 20px; + bottom: 20px; + z-index: 1000; + width: 320px; + max-width: calc(100vw - 40px); + padding: var(--sp-4); + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.18)); + animation: toast-in 0.25s var(--ease); +} +.evolution-toast-title { + font-size: var(--fs-title, 15px); + font-weight: 600; + color: var(--text-1); +} +.evolution-toast-msg { + margin-top: var(--sp-1); + font-size: var(--fs-secondary); + color: var(--text-2); +} +.evolution-toast-actions { + display: flex; + gap: var(--sp-2); + margin-top: var(--sp-3); + justify-content: flex-end; +} +@keyframes toast-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* WorkBuddy P3:设置 → 关于区 */ +.about-row { + font-size: var(--fs-secondary, 13px); + color: var(--text-2); + margin-bottom: 4px; +} +.about-row b { color: var(--accent); } +.about-recent { + margin-top: 6px; + max-height: 180px; + overflow-y: auto; +} +.about-recent-title { + font-size: var(--fs-aux, 12px); + font-weight: 600; + color: var(--text-3); + margin-bottom: 4px; +} +.about-recent-item { + display: flex; + align-items: center; + gap: 8px; + font-size: var(--fs-aux, 12px); + color: var(--text-2); + padding: 4px 0; + border-bottom: 1px solid var(--border); +} +.about-recent-item:last-child { border-bottom: none; } +.about-recent-time { + font-family: var(--font-mono, monospace); + color: var(--text-3); + white-space: nowrap; + font-size: 11px; +} + diff --git a/emrg/gui/renderer/css/layout.css b/emrg/gui/renderer/css/layout.css new file mode 100644 index 00000000..044e69e7 --- /dev/null +++ b/emrg/gui/renderer/css/layout.css @@ -0,0 +1,943 @@ +/* layout.css — 整体布局:左侧边栏 + 工作区(#workspace 会话/面板视图 + 输入卡片) + * 结构: + * #app + * #sidebar(品牌区 + 新对话 + 分组列表 + 底部设置/连接圆点) + * #main(#workspace 视图容器 + #empty-state 欢迎屏 + #composer-wrap 输入卡片) + */ + +#app { + display: flex; + height: 100vh; + width: 100vw; + position: relative; /* #result-resizer 绝对定位锚点(P2 框架) */ +} + +/* ── 侧边栏 ─────────────────────────────── */ +#sidebar { + width: 264px; + min-width: 220px; + display: flex; + flex-direction: column; + height: 100%; /* 撑满 #app 高度,让 .sidebar-footer 能贴底(rant 2026-08-22T07:10:44) */ + background: var(--bg-panel); + border-right: 1px solid var(--border); +} + +.sidebar-brand { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-4) var(--sp-4) var(--sp-3); + font-size: var(--fs-title-lg); + font-weight: 600; + color: var(--text-1); +} +.sidebar-brand .brand-star { + color: var(--accent); + font-size: 18px; +} + +/* 侧边栏导航(rant 14:10:14 P1:5 入口 activity-bar 式) */ +.side-nav { + display: flex; + gap: 2px; + padding: 0 var(--sp-3) var(--sp-2); +} +.side-nav-item { + flex: 1; + min-height: 30px; + border: 1px solid transparent; + border-radius: var(--radius-card); + background: transparent; + color: var(--text-2); + font-size: 15px; + line-height: 1; + cursor: pointer; + transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); +} +.side-nav-item:hover { + background: var(--bg-soft); + color: var(--text-1); +} +.side-nav-item.active { + background: color-mix(in srgb, var(--accent) 14%, transparent); + color: var(--accent); +} +/* 工作区视图(rant 18:55:09 v0.2:面板从侧边栏移入 #workspace,整块工作区显示)—— + * 默认隐藏;.active 时填充整个工作区(宽布局,不再受侧边栏 264px 限制) */ +.workspace-view { + display: none; + flex-direction: column; + height: 100%; + overflow-y: auto; + padding: var(--sp-4); +} +.workspace-view.active { + display: flex; +} +/* 面板视图内容包装(v0.1 .side-panel-body 视觉对等:正文级字号 + 次级文字色) */ +.workspace-view-body { + font-size: var(--fs-secondary); + color: var(--text-3); +} + +/* rant 21:49:51:面板视图标题(设置/Rant/项目/任务面板统一) */ +.workspace-view-title { + font-size: var(--fs-title); + font-weight: 600; + color: var(--text-1); + margin: 0 0 var(--sp-3); +} + +/* rant 14:10:14 P2:设置面板 tab(模型服务/工作目录/GitHub/外观/语言/关于) */ +.panel-tabs { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: var(--sp-3); +} +.panel-tab { + flex: 1; + min-width: 0; + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-2); + font-size: var(--fs-secondary); + line-height: 1.2; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); +} +.panel-tab:hover { + background: var(--bg-soft); + color: var(--text-1); +} +.panel-tab.active { + background: color-mix(in srgb, var(--accent) 14%, transparent); + border-color: color-mix(in srgb, var(--accent) 40%, transparent); + color: var(--accent); +} +.panel-tab-body .settings-group { + margin-bottom: var(--sp-3); +} +.panel-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: var(--sp-3); + padding-top: var(--sp-2); + border-top: 1px solid var(--border); +} + +/* 成长状态卡(WorkBuddy P3:自进化可见化) */ +.growth-card { + margin: 0 var(--sp-3) var(--sp-3); + padding: var(--sp-2) var(--sp-3); + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 8%, transparent), transparent); +} +.growth-line { + font-size: var(--fs-secondary); + font-weight: 600; + color: var(--text-1); +} +.growth-line b { + color: var(--accent); +} +.growth-note { + margin-top: 2px; + font-size: var(--fs-aux); + color: var(--text-3); +} + +.new-chat-btn { + margin: 0 var(--sp-3) var(--sp-3); + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-2); + padding: 9px var(--sp-3); + background: var(--accent); + color: #fff; + font-size: var(--fs-secondary); + font-weight: 500; + border-radius: var(--radius-btn); + box-shadow: var(--shadow-sm); + transition: + background-color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease), + transform var(--dur-fast) var(--ease); +} +.new-chat-btn:hover { + background: var(--accent-strong); + box-shadow: var(--shadow-md); +} + +/* B2(rant 21:59:11):"打开会话"入口——新对话按钮下的轻量 ghost 按钮 */ +.open-chat-btn { + margin: calc(-1 * var(--sp-1)) var(--sp-3) var(--sp-3); + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-2); + padding: 8px var(--sp-3); + background: transparent; + color: var(--text-2); + font-size: var(--fs-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-btn); + cursor: pointer; + transition: + background-color var(--dur-fast) var(--ease), + color var(--dur-fast) var(--ease); +} +.open-chat-btn:hover { + background: var(--bg-soft); + color: var(--text-1); +} + +#conv-list { + flex: 1; + overflow-y: auto; + padding: 0 var(--sp-2) var(--sp-2); +} + +.conv-group-label { + padding: var(--sp-3) var(--sp-2) var(--sp-1); + font-size: var(--fs-aux); + font-weight: 600; + color: var(--text-3); + letter-spacing: 0.02em; +} + +/* P4 slice 2:打开会话区(跨项目 tab)——紧贴新对话按钮,视觉轻量 */ +#open-sessions { + padding-bottom: var(--sp-1); + margin-bottom: var(--sp-1); + max-height: 40%; + overflow-y: auto; +} +.open-sessions-label { + padding-top: var(--sp-2); +} + +.sidebar-footer { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-3) var(--sp-4); + margin-top: auto; /* column flex 中自动贴到 #sidebar 底边(rant 2026-08-22T07:10:44) */ + border-top: 1px solid var(--border); +} + +/* ── 主区 ───────────────────────────────── */ +#main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + position: relative; + background: var(--bg); +} + +/* 工作区包装(rant 18:55:09 v0.2 起:#chat-view 更名 #workspace——会话视图 + 面板视图的 + * 公共父容器;每会话一个 .session-view,每面板一个 .workspace-view,display 互斥切换) */ +#workspace { + flex: 1; + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* 每会话滚动容器:只有激活会话可见,切换 display 保留状态(浏览器 tab 效果) */ +.session-view { + position: absolute; + inset: 0; + overflow-y: auto; + display: none; + flex-direction: column; + gap: var(--sp-3); + padding: var(--sp-5) var(--sp-5) var(--sp-4); + /* rant 21:28:49:去掉 smooth——JS scrollTop=scrollHeight 也走平滑动画,工具行快速 + 连续追加时滚不到底 → 最新内容不可见。JS 跳转全部即时;用户手动滚动不受影响。 */ +} +.session-view.active { + display: flex; +} +/* 会话标题栏:项目/名称(id) 或 项目/id,滚动置顶、全宽贴边(负 margin 抵消 .session-view padding) */ +.session-header { + position: sticky; + top: 0; + z-index: 5; + flex-shrink: 0; + margin: calc(-1 * var(--sp-5)) calc(-1 * var(--sp-5)) var(--sp-3) !important; + padding: var(--sp-3) var(--sp-5); + width: auto !important; + max-width: none !important; + font-size: var(--fs-secondary); + color: var(--text-2); + background: var(--bg); + border-bottom: 1px solid var(--border); +} +/* P3 finalize:会话连接断开标记(P4 每会话容器可见;激活会话另走全局横幅) */ +.session-view.disconnected { + opacity: 0.55; +} +.session-view.disconnected::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: repeating-linear-gradient(45deg, transparent 0 8px, rgba(128, 128, 128, 0.08) 8px 16px); +} +#workspace > *:not(.session-view):not(.workspace-view), +.session-view > * { + width: 100%; + max-width: 760px; + margin-left: auto; + margin-right: auto; +} +/* 工具执行全宽左对齐(宿主 2026-08-20 17:50:30):消息正文保持 760px 阅读宽度, + 工具行/组/输出不受限,铺满整个聊天窗口左侧。 */ +.session-view > .tool-row, +.session-view > .tool-group, +.session-view > .tool-group .tool-group-rows, +.session-view > .tool-output, +.session-view > .tool-expand-btn { + max-width: none; + margin-left: 0; + margin-right: 0; +} + +/* 输入区(宿主 2026-08-20 17:50:30:状态栏/输入框 100% 全宽,不再 808px 居中) */ +#composer-wrap { + position: relative; /* / 指令补全菜单锚点(rant 19:44 P1) */ + padding: var(--sp-2) var(--sp-5) var(--sp-4); + max-width: none; + margin: 0; + width: 100%; +} + +.composer-card { + display: flex; + align-items: flex-end; + gap: var(--sp-2); + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-input); + box-shadow: var(--shadow-md); + padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-4); + transition: + border-color var(--dur-fast) var(--ease), + box-shadow var(--dur-fast) var(--ease); +} +.composer-card:focus-within { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-md); +} + +#input { + flex: 1; + border: none; + background: transparent; + box-shadow: none; + resize: none; + max-height: 150px; + line-height: 1.6; + padding: var(--sp-2) 0; +} +#input:focus { + border: none; + box-shadow: none; +} + +.composer-hint { + text-align: center; + font-size: var(--fs-aux); + color: var(--text-3); + margin-top: var(--sp-2); +} + +/* ── 断连横幅 ───────────────────────────── */ +#conn-banner { + position: absolute; + top: var(--sp-3); + left: 50%; + transform: translateX(-50%); + background: var(--amber-soft); + color: var(--amber); + border: 1px solid var(--border); + border-radius: 999px; + padding: 6px var(--sp-4); + font-size: var(--fs-secondary); + box-shadow: var(--shadow-md); + z-index: 20; + animation: banner-in var(--dur-med) var(--ease); +} + +/* ── GitHub 连接横幅(Windows GCM rant Stage 2) ────── */ +#github-banner { + position: absolute; + top: var(--sp-3); + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: var(--sp-2); + background: var(--amber-soft); + color: var(--amber); + border: 1px solid var(--border); + border-radius: 999px; + padding: 6px var(--sp-3) 6px var(--sp-4); + font-size: var(--fs-secondary); + box-shadow: var(--shadow-md); + z-index: 20; + animation: banner-in var(--dur-med) var(--ease); + white-space: nowrap; + max-width: 90%; +} +#github-banner .btn { padding: 4px 12px; min-height: 0; font-size: var(--fs-secondary); } + +/* ── 升级完成横幅(rant 2026-08-20T18:30:57:版本变化 → 提示 + 一键重启) ── */ +#upgrade-banner { + position: absolute; + top: var(--sp-3); + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: var(--sp-2); + background: var(--accent-soft); + color: var(--accent); + border: 1px solid var(--border); + border-radius: 999px; + padding: 6px var(--sp-3) 6px var(--sp-4); + font-size: var(--fs-secondary); + box-shadow: var(--shadow-md); + z-index: 20; + animation: banner-in var(--dur-med) var(--ease); + white-space: nowrap; + max-width: 90%; +} +#upgrade-banner .btn { padding: 4px 12px; min-height: 0; font-size: var(--fs-secondary); } + +/* ── 侧边栏折叠(⌘B / 窄屏) ────────────── */ +body.sidebar-collapsed #sidebar { + display: none; +} +body.sidebar-collapsed #app { + /* 主区自动占满 */ +} + +/* 窄屏自动收起侧边栏 */ +@media (max-width: 720px) { + #sidebar { + display: none; + } + /* #workspace 现为绝对定位包装(padding 移入 .session-view,避免滚动条内缩) */ + .session-view { + padding: var(--sp-4) var(--sp-4) var(--sp-3); + } + #composer-wrap { + padding: var(--sp-2) var(--sp-3) var(--sp-3); + } +} + +/* ── 结果面板(右栏 Artifacts,WorkBuddy P1) ─────────────── */ +#result-panel { + width: 280px; + min-width: 240px; + display: flex; + flex-direction: column; + background: var(--bg-panel); + border-left: 1px solid var(--border); + overflow: hidden; + transition: width 0.2s cubic-bezier(.2,.8,.3,1), min-width 0.2s cubic-bezier(.2,.8,.3,1); +} +#result-panel.collapsed { + /* 方案 A(rant 2026-08-10T14:11:18):折叠成窄条而非 0,header(含 toggle 按钮)保持可见可点 */ + width: 40px; + min-width: 40px; + border-left: 1px solid var(--border); +} +/* P2 框架:拖拽调整宽度期间抑制 width transition(R1-①:transition 与 mousemove 冲突) */ +#result-panel.dragging { + transition: none; +} +#result-panel.collapsed .result-list { + display: none; /* 只藏内容区 */ +} +#result-panel.collapsed .result-pane { + display: none; /* 折叠时所有 pane 隐藏(含激活中的文件 pane) */ +} +#result-panel.collapsed .result-header { + display: flex; + justify-content: center; + padding: var(--sp-3) 0; +} +#result-panel.collapsed .result-title, +#result-panel.collapsed .result-tabs, +#result-panel.collapsed .result-tabbar { + display: none; /* 窄条只留 toggle 按钮 */ +} +#result-panel.collapsed .result-toggle { + transform: rotate(180deg); /* » → « 指示可展开 */ +} + +.result-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} +.result-title { + font-size: var(--fs-secondary); + font-weight: 600; + color: var(--text-1); +} +.result-toggle { + background: none; + border: none; + color: var(--text-3); + font-size: 16px; + cursor: pointer; + padding: 2px 6px; + border-radius: 6px; +} +.result-toggle:hover { + color: var(--text-1); + background: var(--bg-soft); +} + +/* ── P2 框架:Tab 栏(文件 / 产物 + 打开文件 Tab) ── */ +.result-tabs { + display: flex; + align-items: center; + gap: var(--sp-1); + overflow-x: auto; + scrollbar-width: none; +} +.result-tabs::-webkit-scrollbar { display: none; } +.result-tab { + background: none; + border: none; + padding: var(--sp-1) var(--sp-3); + font-size: var(--fs-secondary); + color: var(--text-3); + cursor: pointer; + border-radius: 6px; + white-space: nowrap; +} +.result-tab:hover { + color: var(--text-1); + background: var(--bg-soft); +} +.result-tab.active { + color: var(--text-1); + background: var(--bg-soft); + font-weight: 600; +} +/* 打开文件 Tab 条(P3 查看器接入;rant 17:28 并入 .result-tabs 同排,每文件一个可关闭 Tab) */ +.result-tabbar { + display: none; + flex-direction: row; + align-items: center; + gap: var(--sp-1); + overflow-x: auto; + scrollbar-width: none; +} +.result-tabbar.has-tabs { display: inline-flex; } +.result-tabbar::-webkit-scrollbar { display: none; } +.result-filetab { + display: inline-flex; + align-items: center; + gap: var(--sp-1); + padding: 2px var(--sp-2); + font-size: var(--fs-small); + color: var(--text-2); + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + white-space: nowrap; + max-width: 160px; + overflow: hidden; + height: 22px; /* 与静态 tab 同高(同排并列) */ +} +.result-filetab.active { + color: var(--text-1); + border-color: var(--accent); +} +.filetab-close { + color: var(--text-3); + font-size: 12px; + line-height: 1; + padding: 0 2px; + border-radius: 4px; +} +.filetab-close:hover { + color: var(--text-1); + background: var(--bg-hover); +} + +/* ── P2 框架:内容 pane 切换(激活 pane 显示) ── */ +.result-pane { display: none; } +.result-pane.active { display: flex; flex-direction: column; } + +/* ── 宽度拖拽手柄 ── */ +#result-resizer { + position: absolute; + top: 0; + bottom: 0; + width: 6px; + right: 277px; /* JS 随宽度更新(right = panelWidth - 3) */ + cursor: col-resize; + z-index: 40; +} +#result-resizer:hover { + background: var(--accent); + opacity: 0.35; +} +#result-resizer.dragging { + background: var(--accent); + opacity: 0.5; +} + +/* ── P3.1:文件浏览器(懒加载目录树,VS Code 风格 rant 2026-08-12T17:28:19) ── */ +.result-files { + /* flex:1 + min-height:0(rant 2026-08-13T12:46:12):flex 子项默认 flex:0 1 auto 高度随内容 + 增长永不压缩进容器 → overflow-y:auto 永不触发(有滚动条槽但滚不动);与 .result-list 对齐 */ + flex: 1; + min-height: 0; + overflow-y: auto; + /* 滚动条 hover 显示(VS Code 行为:平时隐藏,移入文件树区域显示) */ + scrollbar-width: thin; + scrollbar-gutter: stable; +} +.result-files::-webkit-scrollbar { width: 8px; } +.result-files::-webkit-scrollbar-thumb { background: transparent; border-radius: 4px; } +.result-files:hover::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); } +.result-files::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); } + +.ft-row { + display: block; /* 块级排布:.ft-head 行 + .ft-kids 子树纵向排列,行高自适应 + (修复布局 bug:定高 flex-wrap 使下一兄弟行与展开目录首个子项 + 重叠,headless Chrome 像素实证——兄弟行遮挡子项) */ + flex-shrink: 0; /* rant 20:58:57:.ft-row 是 .result-files(flex column)的 flex + item,默认 shrink:1 + overflow:hidden → min-height:auto 解析为 0, + flex 把整棵树压缩到容器高度,scrollHeight==clientHeight 无法滚动 */ + cursor: pointer; + font-size: var(--fs-small); + color: var(--text-2); + white-space: nowrap; + overflow: hidden; +} +.ft-head { + display: flex; + align-items: center; + height: 24px; /* VS Code 紧凑行高(默认 22px,取 24px 适配 14px 图标) */ + border-radius: 4px; +} +.ft-row:hover > .ft-head { + background: var(--bg-soft); + color: var(--text-1); +} +/* 选中态:背景 + 左侧 2px accent 竖条(VS Code active 行) */ +.ft-row.active > .ft-head { + background: var(--bg-soft); + color: var(--text-1); + box-shadow: inset 2px 0 0 var(--accent); +} +.ft-dir { + color: var(--text-1); + font-weight: 500; +} +.ft-icon { + flex: none; + width: 14px; + height: 14px; + margin-right: 4px; + color: currentColor; /* mono:图标颜色跟随文字色 */ + fill: currentColor; +} +.ft-icon path:not([fill]) { fill: currentColor; } +/* 展开/收起 chevron(VS Code 风格 ▸/▾,rant 2026-08-13T20:58:57) */ +.ft-chevron { + flex: none; + width: 14px; + height: 14px; + margin-right: 2px; + color: currentColor; + fill: currentColor; +} +.ft-chevron path:not([fill]) { fill: currentColor; } +.ft-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; /* 超长名省略号 */ + white-space: nowrap; +} +.ft-kids { + position: relative; /* 正常流布局:位于 .ft-head 之下(不再作为 flex 行溢出) */ +} +.ft-kids.hidden { + display: none; +} +/* 缩进引导线:嵌套结构天然对齐父行图标列(left:2px ≈ 父行图标起点) */ +.ft-kids::before { + content: ""; + position: absolute; + left: 2px; + top: 0; + bottom: 0; + border-left: 1px solid var(--border); +} +.ft-hint { + padding: var(--sp-2) var(--sp-3); + color: var(--text-3); + font-size: var(--fs-small); +} + +/* ── P3.3:文件查看器(基础版) ── */ +.result-viewer { + overflow: hidden; +} +.viewer-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sp-2); + padding: var(--sp-2) var(--sp-3); + border-bottom: 1px solid var(--border); + flex: none; +} +.viewer-path { + font-size: var(--fs-small); + color: var(--text-2); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.viewer-open { + flex: none; + background: none; + border: 1px solid var(--border); + color: var(--text-2); + font-size: var(--fs-small); + padding: 2px var(--sp-2); + border-radius: 6px; + cursor: pointer; +} +.viewer-open:hover { + color: var(--text-1); + background: var(--bg-soft); +} +.viewer-pre { + flex: 1; + overflow: auto; + margin: 0; + padding: var(--sp-3); + font-size: var(--fs-small); + line-height: 1.5; + white-space: pre; + color: var(--text-1); +} +/* P3.3:查看器图片直显 + md 渲染 */ +.viewer-img { + flex: 1; + object-fit: contain; + max-width: 100%; + padding: var(--sp-3); + box-sizing: border-box; +} +.viewer-md { + flex: 1; + overflow: auto; + padding: var(--sp-3); + font-size: var(--fs-secondary); + line-height: 1.6; + color: var(--text-1); +} +.viewer-md h1, .viewer-md h2, .viewer-md h3 { margin: var(--sp-2) 0; } +.viewer-md code { + background: var(--bg-soft); + border-radius: 4px; + padding: 1px 4px; +} +.viewer-md pre code { + display: block; + padding: var(--sp-3); + overflow-x: auto; +} +/* P3.4:HTML 预览占位(混合模型——WebContentsView 叠加其上;折叠/隐藏时占位可见) */ +.viewer-html { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: var(--sp-3); + box-sizing: border-box; +} +.viewer-html .result-empty { + max-width: 100%; +} + +/* ── P3.2:产物文件行(write/edit 成功文件,点击打开查看器 Tab) ── */ +.artifact-row { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-2) var(--sp-3); + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + cursor: pointer; + overflow: hidden; +} +.artifact-row:hover { + border-color: var(--accent); + background: var(--bg-soft); +} +.artifact-name { + font-size: var(--fs-secondary); + font-weight: 600; + color: var(--text-1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: none; + max-width: 55%; +} +.artifact-rel { + font-size: var(--fs-small); + color: var(--text-3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.result-list { + flex: 1; + overflow-y: auto; + padding: var(--sp-3); + gap: var(--sp-2); +} +.result-empty { + color: var(--text-3); + font-size: var(--fs-secondary); + text-align: center; + padding: var(--sp-6) var(--sp-3); + line-height: 1.6; +} + +/* 产物卡片 */ +.result-item { + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg); + overflow: hidden; +} +.result-item.done { border-left: 3px solid var(--accent); } +.result-item.failed { border-left: 3px solid var(--red); } +.result-head { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-2) var(--sp-3); + cursor: pointer; + user-select: none; +} +.result-tool { + font-size: var(--fs-secondary); + font-weight: 600; + color: var(--text-1); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.result-elapsed { + font-size: 11px; + color: var(--text-3); +} +.result-chevron { + color: var(--text-3); + font-size: 10px; + transition: transform 0.15s; +} +.result-item.expanded .result-chevron { + transform: rotate(180deg); +} +.result-body { + padding: var(--sp-2) var(--sp-3) var(--sp-3); + font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-size: 11px; + color: var(--text-2); + border-top: 1px solid var(--border); + white-space: pre-wrap; + word-break: break-all; + max-height: 180px; + overflow-y: auto; + position: relative; +} +.result-copy { + position: sticky; + bottom: 4px; + float: right; + margin: 4px 0 0 8px; + font-size: 11px; + padding: 2px 8px; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--bg-panel); + color: var(--text-2); + cursor: pointer; +} +.result-copy:hover { + color: var(--accent); + border-color: var(--accent); +} +.result-file { + padding: var(--sp-2) var(--sp-3); + border-top: 1px solid var(--border); + font-size: var(--fs-secondary); + cursor: pointer; + color: var(--blue); + display: flex; + align-items: center; + gap: var(--sp-2); +} +.result-file::before { + content: "📄"; + font-size: 12px; +} +.result-file:hover { + background: var(--accent-soft); + color: var(--accent); +} +.result-filename { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 窄屏(<900px)自动隐藏 */ +@media (max-width: 900px) { + #result-panel { + width: 0; + min-width: 0; + border-left: none; + } + #result-panel .result-header, + #result-panel .result-list { + display: none; + } +} diff --git a/emrg/gui/renderer/css/tokens.css b/emrg/gui/renderer/css/tokens.css new file mode 100644 index 00000000..6033d3ef --- /dev/null +++ b/emrg/gui/renderer/css/tokens.css @@ -0,0 +1,139 @@ +/* tokens.css — 设计 token:双主题色板 / 字体 / 圆角 / 阴影 / 间距 / 动效 + * 设计来源:docs/design/gui-redesign.md(rant 2026-08-05T21:59:47 完整复述) + * 双主题:浅色 + 深色。默认 prefers-color-scheme 跟随系统, + * 手动覆盖:(设置里三选)。 + */ +:root { + /* 字体 */ + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; + --font-mono: "SF Mono", "Cascadia Code", Consolas, "JetBrains Mono", "Courier New", monospace; + --fs-body: 15px; + --fs-secondary: 13px; + --fs-aux: 12px; + --fs-title: 17px; + --fs-title-lg: 20px; + --lh-body: 1.7; + + /* 圆角 */ + --radius-bubble: 18px; + --radius-input: 12px; + --radius-btn: 12px; + --radius-card: 14px; + --radius-dialog: 20px; + + /* 间距(8px 基准网格) */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 24px; + --sp-6: 32px; + + /* 动效 */ + --dur-fast: 150ms; + --dur-med: 250ms; + --dur-slow: 350ms; + --ease: cubic-bezier(0.2, 0.8, 0.3, 1); + + /* 浅色主题(默认) */ + --bg: #fafafa; + --bg-panel: #ffffff; + --bg-soft: #f2f3f5; + --text-1: #1a1d21; + --text-2: #5f6672; + --text-3: #8a919d; /* WCAG 对比度 3.04:1(辅助文本 ≥3:1),原 #9aa1ad 2.49:1 不达标 */ + --border: #e8eaed; + --accent: #16a06c; /* 生机绿 */ + --accent-strong: #0e8a5c; + --accent-soft: #e6f5ee; + --blue: #3b82f6; + --red: #e5484d; + --red-soft: #fdecec; + --amber: #f5a623; + --amber-soft: #fdf3e0; + + /* 阴影(浅色三档柔和) */ + --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06); + --shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08); + --shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.14); + + /* 滚动条 */ + --scrollbar-thumb: #d3d6db; + --scrollbar-hover: #b9bdc4; +} + +/* 深色主题(跟随系统) */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #17181c; + --bg-panel: #1f2127; + --bg-soft: #26282f; + --text-1: #eceef2; + --text-2: #a4a9b4; + --text-3: #6b7078; + --border: #2e3138; + --accent: #2fbf83; + --accent-strong: #3fd493; + --accent-soft: #1d3a2f; + --blue: #5b9cf6; + --red: #ef5d62; + --red-soft: #3a2326; + --amber: #f0a940; + --amber-soft: #3a3120; + /* 深色阴影弱化,靠底色分层 */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.45); + --scrollbar-thumb: #3a3d45; + --scrollbar-hover: #4a4e58; + } +} + +/* 手动强制深色 */ +:root[data-theme="dark"] { + --bg: #17181c; + --bg-panel: #1f2127; + --bg-soft: #26282f; + --text-1: #eceef2; + --text-2: #a4a9b4; + --text-3: #6b7078; + --border: #2e3138; + --accent: #2fbf83; + --accent-strong: #3fd493; + --accent-soft: #1d3a2f; + --blue: #5b9cf6; + --red: #ef5d62; + --red-soft: #3a2326; + --amber: #f0a940; + --amber-soft: #3a3120; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.45); + --scrollbar-thumb: #3a3d45; + --scrollbar-hover: #4a4e58; +} + +/* 手动强制浅色 */ +:root[data-theme="light"] { + --bg: #fafafa; + --bg-panel: #ffffff; + --bg-soft: #f2f3f5; + --text-1: #1a1d21; + --text-2: #5f6672; + --text-3: #8a919d; /* WCAG 对比度 3.04:1(辅助文本 ≥3:1),原 #9aa1ad 2.49:1 不达标 */ + --border: #e8eaed; + --accent: #16a06c; + --accent-strong: #0e8a5c; + --accent-soft: #e6f5ee; + --blue: #3b82f6; + --red: #e5484d; + --red-soft: #fdecec; + --amber: #f5a623; + --amber-soft: #fdf3e0; + --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06); + --shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08); + --shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.14); + --scrollbar-thumb: #d3d6db; + --scrollbar-hover: #b9bdc4; +} diff --git a/emrg/gui/renderer/src/components/Shell.tsx b/emrg/gui/renderer/src/components/Shell.tsx index aea3b236..0b69be39 100644 --- a/emrg/gui/renderer/src/components/Shell.tsx +++ b/emrg/gui/renderer/src/components/Shell.tsx @@ -1,7 +1,8 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useI18n } from "../lib/i18n"; import { useSnapshotStore } from "../hooks/useSnapshotStore"; import { useDaemonBridge } from "./DaemonBridgeProvider"; +import { createProdMarkdownRenderer } from "../lib/vendorMarkdown"; import { Sidebar, type SidebarViewId } from "./Sidebar"; import { ResultPanel } from "./ResultPanel"; import { WorkspaceView, type WorkspaceViewId } from "./WorkspaceView"; @@ -72,6 +73,9 @@ export function Shell() { } const isPanelView = activeView !== "sessions"; + // 生产 markdown 渲染器(真实 vendor marked/DOMPurify/hljs,Batch 5 承诺项)—— + // 一次构造复用;缺省降级在 TranscriptView 内部仍兜底 + const mdRenderer = useMemo(() => createProdMarkdownRenderer(), []); /** Composer 的 /指令路由(vanilla handleCommand 的 React 版;无对话框的指令进 runDirect) */ function handleCommand(routing: CommandRouting): void { @@ -156,7 +160,7 @@ export function Shell() { {t("app.sessionDisconnected")} ) : null} - + )} diff --git a/emrg/gui/renderer/src/lib/vendorMarkdown.test.ts b/emrg/gui/renderer/src/lib/vendorMarkdown.test.ts new file mode 100644 index 00000000..52cea9a7 --- /dev/null +++ b/emrg/gui/renderer/src/lib/vendorMarkdown.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { createProdMarkdownRenderer } from "./vendorMarkdown"; + +/** + * vendorMarkdown.test.ts — 生产 markdown 接线测试(Batch 5 承诺项回归)。 + * 断言 createProdMarkdownRenderer() 返回的渲染器使用**真实** vendor 依赖 + * (marked/DOMPurify/hljs),而非降级转义: + * - 粗体/行内代码/链接 → 真实 HTML 标签(降级路径只会返回转义文本) + * - 代码块 → hljs 语言类 + 高亮 token 类 + * - window.hljs 由 highlight.custom.js 副作用导入写入(ResultPanel/PlainCode 依赖) + */ +describe("vendorMarkdown", () => { + it("production renderer renders real markdown, not escaped fallback", async () => { + const md = createProdMarkdownRenderer(); + const html = await md.renderMarkdown("**bold** and `code` and [link](https://x.dev)"); + expect(html).toContain("bold"); + expect(html).toContain("code"); + expect(html).toContain('link'); + }); + + it("code block gets hljs language + highlight token classes", async () => { + const md = createProdMarkdownRenderer(); + const html = await md.renderMarkdown("```python\nprint('hi')\n```"); + expect(html).toContain("language-python"); + expect(html).toContain("hljs"); + // hljs 真实高亮输出带 class 的 token span(降级纯文本路径不会产生) + expect(html).toMatch(//); + }); + + it("hljs global is available (ResultPanel/PlainCode window.hljs contract)", () => { + const win = window as unknown as { hljs?: unknown }; + expect(win.hljs).toBeDefined(); + expect(typeof (win.hljs as { highlight?: unknown }).highlight).toBe("function"); + }); + + it("xss is sanitized by DOMPurify", async () => { + const md = createProdMarkdownRenderer(); + // CommonMark: HTML 块延续到空行 —— script 块后须空行才能解析 markdown + const html = await md.renderMarkdown("\n\n**ok**"); + expect(html).not.toContain("ok"); + }); +}); diff --git a/emrg/gui/renderer/src/lib/vendorMarkdown.ts b/emrg/gui/renderer/src/lib/vendorMarkdown.ts new file mode 100644 index 00000000..ceaf671c --- /dev/null +++ b/emrg/gui/renderer/src/lib/vendorMarkdown.ts @@ -0,0 +1,34 @@ +/** + * vendorMarkdown.ts — 生产环境 vendor 依赖接线(Batch 5 承诺项: + * ResultPanel 注释 "Batch 5 接真实 hljs"、markdown.ts "构造注入")。 + * + * React 迁移设计把 vendor 依赖(marked/DOMPurify/hljs)定为**构造注入** + * 而非 vanilla 的 window 全局读取。但 Batch 5 final switch 时没人注入 + * 真实依赖:TranscriptView 走 `renderer ?? createMarkdownRenderer({ t })` + * 降级为纯转义文本,聊天 markdown / 代码高亮全部失效(vendor/ 目录仍在 + * 打包清单里,却没有任何代码加载它们)。 + * + * 本模块把 vendor 三件套接进 bundle: + * - marked.min.js / dompurify.min.js:UMD → default import(vite 内联打包) + * - highlight.custom.js:IIFE 副作用导入,运行时设 `window.hljs` + * (构建期在 Node 中执行时 typeof window === 'undefined' → no-op 安全) + * - highlight.github-dark.css:代码块主题,随 bundle 输出 + * + * 接线后 createProdMarkdownRenderer() 交给 Shell → TranscriptView + * (renderer prop),ResultPanel/PlainCode 的 window.hljs 读取也同步恢复。 + */ +import marked from "../../../vendor/marked.min.js"; +import DOMPurify from "../../../vendor/dompurify.min.js"; +import "../../../vendor/highlight.custom.js"; +import "../../../vendor/highlight.github-dark.css"; +import { createMarkdownRenderer, type MarkdownRenderer } from "./markdown"; + +/** 生产环境 markdown 渲染器(真实 marked + DOMPurify + hljs;缺省 t 走 createMarkdownRenderer 的 t 注入) */ +export function createProdMarkdownRenderer(): MarkdownRenderer { + // hljs 由 highlight.custom.js 副作用导入在运行时写入 window(与 vanilla 同语义, + // ResultPanel/PlainCode 直接读 window.hljs);此处惰性读取避免模块求值顺序问题。 + const hljs = (typeof window !== "undefined" + ? (window as unknown as { hljs?: { highlight: (code: string, opts: { language: string; ignoreIllegals: boolean }) => { value: string }; getLanguage: (lang: string) => boolean; highlightAuto: (code: string) => { value: string } } }).hljs + : undefined); + return createMarkdownRenderer({ marked, DOMPurify, hljs }); +} diff --git a/emrg/gui/renderer/src/main.tsx b/emrg/gui/renderer/src/main.tsx index 09676b13..7a0faf31 100644 --- a/emrg/gui/renderer/src/main.tsx +++ b/emrg/gui/renderer/src/main.tsx @@ -1,6 +1,15 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { App } from "./App"; +// 样式:迁移期 shell.css 只含骨架;Batch 5 切换后恢复 vanilla css/(设计承诺 +// "复用 vanilla CSS,无需改样式表",#1024 删除后未接入 build —— 组件 className +// 与 classes 完全对齐,tokens.css 提供组件内联 var(--*) 变量)。 +// 顺序:tokens(变量) → base(基础) → components → layout → animations → shell(React 覆盖) +import "../css/tokens.css"; +import "../css/base.css"; +import "../css/components.css"; +import "../css/layout.css"; +import "../css/animations.css"; import "./shell.css"; const rootEl = document.getElementById("root"); diff --git a/emrg/gui/renderer/src/vendor.d.ts b/emrg/gui/renderer/src/vendor.d.ts new file mode 100644 index 00000000..fddaa797 --- /dev/null +++ b/emrg/gui/renderer/src/vendor.d.ts @@ -0,0 +1,23 @@ +/** + * vendor.d.ts — vendor/ 目录下 UMD 文件的模块声明(TS 无声明文件)。 + * marked/dompurify 以 default import 进入 bundle;highlight.custom.js 仅副作用 + * 导入(运行时写 window.hljs),无需导出声明。 + */ +declare module "*/vendor/marked.min.js" { + interface MarkedLike { + parse(text: string, opts?: Record): string | Promise; + lexer(text: string): Array>; + parser(tokens: Array>): string; + use(opts: { renderer?: { code?: (code: string, infostring: string, escaped: boolean) => string } }): unknown; + } + const marked: MarkedLike; + export default marked; +} + +declare module "*/vendor/dompurify.min.js" { + interface DomPurifyLike { + sanitize(html: string): string; + } + const DOMPurify: DomPurifyLike; + export default DOMPurify; +}