From 833dded0967e924e14e1d8bf6a83bfc2ab555e3a Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 27 Aug 2026 04:02:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?emrg:=20gui=20renderer=20Batch=205=20cleanu?= =?UTF-8?q?p=20=E2=80=94=20delete=20vanilla=20renderer=20+=20legacy=20vm.r?= =?UTF-8?q?unInContext=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 7 +- Agent.md | 6 +- DEVELOPMENT.md | 2 +- emrg/gui/renderer/css/animations.css | 91 - emrg/gui/renderer/css/base.css | 108 - emrg/gui/renderer/css/components.css | 1437 ---------- emrg/gui/renderer/css/layout.css | 943 ------ emrg/gui/renderer/css/tokens.css | 139 - emrg/gui/renderer/index.html | 521 ---- emrg/gui/renderer/js/app.js | 1870 ------------ emrg/gui/renderer/js/chat.js | 471 --- emrg/gui/renderer/js/commands.js | 68 - emrg/gui/renderer/js/copywriting.js | 63 - emrg/gui/renderer/js/dialogs.js | 1689 ----------- emrg/gui/renderer/js/error-boundary.js | 209 -- emrg/gui/renderer/js/file-tree.js | 233 -- emrg/gui/renderer/js/i18n.js | 934 ------ emrg/gui/renderer/js/markdown.js | 206 -- emrg/gui/renderer/js/result-panel.js | 522 ---- emrg/gui/renderer/js/sidebar.js | 62 - emrg/gui/renderer/js/utils.js | 108 - emrg/gui/test/app-commands.test.js | 389 --- emrg/gui/test/build-config.test.js | 31 - emrg/gui/test/commands.test.js | 83 - emrg/gui/test/i18n.test.js | 284 -- emrg/gui/test/renderer.smoke.test.js | 3645 ------------------------ emrg/gui/test/tool-group.test.js | 236 -- tests/test_upgrade.py | 4 - 28 files changed, 6 insertions(+), 14355 deletions(-) delete mode 100644 emrg/gui/renderer/css/animations.css delete mode 100644 emrg/gui/renderer/css/base.css delete mode 100644 emrg/gui/renderer/css/components.css delete mode 100644 emrg/gui/renderer/css/layout.css delete mode 100644 emrg/gui/renderer/css/tokens.css delete mode 100644 emrg/gui/renderer/index.html delete mode 100644 emrg/gui/renderer/js/app.js delete mode 100644 emrg/gui/renderer/js/chat.js delete mode 100644 emrg/gui/renderer/js/commands.js delete mode 100644 emrg/gui/renderer/js/copywriting.js delete mode 100644 emrg/gui/renderer/js/dialogs.js delete mode 100644 emrg/gui/renderer/js/error-boundary.js delete mode 100644 emrg/gui/renderer/js/file-tree.js delete mode 100644 emrg/gui/renderer/js/i18n.js delete mode 100644 emrg/gui/renderer/js/markdown.js delete mode 100644 emrg/gui/renderer/js/result-panel.js delete mode 100644 emrg/gui/renderer/js/sidebar.js delete mode 100644 emrg/gui/renderer/js/utils.js delete mode 100644 emrg/gui/test/app-commands.test.js delete mode 100644 emrg/gui/test/commands.test.js delete mode 100644 emrg/gui/test/i18n.test.js delete mode 100644 emrg/gui/test/renderer.smoke.test.js delete mode 100644 emrg/gui/test/tool-group.test.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88034bf7..6739a968 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,11 +44,8 @@ jobs: working-directory: emrg/gui run: | node --check main.js && node --check preload.js && node --check daemon_client.js - # renderer JS — 兼容旧扁平结构(renderer/*.js)与新 js/ 子目录结构(renderer/js/*.js,#417) - for f in renderer/app.js renderer/markdown.js renderer/js/*.js; do - [ -f "$f" ] || continue - node --check "$f" || exit 1 - done + # renderer JS — vanilla files deleted in Batch 5 cleanup (#1024); React + # source is typechecked by tsc in the "GUI renderer React suite" step above # v0.2.29 教训:Windows pytest 此前只在 tag 触发的 Build Release 跑 # (test.yml 仅 ubuntu)——FakeGitRun git.EXE 大小写 bug(#723)等 Windows-only diff --git a/Agent.md b/Agent.md index 7d0b381f..5977ed16 100644 --- a/Agent.md +++ b/Agent.md @@ -85,8 +85,8 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request - Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand) - 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` (265: 45 daemon_client + 20 conn-manager + 22 app-commands + 132 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group + 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; release pipeline Vite build step + dist whitelist landed in #1022; gui `npm start` pre-builds dist via `prestart` (fresh-clone dev flow) + - 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 91 cover the migrated logic); gui `npm start` pre-builds dist via `prestart` (fresh-clone dev flow) - **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/`) @@ -120,7 +120,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` (265: 45 daemon_client + 20 conn-manager + 22 app-commands + 132 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group + 3 preload-api) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` +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/` 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 路径不受影响) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c8427e67..4ecd5bd0 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -159,7 +159,7 @@ uv run python -m emrg --help cd emrg/gui npm ci # install deps (production: --omit=dev) npm start # launch GUI (auto-starts daemon) -npm test # run Node tests (178: 43 daemon_client + 19 conn-manager + 22 app-commands + 59 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config + 7 gui-state; integration runs in CI, local: npm run test:integration) +npm test # run Node tests (89: 45 daemon_client + 20 conn-manager + 8 integration + 6 build-config + 7 gui-state + 3 preload-api; integration runs in CI, local: npm run test:integration) ``` ### Packaging (installer builds) diff --git a/emrg/gui/renderer/css/animations.css b/emrg/gui/renderer/css/animations.css deleted file mode 100644 index 07e9fd81..00000000 --- a/emrg/gui/renderer/css/animations.css +++ /dev/null @@ -1,91 +0,0 @@ -/* 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 deleted file mode 100644 index d9c8158d..00000000 --- a/emrg/gui/renderer/css/base.css +++ /dev/null @@ -1,108 +0,0 @@ -/* 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 deleted file mode 100644 index bd52897e..00000000 --- a/emrg/gui/renderer/css/components.css +++ /dev/null @@ -1,1437 +0,0 @@ -/* 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 deleted file mode 100644 index 044e69e7..00000000 --- a/emrg/gui/renderer/css/layout.css +++ /dev/null @@ -1,943 +0,0 @@ -/* 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 deleted file mode 100644 index 6033d3ef..00000000 --- a/emrg/gui/renderer/css/tokens.css +++ /dev/null @@ -1,139 +0,0 @@ -/* 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/index.html b/emrg/gui/renderer/index.html deleted file mode 100644 index 594337b7..00000000 --- a/emrg/gui/renderer/index.html +++ /dev/null @@ -1,521 +0,0 @@ - - - - - - EMRG - - - - - - - - - -
- - - - -
- - - - - - - - - - -
-
-
- -

项目管理

-
-
- -
-
-
-
-
- -

任务管理

-
管理定时任务与自定义任务类型(内置类型只读)
-
-
- - -
- - - - - - -
-
-
-
- -

Rant 管理

-
- - - - -
-
- 时间 - 项目 - 状态 - 进度 - 内容 -
-
-
- -
- - -
-
-
- -

设置

-
- - - - - -
-
-
- - -
可用模型
-
- - - -
点左侧圆点可设为默认模型,切换后下一条消息即生效
-
-
- - - - -
- - -
-
-
- - - - - - - -
- -
- 加载中… - -
-
- - - -
-
- - - -
-
EMRG 可能会犯错,请核对重要信息
-
-
- - - - -
-
- - - -
-
-
-

欢迎使用 EMRG

-

我是你的 AI 工作伙伴。先做两步简单设置,马上就能开始。

-
- - - -
- -
-
-
- - - -
-

确认

-

-
- - -
-
-
- - - -
-

重命名对话

- -
- - -
-
-
- - - - - - - - - - - - -
-

/ 指令帮助

-

以 / 开头的指令在输入框执行,或点击补全菜单选择。

-
-
- -
-
-
- - - - -
-

打开会话

-

选择项目后选择要打开的会话(跨项目多开)。

-
-
- - - -
-
-
- - - -
-

新建会话

-

选择项目新建会话(跨项目多开)。

-
-
- - -
-
-
- - - -
-

回退到历史消息点

-

选择要保留到的消息点,之后的对话将被移除。

-
-
- -
-
-
- - - -
-

记忆

-

EMRG 记住的长期信息,点击查看详情。

-
- -
- -
-
-
- - - -
-

技能

-

EMRG 已加载的技能。

-
-
- -
-
-
- - - -
-

连接 GitHub

-

在浏览器中打开 GitHub 并输入下方一次性代码以完成授权。

-
-
- - -
-
等待浏览器中确认…
-
-
- - - - - - - - - - - - - - - - - - - - - - diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js deleted file mode 100644 index c5197f49..00000000 --- a/emrg/gui/renderer/js/app.js +++ /dev/null @@ -1,1870 +0,0 @@ -"use strict"; -/** - * app.js — 应用主控:启动、事件处理(main 已分类)、模型切换器、右键菜单、主题。 - * 事件契约与 main 不变:message_delta/done/tool_started/tool_finished/cancelled/ - * error/pong/status/sessions/disconnected/group_cleared/list_result/command_result。 - */ - -const App = (() => { - // 模型切换菜单键盘导航 handler(打开时注册,关闭时移除) - let _modelMenuKeyHandler = null; - // 右键菜单键盘导航 handler(同上生命周期管理) - let _ctxMenuKeyHandler = null; - - const state = { - sessionId: null, - sessions: [], - // P3 slice 1(rant 15:07:19):会话级状态表——每会话独立 busy/ownStreamRequestId/ - // mode/autoScroll。busy/ownStreamRequestId/mode 为**激活会话条目**的实时视图 - // (defineProperty getter/setter → sidState(sessionId)),既有调用点零改动; - // 事件按 sid 路由时操作对应条目(后台会话的 done 不误清激活会话的 busy)。 - sessionsBySid: new Map(), // sid → { busy, ownStreamRequestId, mode, autoScroll } - // P2 queue-injection(#655):busy 时发送的消息入 daemon 队列(task_queued), - // 此处按会话记录以便 queued_requeue 以原 requestId 重发(不重加用户行)。 - queuedSends: new Map(), // sid → [{ requestId, text, sandbox }] - // P4 slice 2(rant 15:07:19):跨项目打开的会话(侧边栏数据源,main 广播) - openSessions: [], // [{ sid, projectName, projectPath, lastActive }],lastActive 倒序 - apiKeyConfigured: false, - configExists: false, - model: "", - serverId: "", - version: "", // WorkBuddy P3:init 返回的 package.json 版本号(/version 不再硬编码) - evolutionCount: null, - // WorkBuddy P3:上次已知进化计数(用于检测"进化刚发生"→toast) - lastKnownEvolutionCount: null, - autoScroll: true, - // GUI / 指令补全菜单(rant 19:44 P1):items=[{cmd,hint,phase}] index=当前高亮 - cmdMenu: { items: [], index: -1 }, - // Rant 2026-08-20T18:18:会话级沙箱档位(read-only / workspace-write / danger-full-access),默认可写工作区 - sandbox: "workspace-write", - // B3(rant 21:59:11):每会话输入框草稿(浏览器 tab 式状态保留)——切换会话保存旧 - // sid 草稿、恢复新 sid 草稿;发送成功即清除该 sid 草稿 - drafts: new Map(), // sid → 草稿文本 - // rant 18:55:09 v0.2:工作区视图导航(null/undefined=会话视图;projects/tasks/rants/settings=面板视图) - activeView: null, - }; - - // P3 slice 1:会话条目访问器(get-or-create)。无 sid/未激活 → 归入当前激活会话。 - function sidState(sid) { - const key = sid || state.sessionId || "default"; - if (!state.sessionsBySid.has(key)) { - state.sessionsBySid.set(key, { busy: false, ownStreamRequestId: null, sandbox: "workspace-write", autoScroll: true, disconnected: false }); - } - return state.sessionsBySid.get(key); - } - - // 激活会话条目 = state.busy/ownStreamRequestId/sandbox 的事实源(P3 过渡期兼容层) - Object.defineProperty(state, "busy", { - get() { return sidState(state.sessionId).busy; }, - set(v) { sidState(state.sessionId).busy = v; }, - }); - Object.defineProperty(state, "ownStreamRequestId", { - get() { return sidState(state.sessionId).ownStreamRequestId; }, - set(v) { sidState(state.sessionId).ownStreamRequestId = v; }, - }); - Object.defineProperty(state, "sandbox", { - get() { return sidState(state.sessionId).sandbox; }, - set(v) { sidState(state.sessionId).sandbox = v; }, - }); - - // ── 启动 ───────────────────────────────── - async function boot() { - try { - const init = await window.emrg.init(); - state.configExists = init.config_exists; - state.apiKeyConfigured = init.api_key_configured; - state.serverId = init.server_id || ""; - state.model = init.model || ""; - state.version = init.version || ""; - state.currentVersion = init.current_version || ""; // rant 18:30:57:进程实际运行版本(升级横幅对比基准;14:38:27 起为 daemon 内存版本) - state.lastKnownVersion = state.currentVersion; - state.evolutionCount = init.evolution_count ?? null; - state.lastKnownEvolutionCount = state.evolutionCount; - updateConnectionDot(init.config_exists && init.api_key_configured ? "green" : "gray"); - updateModelSwitcher(); - updateGrowthCard(); - if (window.EMRG_I18N) window.EMRG_I18N.apply(); // rant 21:19:应用当前语言静态文案 - - if (!init.config_exists) { - Dialogs.showWelcome(); // 首启引导 - return; - } - if (!init.api_key_configured) { - Dialogs.showSettings(); // key 空/占位符 - return; - } - if (init.sessions && init.sessions.length > 0) { - state.sessions = init.sessions; // rant 11:44:52:历史会话列表已移除,不再渲染 conv-list - // P4 slice 2:main 恢复的打开会话 + 激活会话(跳过 switchSession 的 IPC 往返) - state.openSessions = init.open_sessions || []; - Sidebar.renderOpenSessions(state.openSessions); - const restoredSid = init.active_sid; - if (restoredSid && init.sessions.some((s) => s.session_id === restoredSid)) { - state.sessionId = restoredSid; - activateSessionView(restoredSid); - updateEmptyState(); - Sidebar.highlight(restoredSid); - } else { - const current = init.sessions.find((s) => s.session_id === state.sessionId); - if (current) { - Sidebar.highlight(state.sessionId); - } else { - await switchSession(init.sessions[0].session_id, { silent: true }); - } - } - } else { - await newSession(); - } - // 修复:boot 成功路径必须启用输入框(此前仅 done/cancelled/disconnected/error - // 回调会调用 setComposerDisabled(false),形成"需先发消息才能启用输入框"死锁) - setComposerDisabled(false); - } catch (e) { - Chat.addSystemMessage(_t("app.bootFailed", { msg: e.message })); - } - } - - // ── 发送 ───────────────────────────────── - async function sendMessage() { - const input = $("input"); - const text = input.value.trim(); - if (!text) return; - // GUI / 指令(rant 19:44 P1):/ 开头 → 路由到指令 handler,不走 sendMessage - const parsed = Commands.parseInput(text); - if (parsed.type !== "message") { - input.value = ""; - input.style.height = "auto"; - hideCmdMenu(); - await handleCommand(parsed); - return; - } - if (!state.sessionId) { - Chat.addSystemMessage(_t("app.needSession")); - return; - } - // P2 queue-injection(#655):busy 不再拦截——daemon 排队注入(task_queued), - // 回合结束未注入则 queued_requeue 以原 requestId 重发。busy 时记录待重发条目。 - const wasBusy = state.busy; - state.busy = true; - setComposerDisabled(true); - Chat.addUserMessage(text); - input.value = ""; - input.style.height = "auto"; - // B3:消息已发送 → 清除该会话草稿 - state.drafts.delete(state.sessionId); - // G143:send 前预生成 requestId 并标记自有流——消除 IPC 往返竞态窗口 - const requestId = genRequestId(); - state.ownStreamRequestId = requestId; - if (wasBusy) { - const sid = state.sessionId; - if (!state.queuedSends.has(sid)) state.queuedSends.set(sid, []); - state.queuedSends.get(sid).push({ requestId, text, sandbox: state.sandbox }); - } - try { - const res = await window.emrg.sendMessage({ sessionId: state.sessionId, text, requestId, sandbox: state.sandbox }); - state.ownStreamRequestId = res.requestId || requestId; // G124:以 daemon 回显为准 - } catch (e) { - state.busy = false; - state.ownStreamRequestId = null; - setComposerDisabled(false); - // G49:失败恢复输入框,文案不责怪用户 - Chat.addSystemMessage(EMRG_Copy.COPY.sendFailed); - input.value = text; - } - } - - // ── / 指令(rant 19:44 P1-P4)────────────────── - /** 执行 / 指令。全部 4 阶段已实现(phase 4 = 演化类 /rant /trigger)。 */ - async function handleCommand(parsed) { - const cmd = parsed.cmd; - const meta = Commands.COMMANDS[cmd]; - if (!meta) { - Chat.addSystemMessage(_t("app.cmdUnknown", { cmd })); - return; - } - try { - switch (cmd) { - case "/clear": - if (!state.sessionId) { - Chat.addSystemMessage(_t("app.needSession")); - return; - } - await window.emrg.clearSession({ sessionId: state.sessionId }); - Chat.clear(state.sessionId); - Chat.addSystemMessage(_t("app.cleared")); - break; - case "/compact": - if (!state.sessionId) { - Chat.addSystemMessage(_t("app.needSession")); - return; - } - await window.emrg.compactSession({ sessionId: state.sessionId }); - Chat.addSystemMessage(_t("app.compacted")); - break; - case "/version": - showVersionInfo(); - break; - case "/help": - showHelpDialog(); - break; - case "/image": - Chat.addSystemMessage(_t("app.imagePaste")); - break; - case "/sessions": - case "/resume": - // P2:/resume 直接切换;无参数 → 现代两步弹窗(rant 14:10:14 P6:sessions-dialog 移除,侧边栏会话区 + /open 已覆盖) - if (parsed.args.length > 0) { - await switchSession(parsed.args[0]); - } else { - Dialogs.showOpenSessionDialog(); - } - break; - case "/open": - // P5(rant 15:07:19):打开会话对话框(两步:项目 → 会话,跨项目多开) - Dialogs.showOpenSessionDialog(); - break; - case "/rename": - // P2:复用现有重命名对话框(右键菜单同款) - if (!state.sessionId) { - Chat.addSystemMessage(_t("app.needSession")); - return; - } - const cur = state.sessions.find((s) => s.session_id === state.sessionId); - Dialogs.showRename(state.sessionId, cur ? cur.title : ""); - break; - case "/delete": - // P2:复用现有删除确认(右键菜单同款) - if (!state.sessionId) { - Chat.addSystemMessage(_t("app.needSession")); - return; - } - Dialogs.showConfirm(EMRG_Copy.COPY.deleteConfirmTitle, EMRG_Copy.COPY.deleteConfirmBody, { - okText: _t("dlg.delete"), - danger: true, - onOk: () => deleteSession(state.sessionId), - }); - break; - case "/rewind": - // P2:历史消息点选择对话框 - showRewindDialog(); - break; - case "/model": - // P3:触发模型切换器(已有 UI) - document.querySelector(".model-switcher")?.click(); - break; - case "/memory": - // P3:记忆浏览器对话框(/memory [session|project|]) - showMemoryDialog(parsed.args[0] || ""); - break; - case "/skills": - // P3:技能列表对话框 - showSkillsDialog(); - break; - case "/rant": - // P4:/rant 直接跟内容 → 快速提交;无参数 → 打开 Rant 面板 + 新建表单(rant 14:10:14 P6:rant-dialog 移除) - if (parsed.args.length > 0) { - await submitRant(parsed.args.join(" "), ""); - } else { - await openRantsPanel(); - Dialogs.openRantForm(); - } - break; - case "/trigger": - // P4:/trigger 直接触发;无参数 → 打开任务面板(rant 14:10:14 P3:替代 tasks-dialog) - if (parsed.args.length > 0) { - await doTrigger(parsed.args[0]); - } else { - openTasksPanel(); - } - break; - default: - Chat.addSystemMessage(_t("app.cmdUnknown", { cmd })); - } - } catch (e) { - Chat.addSystemMessage(_t("app.cmdFailed", { cmd, msg: e.message })); - } - } - - // /rewind:历史消息点选择对话框(daemon list_history → 选择 → rewind_session) - async function showRewindDialog() { - const list = $("rewind-list"); - const dialog = $("rewind-dialog"); - if (!list || !dialog) return; - if (!state.sessionId) { - Chat.addSystemMessage(_t("app.needSession")); - return; - } - list.innerHTML = `
${_t("dlg.loading")}
`; - dialog.showModal(); - try { - const { messages } = await window.emrg.listHistory({ sessionId: state.sessionId }); - list.innerHTML = ""; - if (!messages || messages.length === 0) { - list.innerHTML = `
${_t("app.noHistory")}
`; - return; - } - // 倒序:最新消息点在最上 - [...messages].reverse().forEach((m) => { - const idx = m.record_index; - const row = el("button", { - class: "help-row", - type: "button", - style: "width:100%;text-align:left;cursor:pointer;background:none;border:none;", - }); - const name = el("span", { class: "help-cmd" }, `#${idx}`); - const hint = el("span", { class: "help-hint" }, (m.preview || m.content || "").slice(0, 60)); - row.appendChild(name); - row.appendChild(hint); - row.addEventListener("click", async () => { - dialog.close(); - await doRewind(idx); - }); - list.appendChild(row); - }); - } catch (e) { - list.innerHTML = `
${_t("app.historyFailed", { msg: escapeHtml(e.message) })}
`; - } - } - - async function doRewind(recordIndex) { - try { - const res = await window.emrg.rewindSession({ sessionId: state.sessionId, recordIndex }); - Chat.clear(state.sessionId); - Chat.addSystemMessage(_t("app.rewound", { index: recordIndex, n: res.removedCount ?? 0 })); - } catch (e) { - Chat.addSystemMessage(_t("app.rewindFailed", { msg: e.message })); - } - } - - // /memory:记忆浏览器对话框(daemon list_memories → 列表;read_memory → 详情) - async function showMemoryDialog(sub) { - const list = $("memory-list"); - const detail = $("memory-detail"); - const dialog = $("memory-dialog"); - if (!list || !dialog) return; - list.innerHTML = `
${_t("dlg.loading")}
`; - if (detail) detail.classList.add("hidden"); - dialog.showModal(); - const scope = String(sub || "").toLowerCase() === "session" ? "session" : "project"; - try { - const memories = await window.emrg.listMemories({ scope, sessionId: state.sessionId }); - list.innerHTML = ""; - if (!memories || memories.length === 0) { - list.innerHTML = `
${_t("app.noMemories", { scope: scope === "session" ? _t("app.sessionMem") : _t("app.projectMem") })}
`; - return; - } - for (const m of memories) { - const row = el("button", { - class: "help-row", - type: "button", - style: "width:100%;text-align:left;cursor:pointer;background:none;border:none;", - }); - const title = m.title || m.id || _t("app.unnamed"); - const name = el("span", { class: "help-cmd" }, String(title).slice(0, 40)); - const hint = el("span", { class: "help-hint" }, (m.summary || m.content || "").slice(0, 50)); - row.appendChild(name); - row.appendChild(hint); - row.addEventListener("click", async () => { - try { - const mem = await window.emrg.readMemory({ memoryId: m.id, scope, sessionId: state.sessionId }); - const body = mem.content || mem.body || ""; - if (detail) { - detail.innerHTML = `
${escapeHtml(String(title).slice(0, 80))}
${escapeHtml(body.slice(0, 2000))}
`; - detail.classList.remove("hidden"); - } else { - Chat.addSystemMessage(body.slice(0, 500)); - } - } catch (err) { - Chat.addSystemMessage(_t("app.readMemFailed", { msg: err.message })); - } - }); - list.appendChild(row); - } - } catch (e) { - list.innerHTML = `
${_t("app.memFailed", { msg: escapeHtml(e.message) })}
`; - } - } - - // /skills:技能列表对话框(main 进程读 ~/.emrg/skills + /.emrg/skills) - async function showSkillsDialog() { - const list = $("skills-list"); - const dialog = $("skills-dialog"); - if (!list || !dialog) return; - list.innerHTML = `
${_t("dlg.loading")}
`; - dialog.showModal(); - try { - const skills = await window.emrg.listSkills(); - list.innerHTML = ""; - if (!skills || skills.length === 0) { - list.innerHTML = `
${_t("app.noSkills")}
`; - return; - } - for (const s of skills) { - const row = el("div", { class: "help-row" }); - const name = el("span", { class: "help-cmd" }, s.name || _t("app.unnamed")); - const hint = el("span", { class: "help-hint" }, `${s.source || ""}${s.description ? " · " + s.description.slice(0, 50) : ""}`); - row.appendChild(name); - row.appendChild(hint); - list.appendChild(row); - } - } catch (e) { - list.innerHTML = `
${_t("app.skillsFailed", { msg: escapeHtml(e.message) })}
`; - } - } - - async function submitRant(message, project) { - const text = String(message || "").trim(); - if (!text) { - Chat.addSystemMessage(_t("app.rantEmpty")); - return; - } - try { - const res = await window.emrg.sendRant({ message: text, project }); - Chat.addSystemMessage(`${_t("app.rantReceived")}${res.count ? _t("app.rantCount", { n: res.count }) : ""}`); - } catch (e) { - Chat.addSystemMessage(_t("app.rantFailed", { msg: e.message })); - } - } - - // /trigger:任务面板(rant 14:10:14 P3:替代 tasks-dialog;点击行立即触发) - async function openTasksPanel() { - if (state.activeView !== "tasks") switchView("tasks"); - try { - await Dialogs.loadTaskMeta(); - await Dialogs.renderTaskList(); - Dialogs.startTaskPoll?.(); // rant 2026-08-22T07:18:35:面板激活 → 5s 状态轮询 - } catch (e) { - Chat.addSystemMessage(_t("app.tasksFailed", { msg: e.message })); - } - } - - // 项目面板(rant 14:10:14 P5:打开即加载项目列表) - async function openProjectsPanel() { - if (state.activeView !== "projects") switchView("projects"); - try { - await Dialogs.renderProjectList(); - } catch (e) { - Chat.addSystemMessage(_t("projects.addFailed", { msg: e.message })); - } - } - - // Rant 面板(rant 14:10:14 P4:打开即加载 rant 列表) - async function openRantsPanel() { - if (state.activeView !== "rants") switchView("rants"); - try { - await Dialogs.renderRantList(); - } catch (e) { - Chat.addSystemMessage(_t("rants.loadFailed", { msg: e.message })); - } - } - - async function doTrigger(name) { - const n = String(name || "").trim(); - if (!n) return; - try { - const res = await window.emrg.triggerTask({ name: n }); - if (res.error) { - Chat.addSystemMessage(_t("app.triggerFailed", { msg: res.error })); - showToast(_t("app.triggerFailed", { msg: res.error }), { type: "error" }); - } else if (res.result === "running") { - Chat.addSystemMessage(_t("app.taskRunning", { n })); - showToast(_t("app.taskRunning", { n }), { type: "info", durationMs: 4000 }); - await Dialogs.refreshTaskListIfOpen(); // rant 2026-08-21T17:41:23:面板开着则立即刷新 - } else { - Chat.addSystemMessage(_t("app.triggered", { n })); - showToast(_t("app.triggered", { n }), { type: "success" }); - await Dialogs.refreshTaskListIfOpen(); // rant 2026-08-21T17:41:23:面板开着则立即刷新 - } - } catch (e) { - Chat.addSystemMessage(_t("app.triggerFailed", { msg: e.message })); - showToast(_t("app.triggerFailed", { msg: e.message }), { type: "error" }); - } - } - - // / 补全菜单:输入以 / 开头 → 显示匹配指令;↑↓ 导航、Enter/点击选择填充 - function showCmdMenu(prefix) { - const items = Commands.getCompletions(prefix); - const menu = $("cmd-menu"); - if (!menu) return; - if (items.length === 0) { - hideCmdMenu(); - return; - } - state.cmdMenu = { items, index: 0 }; - menu.innerHTML = ""; - items.forEach((it, i) => { - const row = el("button", { class: "cmd-menu-item", type: "button", dataset: { cmd: it.cmd } }); - row.innerHTML = `${escapeHtml(it.cmd)}${escapeHtml(it.hint)}`; - row.addEventListener("mousedown", (e) => { - e.preventDefault(); // 防输入框失焦 - selectCmd(it.cmd); - }); - menu.appendChild(row); - }); - menu.hidden = false; - highlightCmdMenu(); - } - - function hideCmdMenu() { - state.cmdMenu = { items: [], index: -1 }; - const menu = $("cmd-menu"); - if (menu) menu.hidden = true; - } - - function highlightCmdMenu() { - const menu = $("cmd-menu"); - if (!menu) return; - [...menu.children].forEach((c, i) => c.classList.toggle("selected", i === state.cmdMenu.index)); - } - - /** 选择补全项:填充输入框 + 关闭菜单(用户可继续回车执行) */ - function selectCmd(cmd) { - const input = $("input"); - input.value = cmd; - input.focus(); - input.style.height = "auto"; - input.style.height = Math.min(input.scrollHeight, 150) + "px"; - hideCmdMenu(); - } - - /** /help 帮助对话框:列出全部指令 + 说明 */ - function showHelpDialog() { - const list = $("help-list"); - if (!list) return; - list.innerHTML = ""; - Object.entries(Commands.COMMANDS).forEach(([cmd, meta]) => { - const row = el("div", { class: "help-row" }); - const name = el("span", { class: "help-cmd" }, cmd); - const hint = el("span", { class: "help-hint" }, Commands.hintText(cmd)); // rant 21:19:hint 经 i18n 解析 - row.appendChild(name); - row.appendChild(hint); - list.appendChild(row); - }); - $("help-dialog").showModal(); - } - - // ── 会话 ───────────────────────────────── - // P3 slice 2:每会话一个 .session-view 容器(浏览器 tab 效果——切换 display 保留状态) - function ensureSessionView(sid) { - if (!sid) return $("workspace"); - // 在 wrapper 内按 dataset.sid 查找既有容器(不依赖 getElementById——测试沙箱对 - // 未知 id 返回新 mock,且 mock 的 .id 不入 attributes;dataset 双端一致) - let view = [...$("workspace").children].find((c) => c.dataset?.sid === sid); - if (!view) { - view = el("div", { class: "session-view", id: "session-view-" + sid, dataset: { sid } }); - // 顶部标题栏(项目/名称(id) 或 项目/id);Chat.clear 保留它,消息节点在其后追加 - view.appendChild(el("div", { class: "session-header" })); - $("workspace").appendChild(view); - } - Chat.registerContainer(sid, view); // 幂等:重复注册覆盖同一元素 - return view; - } - - /** 会话标题栏显示的项目名:openSessions 优先,无项目回退 "home"(rant 2026-08-20T16:03:31) */ - function sessionProjectName(sid) { - const os = state.openSessions.find((s) => s.sid === sid); - if (os && os.projectName) return os.projectName; - // rant 17:48:07:回退用会话自身 cwd 末段(与历史列表一致),再 "home" - const cur = state.sessions.find((s) => s.session_id === sid); - if (cur && cur.cwd) { - const norm = String(cur.cwd).replace(/\\/g, "/").replace(/\/+$/, ""); - const seg = norm.split("/"); - if (seg[seg.length - 1]) return seg[seg.length - 1]; - } - return "home"; - } - - // 会话视图顶部标题栏:统一 project/name|id(rant 2026-08-20T17:48:07) - function renderSessionHeader(sid) { - if (!sid) return; - const view = [...$("workspace").children].find((c) => c.dataset?.sid === sid); - if (!view) return; - let header = view.querySelector(".session-header"); - if (!header) { - header = el("div", { class: "session-header" }); - view.insertBefore(header, view.firstChild); - } - const cur = state.sessions.find((s) => s.session_id === sid) || {}; - const project = sessionProjectName(sid); - const title = cur.title && cur.title !== sid ? cur.title : ""; - const text = title ? `${project}/${title}` : `${project}/${sid}`; // rant 22:04:57:有 name 显示 name,无 name 显示完整 id - header.textContent = text; - header.title = text; // 悬停完整信息 - } - - // rant 18:55:09 v0.2:工作区视图互斥(激活会话视图 → 同步隐藏所有面板视图 + 清导航高亮) - function activateSessionView(sid) { - for (const p of VIEWS) { - const btn = $(`nav-${p}`); - if (btn) btn.classList.toggle("active", false); - const panel = $(`panel-${p}`); - if (panel) panel.classList.remove("active"); - } - // 浏览器 tab 语义:仅**新建**容器滚到底;既有容器保留滚动位置(rant 18:55:09 验收 - // "回会话视图滚动位置保留"——面板往返/会话切换不跳底) - const existed = [...$("workspace").children].some((c) => c.dataset?.sid === sid); - const view = ensureSessionView(sid); - for (const child of $("workspace").children) { - if (child.classList) child.classList.remove("active"); - } - view.classList.add("active"); - renderSessionHeader(sid); // 标题栏随激活/重命名刷新 - if (!existed) Chat.scrollToBottom(sid); - setWorkspaceChrome("sessions"); // 恢复输入区 + 成果面板(含 back-to-bottom 按位置恢复) - state.activeView = "sessions"; - return view; - } - - // B3(rant 21:59:11):会话切换时保留/恢复输入框草稿(浏览器 tab 式状态) - function saveDraft(sid) { - if (!sid) return; - state.drafts.set(sid, $("input").value); - } - function restoreDraft(sid) { - const input = $("input"); - input.value = sid ? (state.drafts.get(sid) || "") : ""; - input.style.height = "auto"; - input.style.height = Math.min(input.scrollHeight, 150) + "px"; - } - - /** 会话的项目根目录(openSessions 优先,无项目回退空——文件树空根;rant 2026-08-20T16:03:31) */ - function projectPathFor(sid) { - const os = state.openSessions.find((s) => s.sid === sid); - return (os && os.projectPath) || ""; - } - - async function switchSession(sid, opts = {}) { // G65:busy 即自有流进行中/发送中(IPC 往返窗口内 ownStreamRequestId 尚未赋值) - window.emrg?.log?.("info", `[switch] enter sid=${sid} opts=${JSON.stringify(opts)} busy=${state.busy} activeView=${state.activeView}`); - if (state.busy) { - window.emrg?.log?.("warn", `[switch] ABORT busy sid=${sid}`); - Chat.addSystemMessage(EMRG_Copy.COPY.sessionBusy); - return; - } - try { - // B3:离开前保存当前会话草稿 - saveDraft(state.sessionId); - window.emrg?.log?.("info", `[switch] invoke IPC sid=${sid} projectPath=${opts.projectPath ?? ""}`); - const res = await window.emrg.switchSession({ sessionId: sid, projectPath: opts.projectPath }); - window.emrg?.log?.("info", `[switch] IPC returned sid=${sid} res=${JSON.stringify(res).slice(0, 200)}`); - state.sessionId = sid; - // B3:恢复目标会话草稿 - restoreDraft(sid); - // P3 slice 1:切换只移动激活指针——每会话条目保留自己的 busy/ownStreamRequestId - // P3 slice 2:激活该会话容器(状态保留,不 Chat.clear——切回继续看到原消息/流式现场) - activateSessionView(sid); - ResultPanel.switchSession(sid); // P2 框架:右栏 Tab/产物状态按 sid 隔离 - FileTree.setSession(sid, projectPathFor(sid)); // P3.1:文件树根跟随会话项目 - updateEmptyState(); - if (res.error === "session_not_found") { - window.emrg?.log?.("warn", `[switch] session_not_found sid=${sid}`); - Chat.addSystemMessage(_t("app.deletedSwitch")); - if (res.next_session) { - await switchSession(res.next_session, { silent: true }); - } else { - await newSession(); - } - return; - } - // G13:v1 不加载历史(G12)→ rant 14:15:12:非 silent 切换加载最近 50 条 + 滚动分页 - if (!opts.silent) { - Chat.addSystemMessage(_t("app.switched"), sid); - await loadHistory(sid); - } - // P3 finalize:切入断线会话 → 提示自动重连中(状态保留,不打断输入——G89) - if (sidState(sid).disconnected) { - Chat.addSystemMessage(_t("app.sessionDisconnected"), sid); - } - // rant 2026-08-22T08:07:47:任务/rant/项目面板点"打开会话"只切 sessionId 不切视图, - // 界面毫无变化。switchSession 成功后若仍在面板视图则切回会话视图(所有入口一致)。 - // 递归/初始化路径用 silent,不会在这里触发重复 switchView。 - if (!opts.silent && state.activeView !== "sessions") { - window.emrg?.log?.("info", `[switch] switching view sessions (was ${state.activeView})`); - switchView("sessions"); - } - updateEmptyState(); - Sidebar.highlight(sid, opts.scopeNav); // rant 22:04:02:点击哪列表只高亮该列表(scopeNav 缺省则两列表) - setComposerDisabled(false); // 防御性:独立调用 switchSession 也确保输入框可用 - window.emrg?.log?.("info", `[switch] DONE sid=${sid} activeView=${state.activeView}`); - } catch (e) { - // P6(rant 15:07:19 上限 20):超限提示本地化(main 抛 too many open sessions) - window.emrg?.log?.("error", `[switch] EXCEPTION sid=${sid} ${e?.message || e}`); - Chat.addSystemMessage(/too many open sessions/i.test(e.message || "") ? _t("app.tooManyOpenSessions") : _t("app.switchFailed", { msg: e.message })); - } - } - - // ── 工作区视图导航(rant 18:55:09 v0.2:面板 → 工作区视图;sessions=会话视图)── - const VIEWS = ["sessions", "projects", "tasks", "rants", "settings"]; - - /** 切换工作区视图:点当前激活项关闭回会话视图,点其他项切换;高亮当前导航(DOM 显隐 .active 互斥,状态保留)。 */ - function switchView(name) { - if (!VIEWS.includes(name)) return; - const isOpen = state.activeView === name; - // rant 2026-08-24:视图切换链路日志(任务管理点击排障)——记录每次导航切换 - window.emrg?.log?.("info", `[view] switchView name=${name} prev=${state.activeView} isOpen=${isOpen}`); - // rant 10:36:39:离开任务视图 → 停倒计时(防泄漏;重开由 renderTaskList 重新启动) - // rant 2026-08-22T07:18:35:同时停 5s 状态轮询(与倒计时同生命周期防泄漏) - if (state.activeView === "tasks" && name !== "tasks") { - Dialogs.stopTaskCountdown?.(); - Dialogs.stopTaskPoll?.(); - } - for (const p of VIEWS) { - const btn = $(`nav-${p}`); - if (btn) btn.classList.toggle("active", false); - } - if (!isOpen && name !== "sessions") { - // 打开面板视图:隐藏全部会话视图 + 全部面板视图(互斥——显式清面板,不依赖 DOM 树),激活目标面板 - state.activeView = name; - window.emrg?.log?.("info", `[view] panel activated name=${name}`); - for (const child of $("workspace").children) { - if (child.classList) child.classList.remove("active"); - } - for (const p of VIEWS) { - const v = $(`panel-${p}`); - if (v) v.classList.remove("active"); - } - const view = $(`panel-${name}`); - if (view) view.classList.add("active"); - const btn = $(`nav-${name}`); - if (btn) btn.classList.add("active"); - setWorkspaceChrome("panel"); // 隐藏输入区 + 成果面板 + 空状态 - } else { - // 点当前激活项(toggle 关闭)/ 点 💬 会话 → 回会话视图 - // rant 10:36:39:从任务面板 toggle 关闭同样要停倒计时(activeView 即将离开 tasks) - // rant 2026-08-22T07:18:35:同时停 5s 轮询 - if (state.activeView === "tasks") { - Dialogs.stopTaskCountdown?.(); - Dialogs.stopTaskPoll?.(); - } - showSessionsView(); - } - } - - /** 回会话视图:激活当前 sid 的会话视图(无会话 → 仅恢复工作区 chrome) */ - function showSessionsView() { - state.activeView = "sessions"; - if (state.sessionId) { - activateSessionView(state.sessionId); - } else { - for (const child of $("workspace").children) { - if (child.classList) child.classList.remove("active"); - } - setWorkspaceChrome("sessions"); - } - updateEmptyState(); - } - - /** 工作区外围 chrome(输入区 + 空状态 + 成果面板 + 拖拽手柄)按视图模式显隐:panel 隐藏,sessions 恢复。 */ - function setWorkspaceChrome(mode) { - const composer = $("composer-wrap"); - const empty = $("empty-state"); - const panel = $("result-panel"); - const resizer = $("result-resizer"); - const btb = $("back-to-bottom"); - if (mode === "panel") { - composer.classList.add("hidden"); - empty.classList.add("hidden"); - if (panel) panel.classList.add("hidden"); - if (resizer) resizer.classList.add("hidden"); - if (btb) btb.classList.add("hidden"); // 回到底部按钮属会话视图,面板视图下隐藏 - // HTML 预览(WebContentsView)随面板隐藏(main 侧比对路径;无预览时 no-op) - try { if (window.emrg && typeof window.emrg.closePreview === "function") window.emrg.closePreview({}); } catch { /* ignore */ } - } else { - composer.classList.remove("hidden"); - if (panel) panel.classList.remove("hidden"); - if (resizer) resizer.classList.remove("hidden"); - updateBackToBottomState(); // 回会话视图:按当前滚动位置恢复按钮 - updateEmptyState(); - } - } - - /** 当前滚动容器:激活会话的 .session-view(#workspace overflow:hidden 自身不滚动) */ - function activeScrollEl() { - const ws = $("workspace"); - if (state.sessionId) { - const v = [...(ws.children || [])].find((c) => c.dataset?.sid === state.sessionId); - if (v) return v; - } - const active = [...(ws.children || [])].find((c) => c.classList && c.classList.contains("active")); - return active || ws; - } - - /** 更新"回到底部"按钮 + autoScroll(scroll 事件不冒泡 → 需 capture 捕获子 .session-view 滚动) */ - function updateBackToBottomState() { - const btn = $("back-to-bottom"); - const el = activeScrollEl(); - const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 40; - state.autoScroll = atBottom; - // 上滑阅读(不在底部)→ 显示"回到底部"悬浮按钮(不打扰) - if (btn) btn.classList.toggle("hidden", atBottom); - } - - /** rant 14:10:14 P2:设置面板 tab 切换(模型服务/工作目录/GitHub/外观/语言/关于)。 */ - const SETTINGS_TABS = ["model", "github", "appearance", "language", "about"]; - function switchSettingsTab(name) { - if (!SETTINGS_TABS.includes(name)) return; - for (const t of SETTINGS_TABS) { - const tabBtn = $(`settings-tab-${t}`); - const body = $(`settings-body-${t}`); - if (tabBtn) tabBtn.classList.toggle("active", t === name); - if (body) body.classList.toggle("hidden", t !== name); - } - } - - // ── 历史按需加载(rant 14:15:12:切会话恢复最近 N 条 + 滚动到顶加载更早)── - const historyPages = new Map(); // sid → { offset, hasMore, loading } - const HISTORY_PAGE = 50; - - function historyPageState(sid) { - if (!historyPages.has(sid)) { - historyPages.set(sid, { offset: 0, hasMore: false, loading: false }); - } - return historyPages.get(sid); - } - - /** 渲染历史消息到该会话容器(只读 user 气泡,不触发工具/交互)。 */ - function renderHistoryMessages(sid, messages) { - for (const m of messages || []) { - Chat.addHistoryMessage(m.preview || m.content || "", sid); - } - } - - /** 加载最近一页历史(offset 从最新往回数)——切会话时调用。 */ - async function loadHistory(sid) { - const st2 = historyPageState(sid); - st2.loading = true; - try { - const res = await window.emrg.listHistory({ sessionId: sid, limit: HISTORY_PAGE, offset: st2.offset }); - renderHistoryMessages(sid, res.messages || []); - st2.offset += (res.messages || []).length; - st2.hasMore = !!res.hasMore; - if (st2.hasMore) { - Chat.setLoadBar(sid, _t("app.historyLoadMore")); - } - } catch (e) { - Chat.addSystemMessage(_t("app.historyFailed", { msg: e.message }), sid); - } finally { - st2.loading = false; - } - } - - /** 滚动到顶 → 加载更早一页(prepend,保持滚动位置)。 */ - async function loadOlderHistory(sid) { - const st2 = historyPageState(sid); - if (!st2.hasMore || st2.loading) return; - st2.loading = true; - const view = Chat.chatContainer(sid); - const prevScrollTop = view.scrollTop; - const prevHeight = view.scrollHeight; - try { - const res = await window.emrg.listHistory({ sessionId: sid, limit: HISTORY_PAGE, offset: st2.offset }); - const msgs = res.messages || []; - // prepend:先清加载条,再逐条插到顶部(addHistoryMessage prepend 会插在加载条之后) - for (const m of msgs) { - Chat.addHistoryMessage(m.preview || m.content || "", sid, { prepend: true }); - } - st2.offset += msgs.length; - st2.hasMore = !!res.hasMore; - if (msgs.length === 0) st2.hasMore = false; - if (st2.hasMore) { - Chat.setLoadBar(sid, _t("app.historyLoadMore")); - } else { - Chat.setLoadBar(sid, _t("app.historyNoMore")); - } - // 保持视觉位置:新内容插到顶部后滚差补偿 - view.scrollTop = prevScrollTop + (view.scrollHeight - prevHeight); - } catch (e) { - Chat.addSystemMessage(_t("app.historyFailed", { msg: e.message }), sid); - } finally { - st2.loading = false; - } - } - - /** 会话视图滚动:到顶且有更早 → 加载(防抖 150ms,绑定在 bindUi 内)。 */ - let historyScrollTimer = null; - - async function newSession(opts = {}) { - if (state.busy) { - Chat.addSystemMessage(EMRG_Copy.COPY.sessionBusy); - return; - } - try { - // B3:离开前保存当前会话草稿;新会话从空草稿开始 - saveDraft(state.sessionId); - const res = await window.emrg.newSession({ projectPath: opts.projectPath }); - state.sessionId = res.session_id; - state.drafts.set(res.session_id, ""); // 新会话无草稿 - activateSessionView(state.sessionId); // P3 slice 2:新会话独立容器(空) - ResultPanel.switchSession(state.sessionId); // P2 框架:新会话右栏状态复位 - FileTree.setSession(state.sessionId, projectPathFor(state.sessionId)); // P3.1:文件树根跟随 - Chat.clear(state.sessionId); // 新会话从空开始(容器可能被复用) - updateEmptyState(); // 欢迎屏即反馈 - await refreshSessions(); - Sidebar.highlight(state.sessionId); - setComposerDisabled(false); // 防御性:独立调用 newSession 也确保输入框可用 - restoreDraft(state.sessionId); // 新会话草稿为空 → 清空输入框 - } catch (e) { - Chat.addSystemMessage(_t("app.newFailed", { msg: e.message })); - } - } - - async function deleteSession(sid) { - if (state.busy && state.sessionId === sid) { - Chat.addSystemMessage(EMRG_Copy.COPY.sessionBusy); - return; - } - try { - await window.emrg.deleteSession({ sessionId: sid }); - // P3 slice 2:删除会话 → 释放其容器(若已打开) - Chat.unregisterContainer(sid); - const view = [...$("workspace").children].find((c) => c.dataset?.sid === sid); - if (view) view.remove(); - if (state.sessionId === sid) { - const remaining = state.sessions.filter((s) => s.session_id !== sid); - if (remaining.length > 0) { - await switchSession(remaining[0].session_id, { silent: true }); - } else { - await newSession(); - } - } - await refreshSessions(); - } catch (e) { - Chat.addSystemMessage(_t("app.deleteFailed", { msg: e.message })); - } - } - - async function refreshSessions() { - try { - const sessions = await window.emrg.listSessions(); - state.sessions = sessions; - Sidebar.renderOpenSessions(state.openSessions); // 打开会话区 title 同步刷新 - renderSessionHeader(state.sessionId); // 激活会话标题栏同步刷新 - } catch { /* 忽略 */ } - } - - // ── P4 slice 2:打开会话(跨项目侧边栏) ─────────── - // 关闭 = 断开连接 + 移出列表,**保留磁盘数据**(与删除区分:关闭留数据/删除删数据) - async function closeOpenSession(sid) { - if (state.busy && state.sessionId === sid) { - Chat.addSystemMessage(EMRG_Copy.COPY.sessionBusy); - return; - } - try { - await window.emrg.closeSession({ sessionId: sid }); - Chat.unregisterContainer(sid); // 释放容器(若已打开) - const view = [...$("workspace").children].find((c) => c.dataset?.sid === sid); - if (view) view.remove(); - if (state.sessionId === sid) { - // 关闭激活会话 → 切到剩余打开会话中最近激活的,否则新建 - const remaining = state.openSessions.filter((s) => s.sid !== sid); - if (remaining.length > 0) { - await switchSession(remaining[0].sid, { silent: true }); - } else { - await newSession(); - } - } - } catch (e) { - Chat.addSystemMessage(_t("app.closeFailed", { msg: e.message })); - } - } - - // 打开会话右键菜单:关闭(留数据)/ 重命名 / 删除(删数据,确认) - function showOpenSessionsMenu(item, entry) { - const menu = $("ctx-menu"); - menu.innerHTML = ""; - const mk = (label, danger, action) => { - const b = el("button", { class: "ctx-item" + (danger ? " danger" : "") }, label); - b.addEventListener("click", () => { - hideCtxMenu(); - action(); - }); - menu.appendChild(b); - }; - const cur = state.sessions.find((s) => s.session_id === entry.sid); - const title = (cur && cur.title) || entry.sid; - mk(_t("app.closeSession"), false, () => closeOpenSession(entry.sid)); - mk(_t("app.rename"), false, () => Dialogs.showRename(entry.sid, title)); - mk(_t("app.deleteConv"), true, () => Dialogs.showConfirm(EMRG_Copy.COPY.deleteConfirmTitle, EMRG_Copy.COPY.deleteConfirmBody, { - okText: _t("dlg.delete"), - danger: true, - onOk: () => deleteSession(entry.sid), - })); - menu.hidden = false; - const rect = item.getBoundingClientRect(); - menu.style.left = Math.min(rect.right, window.innerWidth - 160) + "px"; - menu.style.top = Math.min(rect.bottom, window.innerHeight - 80) + "px"; - const btns = menu.querySelectorAll(".ctx-item"); - let idx = btns.length ? 0 : -1; - const setActive = (i) => { - if (i < 0 || i >= btns.length) return; - idx = i; - btns.forEach((b, j) => b.classList.toggle("active", j === idx)); - }; - setActive(0); - _ctxMenuKeyHandler = (ev) => { - if (ev.key === "ArrowDown") { - ev.preventDefault(); - setActive((idx + 1) % btns.length); - } else if (ev.key === "ArrowUp") { - ev.preventDefault(); - setActive((idx - 1 + btns.length) % btns.length); - } else if (ev.key === "Enter") { - ev.preventDefault(); - if (idx >= 0) btns[idx].click(); - } else if (ev.key === "Escape") { - ev.preventDefault(); - hideCtxMenu(); - } - }; - document.addEventListener("keydown", _ctxMenuKeyHandler); - } - - // ── 右键菜单(重命名 / 删除) ─────────── - function showConvMenu(item, sid, title) { - // 设计 §3.2:右键菜单 = 重命名 / 删除(删除有友好确认) - const menu = $("ctx-menu"); - menu.innerHTML = ""; - const mk = (label, danger, action) => { - const b = el("button", { class: "ctx-item" + (danger ? " danger" : "") }, label); - b.addEventListener("click", () => { - hideCtxMenu(); - action(); - }); - menu.appendChild(b); - }; - mk(_t("app.rename"), false, () => Dialogs.showRename(sid, title)); - mk(_t("app.deleteConv"), true, () => Dialogs.showConfirm(EMRG_Copy.COPY.deleteConfirmTitle, EMRG_Copy.COPY.deleteConfirmBody, { - okText: _t("dlg.delete"), - danger: true, - onOk: () => deleteSession(sid), - })); - menu.hidden = false; - // 定位在右键处,超出视口则上移/左移 - const rect = item.getBoundingClientRect(); - menu.style.left = Math.min(rect.right, window.innerWidth - 160) + "px"; - menu.style.top = Math.min(rect.bottom, window.innerHeight - 80) + "px"; - // 键盘导航:↑↓ 移动 / Enter 选择 / ESC 关闭(与模型切换器一致) - const btns = menu.querySelectorAll(".ctx-item"); - let idx = btns.length ? 0 : -1; - const setActive = (i) => { - if (i < 0 || i >= btns.length) return; - idx = i; - btns.forEach((b, j) => b.classList.toggle("active", j === idx)); - }; - setActive(0); - _ctxMenuKeyHandler = (ev) => { - if (ev.key === "ArrowDown") { - ev.preventDefault(); - setActive((idx + 1) % btns.length); - } else if (ev.key === "ArrowUp") { - ev.preventDefault(); - setActive((idx - 1 + btns.length) % btns.length); - } else if (ev.key === "Enter") { - ev.preventDefault(); - if (idx >= 0) btns[idx].click(); - } else if (ev.key === "Escape") { - ev.preventDefault(); - hideCtxMenu(); - } - }; - document.addEventListener("keydown", _ctxMenuKeyHandler); - } - - function hideCtxMenu() { - const menu = $("ctx-menu"); - menu.hidden = true; - menu.innerHTML = ""; - if (_ctxMenuKeyHandler) { - document.removeEventListener("keydown", _ctxMenuKeyHandler); - _ctxMenuKeyHandler = null; - } - } - - // ── 模型切换器 ───────────────────────── - async function updateModelSwitcher() { - const label = $("model-switcher-label"); - if (label) label.textContent = state.model || (window.EMRG_I18N ? window.EMRG_I18N.t("composer.modelLoading") : "加载中…"); - } - - /** rant 21:19:locale 切换后重刷动态文案(i18n.apply 回调) */ - function refreshLocale() { - updateModelSwitcher(); - // 空状态示例卡片:data-i18n 已由 apply 重刷;若补全菜单打开则重建(hint 本地化) - const menu = $("cmd-menu"); - if (menu && !menu.hidden) { - showCmdMenu(($("input").value || "").trim()); - } - } - - // ── 自进化可见化(WorkBuddy P3)────────────────── - /** 版本信息(/version 命令 + 进化 toast "去看看" 共用) */ - function showVersionInfo() { - Chat.addSystemMessage( - _t("app.versionInfo", { ver: state.version || "0.2.8", id: state.serverId || _t("app.unknown"), model: state.model || _t("app.unknown"), n: state.evolutionCount ?? 0 }) - ); - } - - /** 更新侧边栏成长状态卡(计数 + 提示语) */ - function updateGrowthCard() { - const n = state.evolutionCount ?? 0; - const countEl = $("growth-count"); - if (countEl) countEl.textContent = String(n); - const noteEl = $("growth-note"); - if (noteEl) noteEl.textContent = EMRG_Copy.COPY.growthNote; - } - - /** 进化完成 toast:检测 evolution_count 增长,一天最多提示一次 */ - function maybeShowEvolutionToast() { - if (state.evolutionCount == null) return; - const prev = state.lastKnownEvolutionCount; - state.lastKnownEvolutionCount = state.evolutionCount; - if (prev == null || state.evolutionCount <= prev) return; // 首次连接/无增长不提示 - // 演化刚完成 = 演化真正需要 GitHub 的时刻(Windows GCM rant Stage 2) - maybeShowGithubBanner(); - // 频率控制:一天最多 1 次(localStorage 可能不可用 → 静默跳过) - const today = new Date().toISOString().slice(0, 10); - try { - if (localStorage.getItem("emrg.evoToast.date") === today) return; - localStorage.setItem("emrg.evoToast.date", today); - } catch { /* ignore */ } - showEvolutionToast(); - } - - function showEvolutionToast() { - const toast = $("evolution-toast"); - if (!toast) return; - const msg = $("evolution-toast-msg"); - if (msg) msg.textContent = EMRG_Copy.COPY.evolutionToastMsg(state.evolutionCount ?? 0); - toast.classList.remove("hidden"); - const star = $("brand-star"); - if (star) star.classList.add("pulse"); - } - - function hideEvolutionToast() { - const toast = $("evolution-toast"); - if (toast) toast.classList.add("hidden"); - const star = $("brand-star"); - if (star) star.classList.remove("pulse"); - } - - // ── GitHub 连接横幅(Windows GCM rant Stage 2) ──── - // 触发时机:演化计数增长(演化刚完成、需要 GitHub 推 PR)且未认证时。 - // 本地聊天不依赖 GitHub → 启动时不打扰。横幅可关闭;关闭后本会话不再弹。 - let _githubBannerDismissed = false; - - function showGithubBanner() { - const b = $("github-banner"); - if (!b) return; - const msg = $("github-banner-msg"); - if (msg) msg.textContent = _t("settings.githubBannerMsg"); - b.classList.remove("hidden"); - } - - function hideGithubBanner() { - const b = $("github-banner"); - if (b) b.classList.add("hidden"); - } - - async function maybeShowGithubBanner() { - if (_githubBannerDismissed) return; - try { - const s = await window.emrg.githubStatus(); - if (s && s.authenticated) return; // 已连接 → 无需提示 - showGithubBanner(); - } catch { /* githubStatus 不可用(daemon 忙/未合入)→ 静默跳过 */ } - } - - function initGithubBanner() { - const connect = $("github-banner-connect"); - if (connect) { - connect.addEventListener("click", () => { - hideGithubBanner(); - Dialogs.showSettings(); // 设置页 GitHub 连接区(Stage 2a) - }); - } - const dismiss = $("github-banner-dismiss"); - if (dismiss) { - dismiss.addEventListener("click", () => { - _githubBannerDismissed = true; - hideGithubBanner(); - }); - } - } - - function initEvolutionToast() { - const see = $("evolution-toast-see"); - if (see) { - see.addEventListener("click", () => { - hideEvolutionToast(); - showVersionInfo(); - }); - } - const dismiss = $("evolution-toast-dismiss"); - if (dismiss) dismiss.addEventListener("click", hideEvolutionToast); - } - - async function loadEvolutionSummary() { - const recent = $("about-recent"); - if (!recent) return; - try { - const res = await window.emrg.evolutionSummary({ limit: 5 }); - if (res && res.count !== undefined) { - state.evolutionCount = res.count; - updateGrowthCard(); - } - const items = (res && res.recent) || []; - recent.innerHTML = ""; - if (items.length === 0) { - recent.appendChild(el("div", { class: "about-recent-item" }, _t("app.noImprovements"))); - return; - } - const header = el("div", { class: "about-recent-title" }, _t("app.recentImprovements")); - recent.appendChild(header); - for (const it of items) { - const ts = String(it.timestamp || "").slice(0, 16).replace("T", " "); - const ops = (it.operations || []).join(" · "); - const row = el("div", { class: "about-recent-item" }); - row.appendChild(el("span", { class: "about-recent-time" }, ts)); - row.appendChild(el("span", {}, ops || "self-improvement")); - recent.appendChild(row); - } - } catch { /* 摘要加载失败静默(进化卡仍显示 count) */ } - } - - function initModelSwitcher() { - const sw = $("model-switcher"); - sw.addEventListener("click", async (e) => { - e.stopPropagation(); - closeModelMenu(); - try { - const s = await window.emrg.getSettings(); - const models = s.models?.length ? s.models : s.model ? [s.model] : []; - const menu = el("div", { class: "model-menu" }); - if (!models.length) { - const empty = el("div", { class: "model-menu-empty" }, _t("app.noModels")); - const go = el("a", {}, _t("app.goSettings")); - go.addEventListener("click", () => { - closeModelMenu(); - Dialogs.showSettings(); - }); - empty.appendChild(document.createTextNode(",")); - empty.appendChild(go); - menu.appendChild(empty); - } else { - for (const m of models) { - const name = typeof m === "string" ? m : m.name; - const vision = typeof m === "object" && m.vision; - const item = el("div", { class: "model-menu-item" }); - item.appendChild(el("span", { class: "model-check" }, name === state.model ? "✓ " : "")); - item.appendChild(el("span", {}, name)); - if (vision) item.appendChild(el("span", { class: "model-vision" }, " 🖼")); - item.addEventListener("click", async () => { - closeModelMenu(); - if (name === state.model) return; - try { - await window.emrg.setModel({ model: name }); - state.model = name; - updateModelSwitcher(); - sw.classList.add("highlight"); - setTimeout(() => sw.classList.remove("highlight"), 1200); - } catch (err) { - Chat.addSystemMessage(_t("app.modelSwitchFailed", { msg: err.message })); - } - }); - menu.appendChild(item); - } - } - $("main").appendChild(menu); - document.addEventListener("click", closeModelMenu, { once: true }); - // 键盘导航:↑↓ 移动 / Enter 选择 / ESC 关闭(与 TUI 选择器一致的交互) - const items = menu.querySelectorAll(".model-menu-item"); - let idx = items.length ? 0 : -1; - const setActive = (i) => { - if (i < 0 || i >= items.length) return; - idx = i; - items.forEach((it, j) => it.classList.toggle("active", j === idx)); - }; - setActive(0); - _modelMenuKeyHandler = (ev) => { - if (ev.key === "ArrowDown") { - ev.preventDefault(); - setActive((idx + 1) % items.length); - } else if (ev.key === "ArrowUp") { - ev.preventDefault(); - setActive((idx - 1 + items.length) % items.length); - } else if (ev.key === "Enter") { - ev.preventDefault(); - if (idx >= 0) items[idx].click(); - } else if (ev.key === "Escape") { - ev.preventDefault(); - closeModelMenu(); - } - }; - document.addEventListener("keydown", _modelMenuKeyHandler); - } catch (err) { - Chat.addSystemMessage(_t("app.modelListFailed", { msg: err.message })); - } - }); - } - - function closeModelMenu() { - const menu = document.querySelector(".model-menu"); - if (menu) menu.remove(); - if (_modelMenuKeyHandler) { - document.removeEventListener("keydown", _modelMenuKeyHandler); - _modelMenuKeyHandler = null; - } - } - - // ── 沙箱档位切换器(read-only / workspace-write / danger-full-access,rant 2026-08-20T18:18) ── - function initModeSwitcher() { - const sw = $("sandbox-switcher"); - if (!sw) return; - sw.addEventListener("click", (e) => { - const btn = e.target.closest ? e.target.closest(".mode-btn") : null; - if (!btn || !btn.dataset.sandbox) return; - setSandbox(btn.dataset.sandbox); - }); - } - - function setSandbox(sandbox) { - if (!["read-only", "workspace-write", "danger-full-access"].includes(sandbox)) return; - state.sandbox = sandbox; - const sw = $("sandbox-switcher"); - if (!sw) return; - for (const btn of sw.querySelectorAll(".mode-btn")) { - btn.classList.toggle("active", btn.dataset.sandbox === sandbox); - } - } - - // ── 空状态欢迎屏 ─────────────────────── - function updateEmptyState() { - const empty = $("empty-state"); - // P3 slice 2:检查**激活会话**容器(wrapper 恒含 session-view 子节点,不能数 wrapper) - const activeView = Chat.chatContainer(state.sessionId); - empty.classList.toggle("hidden", activeView.children.length > 0); - } - - // ── 输入条状态 ───────────────────────── - function setComposerDisabled(disabled) { - $("input").disabled = disabled; - $("send-btn").disabled = disabled; - $("stop-btn").style.display = disabled ? "inline-flex" : "none"; - } - - // ── 连接状态 ─────────────────────────── - function updateConnectionDot(kind) { - const dot = $("status-dot"); - if (dot) dot.className = "status-dot " + kind; - } - - function showBanner(text) { - const b = $("conn-banner"); - b.textContent = text; - b.classList.remove("hidden"); - } - function hideBanner() { - $("conn-banner").classList.add("hidden"); - } - - // ── 升级完成横幅(rant 18:30:57 + 12:44:34 + 14:38:27) ───────────── - // 14:38:27 重构:参数为 (installedVersion, runVersion)——installed 是磁盘实时 - // 安装版本(升级 agent 已更新但未重启),run 是 daemon 进程实际运行版本。 - // installed ≠ run → "已从 {run} 升级到 {installed},重启后生效"。 - function maybeShowUpgradeBanner(installedVersion, runVersion) { - if (!installedVersion) return; // 无版本数据(dev 运行)→ 不显示 - if (installedVersion === state.lastKnownVersion) return; // 已提示过该版本(防重复弹) - const b = $("upgrade-banner"); - if (!b) return; - const msg = $("upgrade-banner-msg"); - if (msg) { - if (runVersion && runVersion !== installedVersion) { - msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: runVersion, to: installedVersion }); - } else { - msg.textContent = _t("app.upgradeBannerMsg", { version: installedVersion }); - } - } - b.classList.remove("hidden"); - state.lastKnownVersion = installedVersion; // 已提示,防重复弹 - } - function hideUpgradeBanner() { - const b = $("upgrade-banner"); - if (b) b.classList.add("hidden"); - } - function initUpgradeBanner() { - const restart = $("upgrade-banner-restart"); - if (restart) restart.addEventListener("click", async () => { - restart.disabled = true; - try { - await window.emrg.restartDaemon(); - // shutdown 已发——connManager restart-recovery 会重新 spawn 新 daemon 并重连; - // 恢复后 status 事件里 current_version 已 = 新版本 → 横幅消失。 - hideUpgradeBanner(); - } catch (e) { - restart.disabled = false; - Chat.addSystemMessage(_t("app.upgradeRestartFailed", { msg: e.message })); - } - }); - const dismiss = $("upgrade-banner-dismiss"); - if (dismiss) dismiss.addEventListener("click", hideUpgradeBanner); - } - - // ── 事件处理(main 已分类) ───────────── - async function handleEvent(evt) { - const { type, data } = evt; - // P3(rant 15:07:19):main 事件桥附带 sid(#629 起)——Chat 按会话隔离状态; - // 无 sid(单会话过渡期)→ 默认桶,行为与改造前一致。 - const sid = evt.sid; - switch (type) { - case "message_delta": - Chat.handleDelta(data.chunks || [data], sid); - break; - case "done": - Chat.handleDone(data, sid); - // P3 slice 1:done 释放**该事件所属会话**的 busy 锁(后台会话的广播 done - // 不误清激活会话);仅当该会话是激活会话时同步输入条 UI。 - { - const sst = sidState(sid); - if (data.request_id && (sst.ownStreamRequestId === data.request_id || data.timeout)) { - sst.busy = false; - sst.ownStreamRequestId = null; - if (!sid || sid === state.sessionId) setComposerDisabled(false); - } - } - break; - case "tool_started": - Chat.handleToolStart(data, sid); - break; - case "tool_finished": - Chat.handleToolEnd(data, sid); - ResultPanel.addToolResult(data, sid); // P2 框架:产物登记按事件 sid 归类(P3.2 消费) - break; - case "cancelled": - Chat.clearTyping(sid); // rant 14:11:取消时移除在途节点 typing 光标(无 request_id,全清) - // P3 slice 1:cancelled 释放该事件所属会话的锁(后台会话取消不误清激活会话) - { - const sst = sidState(sid); - sst.busy = false; - sst.ownStreamRequestId = null; - if (!sid || sid === state.sessionId) setComposerDisabled(false); - } - break; - // P2 queue-injection(#655):4 个 daemon→client 广播帧(busy 排队注入协议) - case "task_queued": - Chat.addSystemMessage(_t("app.queued", { pos: data.position || 0 }), sid); - break; - case "steer_committed": - // 已注入当前回合——从待重发记录移除(回合内 deltas 会带上原 turn 的回复) - { - const q = state.queuedSends.get(sid); - if (q && data.request_id) { - const idx = q.findIndex((e) => e.requestId === data.request_id); - if (idx >= 0) q.splice(idx, 1); - if (q.length === 0) state.queuedSends.delete(sid); - } - } - break; - case "queued_requeue": - // 回合正常结束且消息从未注入——daemon 锁已释放,以原 requestId 静默重发 - // (不重加用户行;后台会话按 sid 处理,只操作该会话条目) - { - const q = state.queuedSends.get(sid); - if (q && q.length) { - const ids = new Set(data.request_ids || []); - const toResend = q.filter((e) => ids.has(e.requestId)); - const remaining = q.filter((e) => !ids.has(e.requestId)); - if (toResend.length) { - // P2 审查 ❌ 同 #695:was_busy 在循环前捕获,单客户端时首条重发 - // 开启新回合,M2+ 到达时 daemon busy 被再排队但客户端未跟踪 → 下个 - // queued_requeue 找不到 → 静默丢失。修复=每条重发若 (wasBusy || i>0) - // 重新跟踪——steer_committed 移除已注入的,下个 queued_requeue 重发 - // 其余,收敛。 - const wasBusy = sidState(sid).busy; - for (let i = 0; i < toResend.length; i++) { - const item = toResend[i]; - const sst = sidState(sid); - sst.busy = true; - sst.ownStreamRequestId = item.requestId; - if (!sid || sid === state.sessionId) setComposerDisabled(true); - try { - const res = await window.emrg.sendMessage({ sessionId: sid, text: item.text, requestId: item.requestId, sandbox: item.sandbox }); - sst.ownStreamRequestId = res.requestId || item.requestId; - } catch (e) { - sst.busy = false; - sst.ownStreamRequestId = null; - if (!sid || sid === state.sessionId) setComposerDisabled(false); - } - if (wasBusy || i > 0) { - remaining.push({ requestId: item.requestId, text: item.text, sandbox: item.sandbox }); - } - } - if (remaining.length) state.queuedSends.set(sid, remaining); - else state.queuedSends.delete(sid); - Chat.addSystemMessage(_t("app.queuedResent", { n: toResend.length }), sid); - } - } - } - break; - case "queued_cancelled": - // 回合被取消/异常/断连——daemon 丢弃队列 - if (state.queuedSends.delete(sid)) { - Chat.addSystemMessage(_t("app.queuedCancelled"), sid); - } - break; - case "error": - handleError(data, sid); - break; - case "pong": - state.serverId = data.identity?.instance_id || state.serverId; - state.model = data.model || state.model; - state.evolutionCount = data.evolution_count ?? state.evolutionCount; - updateModelSwitcher(); - updateGrowthCard(); - maybeShowEvolutionToast(); - break; - case "status": - handleStatus(data); - break; - case "upgrade": // rant 14:38:27:心跳检测到 installed ≠ run → 弹"重启生效"横幅 - maybeShowUpgradeBanner(data.installed_version, data.current_version); - break; - case "sessions": - state.sessions = data.sessions || []; - break; - case "open_sessions": - // P4 slice 2:main 广播打开会话列表变化 → 刷新侧边栏打开会话区 - state.openSessions = data.openSessions || []; - Sidebar.renderOpenSessions(state.openSessions); - break; - case "disconnected": - // P3 finalize:断连按 sid 隔离——后台会话断连不影响全局 UI(无横幅/红点), - // 仅激活会话(或无 sid 的单会话过渡期)断连显示全局横幅 + 红点。 - { - const sst = sidState(sid); - sst.busy = false; - sst.ownStreamRequestId = null; - sst.disconnected = true; // P3 finalize:该会话条目标断线(P4 恢复 UI 用) - state.queuedSends.delete(sid); // P2 queue-injection:断连 daemon 丢队列 - const isActive = !sid || sid === state.sessionId; - if (isActive) { - updateConnectionDot("red"); - showBanner(EMRG_Copy.COPY.disconnected); - // G89:断连时恢复输入条(不能依赖 30s 超时兜底) - setComposerDisabled(false); - } - // 容器标断线(仅当该 sid 有**独立注册**容器——chatContainer 对未注册 - // sid 回退激活容器,打错标;P4 openSessions 后每会话都有注册容器) - if (Chat.hasContainer(sid)) { - const cv = Chat.chatContainer(sid); - if (cv && cv.classList) cv.classList.add("disconnected"); - } - } - // P3:广播分组缓存清理按会话隔离(DOM 保留;仅清该会话 Map 引用;无 sid → 默认桶) - Chat.groupNodesFor(sid).clear(); - // 进行中的工具行 → 结果未知(工具副作用不可重放) - for (const row of Chat.toolRowsFor(sid).values()) { - if (row.classList.contains("running")) { - row.classList.remove("running"); - row.classList.add("failed"); - const label = row.querySelector(".tool-label"); - if (label) label.textContent = _t("app.unknownResult"); - } - } - break; - case "group_cleared": - Chat.groupNodes.delete(data.requestId); - break; - case "list_result": - if (data.type === "sessions_list") { - state.sessions = data.sessions || []; - } - break; - case "command_result": - if (data.type === "model_set") { - state.model = data.model || state.model; - updateModelSwitcher(); - } - break; - default: - break; - } - } - - function handleError(data, sid) { - const sst = sidState(sid); - if (data.error && String(data.error).includes("session busy")) { - Chat.addSystemMessage(EMRG_Copy.COPY.sessionBusy, sid); - sst.busy = false; - sst.ownStreamRequestId = null; - if (!sid || sid === state.sessionId) setComposerDisabled(false); - } else { - Chat.clearTyping(sid); // rant 14:11:流式错误时移除在途节点 typing 光标 - Chat.addSystemMessage(_t("app.error", { msg: data.error || _t("app.unknownError") }), sid); - } - } - - function handleStatus(data) { - if (data.connected) { - updateConnectionDot("green"); - hideBanner(); - // P3 finalize:重连成功 → 清全部会话断线标记 + 容器 .disconnected 类 - for (const entry of state.sessionsBySid.values()) entry.disconnected = false; - for (const child of $("workspace").children) { - if (child.classList && child.classList.contains("disconnected")) child.classList.remove("disconnected"); - } - if (data.server_id) state.serverId = data.server_id; - if (data.model) state.model = data.model; - state.evolutionCount = data.evolution_count ?? state.evolutionCount; - // 14:38:27:升级判断移出 status——由 main.js 心跳对比 installed/current 发 - // 专用 upgrade 事件(本分支不再弹横幅,避免重连刷屏副作用)。 - updateModelSwitcher(); - updateGrowthCard(); - maybeShowEvolutionToast(); - Chat.addSystemMessage(EMRG_Copy.COPY.reconnected); - } else if (data.auth_failed) { - updateConnectionDot("red"); - Chat.addSystemMessage(_t("app.authFailed")); - } else if (data.daemon_stopped) { - // Rant 2026-08-09T13:16:36 ⑤:spawn 节流命中——显示真实失败原因(含 - // emrgd.log 尾部),提示宿主手动启动,不再无限重试弹窗。 - updateConnectionDot("red"); - Chat.addSystemMessage(_t("app.daemonStopped", { msg: data.error || "" })); - } else { - updateConnectionDot("red"); - } - } - - // ── UI 绑定 ───────────────────────────── - function bindUi() { - $("send-btn").addEventListener("click", sendMessage); - $("stop-btn").addEventListener("click", () => window.emrg.cancel().catch(() => {})); - // rant 18:55:09 v0.2:侧边栏导航点击 → 工作区视图切换(data-view) - for (const p of VIEWS) { - $(`nav-${p}`)?.addEventListener("click", () => { - switchView(p); - // 面板视图打开时加载对应数据(settings 走 showSettings 刷新全部;tasks/projects/rants 走各自加载) - if (p === "settings" && state.activeView === "settings") { - loadEvolutionSummary(); - Dialogs.showSettings(); - } else if (p === "tasks" && state.activeView === "tasks") { - openTasksPanel(); - } else if (p === "projects" && state.activeView === "projects") { - openProjectsPanel(); - } else if (p === "rants" && state.activeView === "rants") { - openRantsPanel(); - } - }); - } - $("new-chat-btn").addEventListener("click", () => Dialogs.showNewSessionDialog()); - // B2(rant 21:59:11):侧边栏"打开会话"入口 → 两步弹窗(选项目 → 选会话) - $("open-chat-btn")?.addEventListener("click", () => Dialogs.showOpenSessionDialog()); - Dialogs.initOpenSessionDialog(); // P5:打开会话对话框绑定 - Dialogs.initNewSessionDialog(); // P5 slice 2:新建会话对话框绑定 - $("open-session-new-session")?.addEventListener("click", () => Dialogs.showNewSessionDialog()); // P5 slice 2:打开弹窗 → 新建会话 - Dialogs.initLangButtons(); // rant 21:19:设置语言选择器 - $("settings-cancel").addEventListener("click", () => switchView("settings")); // P2:取消=关闭设置视图 - $("settings-save").addEventListener("click", Dialogs.saveSettings); - $("welcome-save").addEventListener("click", Dialogs.saveWelcome); - $("confirm-cancel").addEventListener("click", Dialogs.closeConfirm); - $("confirm-ok").addEventListener("click", Dialogs.confirmOk); - $("help-close").addEventListener("click", () => $("help-dialog").close()); - $("rewind-close").addEventListener("click", () => $("rewind-dialog").close()); - $("memory-close").addEventListener("click", () => $("memory-dialog").close()); - $("skills-close").addEventListener("click", () => $("skills-dialog").close()); - // rant 14:10:14 P6:sessions-dialog / rant-dialog 已移除(侧边栏会话区 + /open + Rant 面板覆盖) - - // rant 14:10:14 P2:设置面板 tab 切换(模型服务/工作目录/GitHub/外观/语言/关于) - document.querySelectorAll("#settings-tabs .panel-tab").forEach((tab) => { - tab.addEventListener("click", () => switchSettingsTab(tab.dataset.settingsTab)); - }); - // rant 14:10:14 P5:项目面板「添加项目」→ 选目录 → 注册 - $("project-add-btn")?.addEventListener("click", async () => { - try { - const res = await window.emrg.pickProjectDir(); - if (res && res.path) { - await window.emrg.registerProject({ path: res.path }); - Chat.addSystemMessage(_t("projects.added", { path: res.path })); - await Dialogs.renderProjectList(); - } - } catch (e) { - Chat.addSystemMessage(_t("projects.addFailed", { msg: e.message })); - } - }); - - // 设置/首启对话框:Enter 提交(与重命名/模型表单一致的交互) - const enterToSave = (fn) => (e) => { - if (e.key === "Enter") { e.preventDefault(); fn(); } - }; - $("set-api-key").addEventListener("keydown", enterToSave(Dialogs.saveSettings)); - $("set-base-url").addEventListener("keydown", enterToSave(Dialogs.saveSettings)); - $("welcome-api-key").addEventListener("keydown", enterToSave(Dialogs.saveWelcome)); - $("welcome-base-url").addEventListener("keydown", enterToSave(Dialogs.saveWelcome)); - - const input = $("input"); - input.addEventListener("keydown", (e) => { - // / 补全菜单键盘导航(rant 19:44 P1):↑↓ 移动、Enter 选择、Esc 关闭 - if (state.cmdMenu.items.length > 0) { - if (e.key === "ArrowDown") { - e.preventDefault(); - state.cmdMenu.index = (state.cmdMenu.index + 1) % state.cmdMenu.items.length; - highlightCmdMenu(); - return; - } - if (e.key === "ArrowUp") { - e.preventDefault(); - state.cmdMenu.index = (state.cmdMenu.index - 1 + state.cmdMenu.items.length) % state.cmdMenu.items.length; - highlightCmdMenu(); - return; - } - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - const item = state.cmdMenu.items[state.cmdMenu.index]; - if (item) selectCmd(item.cmd); - return; - } - if (e.key === "Escape") { - e.preventDefault(); - hideCmdMenu(); - return; - } - } - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - sendMessage(); - } - // Ctrl+Enter 同发送 - if (e.key === "Enter" && e.ctrlKey) { - e.preventDefault(); - sendMessage(); - } - }); - input.addEventListener("input", () => { - input.style.height = "auto"; - input.style.height = Math.min(input.scrollHeight, 150) + "px"; - // / 指令补全:以 / 开头且无空格(仍处于指令词)→ 弹出菜单 - const v = input.value.trim(); - if (v.startsWith("/") && !v.includes(" ")) { - showCmdMenu(v); - } else { - hideCmdMenu(); - } - }); - - const chatView = $("workspace"); - // scroll 事件不冒泡 → 必须用 capture 捕获子 .session-view 的滚动(#634 起滚动容器下移; - // 此前监听挂在 #workspace 上从不触发,back-to-bottom 与滚动加载历史在真实 GUI 静默失效) - chatView.addEventListener("scroll", updateBackToBottomState, true); - $("back-to-bottom").addEventListener("click", () => { - const el = activeScrollEl(); - el.scrollTop = el.scrollHeight; - state.autoScroll = true; - $("back-to-bottom").classList.add("hidden"); - }); - // rant 14:15:12:会话视图滚动到顶 → 加载更早历史(防抖 150ms) - chatView.addEventListener("scroll", (e) => { - const t = e.target; - const isView = t && t.classList && t.classList.contains("session-view"); - if (!isView || !state.sessionId) return; - const st2 = historyPageState(state.sessionId); - if (t.scrollTop <= 2 && st2.hasMore && !st2.loading) { - clearTimeout(historyScrollTimer); - historyScrollTimer = setTimeout(() => loadOlderHistory(state.sessionId), 150); - } - }, { capture: true, passive: true }); - - // 空状态示例问题卡片 → 填入输入框 - $("empty-state").addEventListener("click", (e) => { - const card = e.target.closest(".example-card"); - if (!card) return; - // rant 21:19:示例提示随语言走(textContent 已本地化;data-example 为中文兜底) - input.value = (card.textContent || card.dataset.example || "").trim(); - input.focus(); - input.style.height = "auto"; - input.style.height = Math.min(input.scrollHeight, 150) + "px"; - }); - - // 右键菜单:点击别处隐藏(showConvMenu 内已绑定选项点击) - document.addEventListener("click", (e) => { - if (!$("ctx-menu").hidden && !e.target.closest("#ctx-menu")) hideCtxMenu(); - }); - - // 快捷键:⌘N 新对话 / ⌘B 折叠侧边栏 / ⌘, 设置 / ESC 停止或关弹窗 - document.addEventListener("keydown", (e) => { - if (e.metaKey || e.ctrlKey) { - if (e.key === "n") { - e.preventDefault(); - // B1(rant 21:59:11):⌘N 与侧边栏按钮一致 → 项目选择弹窗 - Dialogs.showNewSessionDialog(); - } else if (e.key === "b") { - e.preventDefault(); - document.body.classList.toggle("sidebar-collapsed"); - } else if (e.key === ",") { - e.preventDefault(); - Dialogs.showSettings(); - } - return; - } - if (e.key === "Escape") { - hideCtxMenu(); // 右键菜单优先关闭 - const dlgs = document.querySelectorAll("dialog[open]"); - if (dlgs.length) return; // dialog 原生 ESC 处理 - if (state.busy) { - e.preventDefault(); - window.emrg.cancel().catch(() => {}); - } - } - }); - - Dialogs.initThemeButtons(); - Dialogs.initModelForm(); - Dialogs.initRenameDialog(); - Dialogs.initGithubSection(); // Windows GCM rant Stage 2:设置页 GitHub 连接 - Dialogs.initDeviceDialog(); // Windows GCM rant Stage 2b:device flow 对话框 - Dialogs.initTaskManagement(); // rant 18:23:15 P3:定时任务/自定义类型管理 - Dialogs.initRantPanel(); // rant 14:10:14 P4:rant 面板(筛选/新建) - initGithubBanner(); // Windows GCM rant Stage 2:演化需 GitHub 但未认证时的连接横幅 - initUpgradeBanner(); // rant 18:30:57:升级完成横幅(重启生效按钮) - initModelSwitcher(); - initModeSwitcher(); // rant 18:18:沙箱档位切换器(替代 Ask/Auto) - ResultPanel.init(); // WorkBuddy P1:结果面板(⌘\ 折叠 + 窄屏自动隐藏) - FileTree.setSession(state.sessionId, projectPathFor(state.sessionId)); // P3.1:文件树初始根 - initEvolutionToast(); // WorkBuddy P3:进化 toast 按钮绑定 - } - - // ── 暴露 ───────────────────────────────── - return { - state, - boot, - sendMessage, - handleCommand, - switchSession, - newSession, - deleteSession, - closeOpenSession, // P4 slice 2:关闭打开会话(保留数据) - showOpenSessionsMenu, // P4 slice 2:打开会话右键菜单 - activateSessionView, // P3 slice 2:激活会话容器(display 切换;导出供测试) - switchView, // rant 18:55:09 v0.2:工作区视图切换(导出供测试) - switchSettingsTab, // rant 14:10:14 P2:设置面板 tab 切换(导出供测试) - openTasksPanel, // rant 14:10:14 P3:任务面板打开 + 加载(导出供测试) - doTrigger, // rant 09:23:10:/trigger 三态语义(running → info;导出供测试) - openProjectsPanel, // rant 14:10:14 P5:项目面板打开 + 加载(导出供测试) - openRantsPanel, // rant 14:10:14 P4:rant 面板打开 + 加载(导出供测试) - refreshSessions, - showConvMenu, - handleEvent, - bindUi, - updateEmptyState, - updateModelSwitcher, - updateGrowthCard, // WorkBuddy P3:成长卡(导出供测试) - maybeShowEvolutionToast, // WorkBuddy P3:进化 toast 检测 - showVersionInfo, // WorkBuddy P3:/version 内容(toast "去看看" 共用) - refreshLocale, // rant 21:19:locale 切换后动态文案重刷 - setSandbox, // rant 18:18:沙箱档位切换(导出供测试与外部调用) - loadEvolutionSummary, // WorkBuddy P3:最近改进摘要 - }; -})(); - -window.App = App; - -// ── 启动(模块级只绑定一次;boot 可被 saveSettings 重复调用) ── -App.bindUi(); -window.emrg.onEvent(App.handleEvent); -App.boot(); diff --git a/emrg/gui/renderer/js/chat.js b/emrg/gui/renderer/js/chat.js deleted file mode 100644 index 125605d6..00000000 --- a/emrg/gui/renderer/js/chat.js +++ /dev/null @@ -1,471 +0,0 @@ -"use strict"; -/** - * chat.js — 聊天渲染:用户气泡 / EMRG ✦ 消息 / 流式 delta / 工具友好行 / 系统消息。 - * 事件类型与 main 契约不变(message_delta/done/tool_started/tool_finished/…)。 - */ - -const Chat = (() => { - // P3(rant 15:07:19):会话级状态隔离——每会话一份 groupNodes/toolRows/doneRids。 - // sid=null 为旧版单会话桶(无 sid 事件/旧调用方 → 行为与改造前完全一致)。 - // 容器路由:registerContainer(sid, el) 后该 sid 渲染进独立容器;未注册 → 默认 $("workspace") - // (P4 openSessions 前实际只存在一个激活会话,容器切换随 P4 落地)。 - const sessionState = new Map(); // sid|null → { groupNodes, toolRows, doneRids } - const containers = new Map(); // sid → 容器元素(P4 起每会话一个 .session-view) - - function st(sid) { - const key = sid || null; - if (!sessionState.has(key)) { - sessionState.set(key, { groupNodes: new Map(), toolRows: new Map(), doneRids: new Set() }); - } - return sessionState.get(key); - } - - /** 该会话的渲染容器:已注册的独立容器优先,其次当前激活会话的容器(无 sid 事件落激活会话,P3 slice 2),最后回退默认聊天区(单会话过渡期行为)。 */ - function chatContainer(sid) { - if (containers.has(sid)) return containers.get(sid); - const active = App.state?.sessionId; - if (active && containers.has(active)) return containers.get(active); - return $("workspace"); - } - - /** P4 起:为新打开的会话注册独立容器;关闭时 unregister 清引用 */ - function registerContainer(sid, el) { - containers.set(sid, el); - } - function unregisterContainer(sid) { - containers.delete(sid); - sessionState.delete(sid); // 会话关闭 → 释放其分组/工具行/已 done 状态 - } - /** 该 sid 是否有**独立注册**容器(区别于 chatContainer 的回退链——断线标记只打真实容器) */ - function hasContainer(sid) { - return containers.has(sid); - } - - /** 复制代码按钮(设计 §3.3):事件委托在聊天区,CSP 无内联 handler */ - function initCodeCopy() { - const cv = $("workspace"); - cv.addEventListener("click", (e) => { - const btn = e.target.closest(".code-copy"); - if (!btn) return; - const pre = btn.closest(".code-block")?.querySelector("pre"); - if (!pre) return; - const code = pre.textContent || ""; - const done = () => { - btn.textContent = EMRG_Copy._t("chat.copied"); - setTimeout(() => { btn.textContent = EMRG_Copy._t("chat.copyCode"); }, 1500); - }; - const fail = () => { - btn.textContent = EMRG_Copy._t("chat.copyFailed"); - setTimeout(() => { btn.textContent = EMRG_Copy._t("chat.copyCode"); }, 1500); - }; - if (navigator.clipboard?.writeText) { - navigator.clipboard.writeText(code).then(done, fail); - } else { - // 非安全上下文兜底:textarea 选中复制 - const ta = document.createElement("textarea"); - ta.value = code; - ta.style.position = "fixed"; - ta.style.opacity = "0"; - document.body.appendChild(ta); - ta.select(); - try { - document.execCommand("copy"); - done(); - } catch { - fail(); - } - document.body.removeChild(ta); - } - }); - } - initCodeCopy(); // 模块级绑定一次(boot 可重复调用,防 listener 泄漏) - - /** 追加节点到该会话聊天区并滚动 */ - function append(node, sid) { - chatContainer(sid).appendChild(node); - scrollToBottom(sid); - // rant 14:11:任何消息增删都重新评估欢迎屏显隐(此前只在切会话时评估 → 首条消息后欢迎屏不隐藏) - App.updateEmptyState?.(); - } - - function scrollToBottom(sid) { - const cv = chatContainer(sid); - cv.scrollTop = cv.scrollHeight; - } - - function clear(sid) { - const key = sid || null; - const cv = chatContainer(key); - // 清空消息节点但保留 .session-header 标题栏(app.js 每个 .session-view 顶部固定) - for (const node of [...cv.children]) { - if (node.classList && node.classList.contains("session-header")) continue; - node.remove(); - } - st(key).groupNodes.clear(); - st(key).toolRows.clear(); - st(key).doneRids.clear(); - App.updateEmptyState?.(); // rant 14:11:清空(切会话/新会话)也同步欢迎屏显隐 - } - - /** 用户消息:右对齐柔和气泡 */ - function addUserMessage(text, sid) { - const node = el("div", { class: "msg user" }, text); - append(node, sid); - return node; - } - - /** EMRG 消息:全宽 + ✦ 标识(流式节点,body 先 textContent 后整体 marked) */ - function createAssistantNode(isOwn, sid) { - const node = el("div", { class: "msg assistant" }); - if (!isOwn) { - node.appendChild(el("div", { class: "remote-label" }, EMRG_Copy._t("chat.fromOtherClient"))); - } - const mark = el("span", { class: "msg-assistant-mark" }, "✦ "); - const body = el("div", { class: "msg-body typing" }); - body.appendChild(mark); - node.appendChild(body); - append(node, sid); - return node; - } - - /** 系统消息(温和置中) */ - function addSystemMessage(text, sid) { - append(el("div", { class: "msg system" }, text), sid); - } - - /** Read-only history user message (rant 14:15:12: restore recent history on - * session switch; not interactive). Reuses the user-bubble style plus a - * history class; prepend inserts after the load bar keeping scroll pos. */ - function addHistoryMessage(text, sid, { prepend = false } = {}) { - const node = el("div", { class: "msg user history" }, text); - const cv = chatContainer(sid); - if (prepend) { - const bar = cv.querySelector(".history-load-bar"); - cv.insertBefore(node, bar ? bar.nextSibling : cv.firstChild); - } else { - cv.appendChild(node); - } - App.updateEmptyState?.(); - return node; - } - - /** Top history load bar (rant 14:15:12): text=null removes it; otherwise - * it becomes the first child of the session container. */ - function setLoadBar(sid, text) { - const cv = chatContainer(sid); - let bar = cv.querySelector(".history-load-bar"); - if (text == null) { - if (bar) bar.remove(); - return; - } - if (!bar) { - bar = el("div", { class: "history-load-bar" }); - cv.insertBefore(bar, cv.firstChild); - } - bar.textContent = text; - } - - /** 流式 delta(G122 main 已按 chunks 批量)——按会话隔离分组/已 done 集合 */ - function handleDelta(chunks, sid) { - const { groupNodes, doneRids } = st(sid); - for (const chunk of chunks) { - const rid = chunk.request_id; - if (!rid || doneRids.has(rid)) continue; // rant 14:11:已 done 的流丢弃残留 delta,不建孤儿节点 - let group = groupNodes.get(rid); - if (!group) { - const isOwn = App.state.ownStreamRequestId === rid; - const node = createAssistantNode(isOwn, sid); - group = { node, nodes: [node], hasText: false, sealed: false }; - groupNodes.set(rid, group); - } else if (group.sealed) { - // rant 21:57:10:上一文本段被工具行"封存"→ 新文本段开新节点(旧节点保留在 DOM 原位) - const isOwn = App.state.ownStreamRequestId === rid; - const node = createAssistantNode(isOwn, sid); - group.node = node; - group.nodes.push(node); - group.sealed = false; - } - const content = chunk.content || ""; - if (content) group.hasText = true; - const body = group.node.querySelector(".msg-body") || group.node; - // rant 21:00:28:流式 markdown(块投影)——稳定块完整渲染并缓存 DOM(不闪烁/不打断选中), - // 尾部 live 块只渲染已稳定部分;代码围栏未闭合 → 纯文本不高亮(TUI fence_count%2 启发式一致)。 - // 无 marked.lexer 或投影异常 → 回退既有纯文本追加(done 时整体渲染)。 - const stream = group.node.__stream || (group.node.__stream = { stableCount: 0, container: null, live: null, rawText: "" }); - const raw = stream.rawText + content; - if (!window.emrgMarkdown.streamProject(body, raw, stream)) { - body.textContent += content; - } - scrollToBottom(sid); - } - } - - /** 取消/错误收尾:移除该会话所有在途节点的 typing 光标(cancelled 事件无 request_id,只能全清) */ - function clearTyping(sid) { - for (const group of st(sid).groupNodes.values()) { - for (const node of group.nodes) { - const body = node.querySelector(".msg-body") || node; - body.classList.remove("typing"); - } - } - } - - /** done:整体 Markdown 渲染(requestIdleCallback 调度,G127)——该 rid 的全部文本段逐个渲染 */ - function handleDone(data, sid) { - const { groupNodes, doneRids } = st(sid); - const rid = data.request_id; - if (rid) { - doneRids.add(rid); - if (doneRids.size > 500) doneRids.clear(); // UUID 不复用,超限即清防长期运行增长 - const group = groupNodes.get(rid); - if (group) { - for (const node of group.nodes) { - const body = node.querySelector(".msg-body") || node; - body.classList.remove("typing"); - // rant 21:00:28:流式投影结束 → live 块转 full 一次性校正(与旧 done 渲染同源 renderMarkdown) - const stream = node.__stream; - if (stream && stream.container) { - const render = () => { - window.emrgMarkdown.streamFinalize(body, stream.rawText).then(() => scrollToBottom(sid)); - }; - if (window.requestIdleCallback) { - window.requestIdleCallback(render, { timeout: 2000 }); - } else { - render(); - } - continue; - } - // 非流式路径(既有):✦ 标记是元素而非文本——流式时 body.textContent 含 "✦ " 前缀, - // 直接整体 render 会让 "✦ # Title" 等块语法(标题/列表/代码围栏)解析失败 - // (前缀不在行首 → marked 不识别)。渲染前剥离前缀,渲染后重新插入标记保持视觉一致。 - const text = body.textContent.replace(/^✦\s*/, ""); - const render = () => { - window.emrgMarkdown.renderMarkdown(text).then((html) => { - body.innerHTML = html; - body.insertBefore(el("span", { class: "msg-assistant-mark" }, "✦ "), body.firstChild); - scrollToBottom(sid); - }); - }; - if (window.requestIdleCallback) { - window.requestIdleCallback(render, { timeout: 2000 }); - } else { - render(); - } - } - groupNodes.delete(rid); - } - } - if (data.timeout) { - addSystemMessage(EMRG_Copy._t("chat.timeoutWarn"), sid); - } - // 工具调用次数上限中断(跨项目教训:截断的工作不提示 = 用户拿半成品) - // 对齐 TUI:明确提示结果可能不完整 + 可继续(TUI 已有 "Try '继续' to resume") - if (data.content && /exceeded/i.test(data.content) && /max|limit|round/i.test(data.content)) { - addSystemMessage(EMRG_Copy._t("chat.maxRoundsHint"), sid); - } - } - - /** rant 21:28:49:工具合并组 —— 组内工具行容器 + 状态 */ - function updateToolGroup(group, sid) { - const rowsEl = group.querySelector(".tool-group-rows"); - const bar = group.querySelector(".tool-group-bar"); - if (!rowsEl || !bar) return; - const rows = rowsEl.children.filter((c) => c.classList && c.classList.contains("tool-row")); - const running = rows.some((r) => r.classList.contains("running")); - const userExpanded = group.dataset.userExpanded === "1"; - if (rows.length >= 2 && !running) { - // ≥2 行全部完成 → bar 显示 + 摘要(数量 + 总耗时);非手动展开则收起 - const total = rows.reduce((acc, r) => acc + (parseFloat(r.dataset.elapsed) || 0), 0); - const summary = bar.querySelector(".tool-group-summary"); - if (summary) { - summary.textContent = EMRG_Copy._t("chat.toolGroupSummary", { - count: rows.length, - time: total.toFixed(1) + "s", - }); - } - bar.classList.remove("hidden"); - group.classList.toggle("collapsed", !userExpanded); - } else { - // 组内仅 1 行 或 存在 running 行 → bar 隐藏、rows 展开(进行中工具实时可见) - bar.classList.add("hidden"); - group.classList.remove("collapsed"); - } - } - - /** 工具友好状态行(进行中 → 完成/失败,默认折叠,点开展示原始输出) */ - function handleToolStart(data, sid) { - const { groupNodes, toolRows } = st(sid); - const rid = data.request_id; - if (rid) { - let group = groupNodes.get(rid); - if (!group) { - // G104:tool_start 也建组(LLM 先出 tool_calls 后出文本) - const isOwn = App.state.ownStreamRequestId === rid; - const node = createAssistantNode(isOwn, sid); - group = { node, nodes: [node], hasText: false, sealed: false }; - groupNodes.set(rid, group); - } else if (group.hasText) { - // rant 21:57:10:已有文本段之后来了工具 → 封存当前段,后续 delta 新建段(保持 TUI 交错顺序) - group.sealed = true; - // rant 21:09:已结束的文本段不再闪烁——封存时移除其 typing 光标, - // 让 ▍ 只保留在最新一段文本后面("前面的文本都结束了还显示闪烁光标")。 - const prevBody = group.node.querySelector(".msg-body") || group.node; - prevBody.classList.remove("typing"); - } - } - const phrases = EMRG_Copy.toolPhrases(data.tool_name); - const row = el("div", { class: "tool-row running" }); - row.appendChild(el("span", { class: "tool-spinner" })); - const label = el("span", { class: "tool-label" }, phrases.doing); - row.appendChild(label); - // Rant 2026-08-19T10:35:24: show the agent's intent (why this call - // happened) as a human-readable purpose instead of raw argument JSON. - if (data.intent) { - row.appendChild(el("span", { class: "tool-intent" }, data.intent)); - } - row.appendChild(el("span", { class: "tool-chevron" }, "⌄")); - // 点展开 → 显示原始输出(等宽字体/圆角浅底) - row.addEventListener("click", () => { - const out = row.querySelector(".tool-output"); - if (out) { - out.classList.toggle("hidden"); - row.classList.toggle("expanded", !out.classList.contains("hidden")); - } - }); - - // rant 21:28:49:连续工具合并 —— 会话容器最后一个子节点判定 - // 1) 前一工具已完成(.tool-row:not(.running))→ 新建 .tool-group,把旧行移入 rows,新行也入 rows - // 2) 已是 .tool-group → 新行直接入其 rows(组收起时新工具 start 自动展开显示 running) - // 3) 其他(文本/用户消息/无节点)→ 独立 .tool-row(现状不变;文本穿插不合并) - const cv = chatContainer(sid); - const last = cv.children.length ? cv.children[cv.children.length - 1] : null; - let appendedToGroup = false; - if (last && last.classList) { - if (last.classList.contains("tool-row") && !last.classList.contains("running")) { - const rowsEl = el("div", { class: "tool-group-rows" }); - const bar = el("div", { class: "tool-group-bar hidden" }); - bar.appendChild(el("span", { class: "tool-group-chev" }, "⌄")); - bar.appendChild(el("span", { class: "tool-group-summary" }, "")); - const grp = el("div", { class: "tool-group" }); - grp.appendChild(bar); - grp.appendChild(rowsEl); - // bar 点击 → 展开/收起 rows + chevron 旋转 + 组标 user-expanded(手动展开后不再自动收起) - bar.addEventListener("click", () => { - const wasCollapsed = grp.classList.contains("collapsed"); - if (wasCollapsed) { - grp.classList.remove("collapsed"); - grp.dataset.userExpanded = "1"; - } else { - grp.classList.add("collapsed"); - delete grp.dataset.userExpanded; // 手动收起后新工具完成仍自动收起(rule 5) - } - }); - // 移旧行入组(真实 DOM appendChild 即移动;mock 需先 remove 防残留) - if (last.parentNode) last.remove(); - rowsEl.appendChild(last); - rowsEl.appendChild(row); - cv.appendChild(grp); - appendedToGroup = true; - } else if (last.classList.contains("tool-group")) { - const rowsEl = last.querySelector(".tool-group-rows"); - if (rowsEl) { - rowsEl.appendChild(row); - last.classList.remove("collapsed"); // 组收起后新工具 start → 自动展开显示 running 行 - appendedToGroup = true; - } - } - } - if (appendedToGroup) { - scrollToBottom(sid); - App.updateEmptyState?.(); - const rowsEl = row.parentNode; - if (rowsEl && rowsEl.classList.contains("tool-group-rows")) { - updateToolGroup(rowsEl.parentNode, sid); - } - } else { - append(row, sid); - } - toolRows.set(data.tool_call_id, row); - } - - function handleToolEnd(data, sid) { - const row = st(sid).toolRows.get(data.tool_call_id); - if (!row) return; - // rant 21:08:工具执行完成后 spinner 必须停止——移除转圈元素(CSS 亦有 - // .tool-row:not(.running) 隐藏兜底),只保留 ✓ 完成标记,防止"对号前面一直转圈"。 - row.querySelector(".tool-spinner")?.remove(); - const ok = !data.error; - row.classList.remove("running"); - row.classList.add(ok ? "done" : "failed"); - const phrases = EMRG_Copy.toolPhrases(data.tool_name); - const label = row.querySelector(".tool-label"); - if (label) { - if (ok) { - const check = el("span", { class: "tool-check" }, "✓ "); - row.insertBefore(check, label); - label.textContent = phrases.done; - } else { - label.textContent = EMRG_Copy.TOOL_FAIL_TEXT; - } - } - const elapsed = data.elapsed !== undefined ? `${data.elapsed.toFixed(1)}s` : ""; - if (elapsed && ok) { - row.title = EMRG_Copy._t("chat.elapsed", { s: elapsed }); - // rant 21:28:49:工具耗时可见 —— label 后 · 3.2s(失败不加) - const time = el("span", { class: "tool-time" }, `· ${elapsed}`); - if (label) label.after(time); - row.dataset.elapsed = String(data.elapsed); // 供合并组摘要求和 - } - // G91/G131:content 截断 2000 字符 + 展开全文 - const content = data.content || ""; - if (content) { - const truncated = content.length > 2000 ? content.slice(0, 2000) + "…" : content; - const out = el("div", { class: "tool-output hidden" }, truncated); - row.appendChild(out); - if (content.length > 2000) { - const btn = el("button", { class: "tool-expand-btn" }, EMRG_Copy._t("chat.expand")); - btn.addEventListener("click", () => { - out.textContent = content; - btn.remove(); - }); - row.appendChild(btn); - } - } - scrollToBottom(sid); - // rant 21:28:49:该行在合并组内 → 更新组摘要(数量 + 总耗时)与收起状态 - const rowsEl = row.parentNode; - if (rowsEl && rowsEl.classList && rowsEl.classList.contains("tool-group-rows")) { - updateToolGroup(rowsEl.parentNode, sid); - } - } - - return { - addUserMessage, - addSystemMessage, - createAssistantNode, - addHistoryMessage, - setLoadBar, - clear, - scrollToBottom, - handleDelta, - handleDone, - handleToolStart, - handleToolEnd, - clearTyping, - registerContainer, - unregisterContainer, - hasContainer, - chatContainer, - // P3:会话级状态访问器(sid=null → 旧版单会话桶,兼容既有调用方) - groupNodesFor: (sid) => st(sid).groupNodes, - toolRowsFor: (sid) => st(sid).toolRows, - get groupNodes() { - return st(null).groupNodes; - }, - get toolRows() { - return st(null).toolRows; - }, - }; -})(); - -window.EMRG_Chat = Chat; diff --git a/emrg/gui/renderer/js/commands.js b/emrg/gui/renderer/js/commands.js deleted file mode 100644 index 4c852521..00000000 --- a/emrg/gui/renderer/js/commands.js +++ /dev/null @@ -1,68 +0,0 @@ -"use strict"; -/** - * commands.js — GUI / 指令注册表与解析器(rant 19:44 P1:解析器 + 补全 + 纯操作) - * 对齐 TUI 15 个 / 指令。指令解析在 GUI 侧(renderer),daemon 零改动(协议已存在)。 - * - * 阶段(分期): - * phase 1 — 纯操作类(/clear /compact /version /help /image 提示)→ 已实现 - * phase 2 — 会话类(/delete /rename /resume /rewind /sessions)→ 已实现 - * phase 3 — 模型/记忆/技能类(/model /memory /skills)→ 已实现 - * phase 4 — 演化类(/rant /trigger)→ 已实现(全部 15 指令完成) - */ - -const Commands = (() => { - // 指令注册表:cmd → { hint, phase } - // hint 为 i18n 词典键(rant 21:19):显示时经 t() 解析为当前语言文案 - const COMMANDS = { - "/clear": { hint: "cmd.clear.hint", phase: 1 }, - "/compact": { hint: "cmd.compact.hint", phase: 1 }, - "/version": { hint: "cmd.version.hint", phase: 1 }, - "/help": { hint: "cmd.help.hint", phase: 1 }, - "/image": { hint: "cmd.image.hint", phase: 1 }, - "/delete": { hint: "cmd.delete.hint", phase: 2 }, - "/rename": { hint: "cmd.rename.hint", phase: 2 }, - "/resume": { hint: "cmd.resume.hint", phase: 2 }, - "/rewind": { hint: "cmd.rewind.hint", phase: 2 }, - "/sessions": { hint: "cmd.sessions.hint", phase: 2 }, - "/open": { hint: "cmd.open.hint", phase: 4 }, // P5:打开会话(跨项目) - "/model": { hint: "cmd.model.hint", phase: 3 }, - "/memory": { hint: "cmd.memory.hint", phase: 3 }, - "/skills": { hint: "cmd.skills.hint", phase: 3 }, - "/rant": { hint: "cmd.rant.hint", phase: 4 }, - "/trigger": { hint: "cmd.trigger.hint", phase: 4 }, - }; - - /** - * 解析输入:/ 开头且匹配注册表 → { type:"command", cmd, args }; - * / 开头未匹配 → { type:"unknown", cmd };否则 { type:"message" }。 - */ - function parseInput(text) { - const t = String(text || "").trim(); - if (!t.startsWith("/")) return { type: "message" }; - const [cmd, ...args] = t.split(/\s+/); - const key = cmd.toLowerCase(); - if (COMMANDS[key]) return { type: "command", cmd: key, args }; - return { type: "unknown", cmd: key }; - } - - /** 解析 i18n hint:词典键 → 当前语言文案 */ - function hintText(cmd) { - const meta = COMMANDS[cmd]; - if (!meta) return ""; - try { - return window.EMRG_I18N ? window.EMRG_I18N.t(meta.hint) : meta.hint; - } catch { return meta.hint; } - } - - /** / 前缀补全:返回匹配的指令(含 hint),供补全菜单过滤 */ - function getCompletions(prefix) { - const p = String(prefix || "").toLowerCase(); - return Object.entries(COMMANDS) - .filter(([cmd]) => cmd.startsWith(p)) - .map(([cmd, meta]) => ({ cmd, hint: hintText(cmd), phase: meta.phase })); - } - - return { COMMANDS, parseInput, getCompletions, hintText }; -})(); - -window.EMRG_Commands = Commands; diff --git a/emrg/gui/renderer/js/copywriting.js b/emrg/gui/renderer/js/copywriting.js deleted file mode 100644 index d9e33fb1..00000000 --- a/emrg/gui/renderer/js/copywriting.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; -/** - * copywriting.js — 去黑话文案映射(非开发者友好,rant 21:19 起经 i18n 词典取词)。 - * 工具名 → 友好的动词短语;系统级状态 → 口语化鼓励性文案。 - * refresh():locale 切换后重建 COPY(i18n.apply 会调用)。 - * 注意:EMRG_Copy 需在 i18n.js 之后加载(引用 EMRG_I18N.t)。 - */ - -const EMRG_Copy = (() => { - const _t = (key, params) => { - try { - return window.EMRG_I18N ? window.EMRG_I18N.t(key, params) : key; - } catch { return key; } - }; - - /** 工具名 → 进行中 / 完成 短语(渐进披露:默认折叠,点开展示原始输出) */ - function toolPhrases(name) { - const base = name || "fallback"; - return { - doing: _t(`tool.${base}.doing`), - done: _t(`tool.${base}.done`), - }; - } - - /** 系统状态文案(locale 敏感:切换语言后 refresh 重建;含 #501 P3 成长卡/toast 键) */ - function buildCopy() { - return { - disconnected: _t("copy.disconnected"), - reconnected: _t("copy.reconnected"), - sessionBusy: _t("copy.sessionBusy"), - sendFailed: _t("copy.sendFailed"), - deleteConfirmTitle: _t("copy.deleteConfirmTitle"), - deleteConfirmBody: _t("copy.deleteConfirmBody"), - noSessions: _t("copy.noSessions"), - aboutEvolution: (n) => - n ? _t("copy.aboutEvolution", { n }) : _t("copy.aboutEvolutionEmpty"), - // WorkBuddy P3(#501):成长卡 + 进化 toast - growthCount: (n) => _t("copy.growthCount", { n }), - growthNote: _t("copy.growthNote"), - evolutionToastTitle: _t("copy.evolutionToastTitle"), - evolutionToastMsg: (n) => _t("copy.evolutionToastMsg", { n }), - evolutionToastSee: _t("copy.evolutionToastSee"), - evolutionToastDismiss: _t("copy.evolutionToastDismiss"), - }; - } - let COPY = buildCopy(); - let failText = _t("tool.failText"); // 工具失败文案(字符串属性,chat.js 直接赋值用) - - function refresh() { - COPY = buildCopy(); - failText = _t("tool.failText"); - } - - return { - toolPhrases, - COPY, - refresh, - _t, // rant 21:19 Stage 2:chat.js 等模块取词统一走这里(i18n 就绪时) - get TOOL_FAIL_TEXT() { return failText; }, - }; -})(); - -window.EMRG_Copy = EMRG_Copy; diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js deleted file mode 100644 index 1868770d..00000000 --- a/emrg/gui/renderer/js/dialogs.js +++ /dev/null @@ -1,1689 +0,0 @@ -"use strict"; -/** - * dialogs.js — 设置 / 首启引导 / 确认对话框。 - * - 设置:模型服务(API Key/接口地址/可用模型增删改设默认)、工作目录、外观(主题三选) - * - 多模型管理:可用模型列表(默认项 + [[llm.models]] 全部条目),+添加/行内编辑/删除/单选设默认 - * - 首启:品牌欢迎语 + 口语化文案 - * - 确认:替代 confirm()(友好文案,不显示 session ID) - */ - -const Dialogs = (() => { - // ── 主题 ───────────────────────────────── - let theme = "system"; - - function renderThemeOptions() { - const wrap = $("theme-options"); - if (!wrap) return; - for (const btn of wrap.querySelectorAll(".theme-option")) { - btn.classList.toggle("active", btn.dataset.theme === theme); - } - } - - function initThemeButtons() { - const wrap = $("theme-options"); - if (!wrap) return; - wrap.addEventListener("click", (e) => { - const btn = e.target.closest(".theme-option"); - if (!btn) return; - theme = btn.dataset.theme || "system"; - applyTheme(theme); - syncTemplateEditorTheme(); // Monaco vs/vs-dark 跟随(rant 09:17:45 验收 6) - renderThemeOptions(); - }); - } - - // ── 语言(rant 21:19:i18n 手动覆盖,即时生效)── - function renderLangOptions() { - const wrap = $("lang-options"); - if (!wrap) return; - // 原始 localStorage 值:"" = 跟随系统(高亮第一项) - let saved = ""; - try { - if (typeof localStorage !== "undefined") saved = localStorage.getItem("emrg.locale") || ""; - } catch { /* ignore */ } - for (const btn of wrap.querySelectorAll(".theme-option")) { - btn.classList.toggle("active", (btn.dataset.lang || "") === saved); - } - } - - function initLangButtons() { - const wrap = $("lang-options"); - if (!wrap) return; - wrap.addEventListener("click", (e) => { - const btn = e.target.closest(".theme-option"); - if (!btn) return; - const loc = btn.dataset.lang || ""; - if (window.EMRG_I18N) window.EMRG_I18N.setLocale(loc); // 立即重刷全界面 - renderLangOptions(); - }); - } - - // ── 多模型管理 ─────────────────────────── - // defaultName:当前默认模型(llm.model);extraModels:[[llm.models]] 条目(含与默认同名的配置项) - let defaultName = ""; - let extraModels = []; - - function renderModelList() { - const list = $("model-list"); - list.innerHTML = ""; - // 默认行(vision/modelId 从同名 extraModels 条目取,编辑默认模型时保留) - const def = extraModels.find((m) => m.name === defaultName); - list.appendChild( - renderModelItem( - defaultName || _t("dlg.notConfigured"), - true, - Boolean(def?.vision), - def?.model && def.model !== def.name ? def.model : "" - ) - ); - // 额外模型行(去重:跳过与默认同名的——默认行已展示) - const seen = new Set([defaultName]); - for (const m of extraModels) { - if (seen.has(m.name)) continue; - seen.add(m.name); - list.appendChild( - renderModelItem(m.name, false, Boolean(m.vision), m.model && m.model !== m.name ? m.model : "") - ); - } - if (extraModels.filter((m) => m.name !== defaultName).length === 0) { - const hint = el("div", { class: "model-list-empty" }, _t("dlg.noOtherModels")); - list.appendChild(hint); - } - } - - function renderModelItem(name, isDefault, vision, modelId) { - const row = el("div", { class: "model-item" + (isDefault ? " default" : "") }); - // 单选圆点 → 设为默认 - const radio = el("button", { class: "model-radio" + (isDefault ? " checked" : ""), title: isDefault ? _t("dlg.currentDefault") : _t("dlg.setDefault") }); - if (isDefault) { - radio.textContent = "●"; - } else { - radio.textContent = "○"; - radio.addEventListener("click", () => { - defaultName = name; - renderModelList(); - }); - } - row.appendChild(radio); - const title = el("span", { class: "model-name" }, name); - if (isDefault) title.appendChild(el("span", { class: "model-badge" }, _t("dlg.defaultBadge"))); - row.appendChild(title); - if (modelId) row.appendChild(el("span", { class: "model-id" }, modelId)); - if (vision) row.appendChild(el("span", { class: "model-vision" }, _t("dlg.supportsImages"))); - // 操作:编辑(非默认行 → 打开表单填本条);删除(默认不可删) - const actions = el("span", { class: "model-actions" }); - const editBtn = el("button", { class: "model-action-btn", title: _t("dlg.edit") }, _t("dlg.edit")); - editBtn.addEventListener("click", () => openModelForm(name, isDefault)); - actions.appendChild(editBtn); - if (!isDefault) { - const delBtn = el("button", { class: "model-action-btn danger", title: _t("dlg.delete") }, _t("dlg.delete")); - delBtn.addEventListener("click", () => { - showConfirm(_t("dlg.deleteModelTitle"), _t("dlg.deleteModelBody", { name }), { - okText: _t("dlg.delete"), - danger: true, - onOk: () => { - extraModels = extraModels.filter((m) => m.name !== name); - renderModelList(); - }, - }); - }); - actions.appendChild(delBtn); - } - row.appendChild(actions); - return row; - } - - // 行内表单:添加(无参)或编辑(name 非空) - let editingName = null; - function openModelForm(name, isDefault = false) { - editingName = isDefault ? (name || null) : name; - $("model-form-name").value = name || ""; - // 编辑时预填 model/vision - const found = extraModels.find((m) => m.name === name); - $("model-form-id").value = found?.model && found.model !== found.name ? found.model : ""; - $("model-form-vision").checked = Boolean(found?.vision); - $("model-form").classList.remove("hidden"); - $("model-form-name").focus(); - } - - function closeModelForm() { - $("model-form").classList.add("hidden"); - editingName = null; - $("model-form-name").value = ""; - $("model-form-id").value = ""; - $("model-form-vision").checked = false; - } - - function saveModelForm() { - const name = $("model-form-name").value.trim(); - if (!name) { - showConfirm(_t("dlg.stepTitle"), _t("dlg.nameRequiredBody"), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - const modelId = $("model-form-id").value.trim() || name; - const vision = $("model-form-vision").checked; - - if (editingName === null) { - // 添加新模型 - if (name === defaultName || extraModels.some((m) => m.name === name)) { - showConfirm(_t("dlg.duplicateTitle"), _t("dlg.duplicateBody"), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - extraModels.push({ name, model: modelId, vision }); - } else if (editingName === defaultName) { - // 编辑默认模型:更新默认名 + 其配置项(保留在 extraModels 中) - const prev = extraModels.find((m) => m.name === editingName); - extraModels = extraModels.filter((m) => m.name !== editingName); - extraModels.push({ name, model: modelId, vision, ...(prev ? {} : {}) }); - defaultName = name; - } else { - // 编辑非默认模型 - if (name === defaultName) { - showConfirm(_t("dlg.duplicateTitle"), _t("dlg.duplicateDefaultBody"), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - if (name !== editingName && extraModels.some((m) => m.name === name)) { - showConfirm(_t("dlg.duplicateTitle"), _t("dlg.duplicateBody"), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - extraModels = extraModels.map((m) => (m.name === editingName ? { name, model: modelId, vision } : m)); - } - closeModelForm(); - renderModelList(); - } - - function initModelForm() { - $("add-model-btn").addEventListener("click", () => openModelForm("")); - $("model-form-save").addEventListener("click", saveModelForm); - $("model-form-cancel").addEventListener("click", closeModelForm); - // 键盘:Enter 保存 / ESC 取消(与重命名对话框一致的交互) - $("model-form-name").addEventListener("keydown", (e) => { - if (e.key === "Enter") { e.preventDefault(); saveModelForm(); } - else if (e.key === "Escape") { e.preventDefault(); closeModelForm(); } - }); - $("model-form-id").addEventListener("keydown", (e) => { - if (e.key === "Enter") { e.preventDefault(); saveModelForm(); } - else if (e.key === "Escape") { e.preventDefault(); closeModelForm(); } - }); - } - - // ── 设置 ───────────────────────────────── - async function showSettings() { - try { - const s = await window.emrg.getSettings(); - $("set-api-key").value = s.apiKey || ""; - $("set-base-url").value = s.baseUrl || ""; - if (s.theme) theme = s.theme; - renderThemeOptions(); - renderLangOptions(); // rant 21:19:语言选择器高亮当前值 - defaultName = s.model || ""; - extraModels = (s.modelDetails || []).map((m) => ({ - name: m.name || "", - model: m.model || m.name || "", - vision: Boolean(m.vision), - })); - renderModelList(); - // WorkBuddy P3:关于区(版本 + 进化计数)随设置面板打开时刷新 - try { - const ver = $("about-version"); - if (ver) ver.textContent = `v${App.state.version || "0.2.8"}`; - const evo = $("about-evolutions"); - if (evo) evo.textContent = `🌱 ${_t("copy.growthCountPrefix")} ${App.state.evolutionCount ?? 0} ${_t("copy.times")}`; - } catch { /* 元素缺失(测试桩)时忽略 */ } - // Windows GCM rant Stage 2:GitHub 连接状态随设置面板打开时刷新 - await refreshGithubStatus(); - } catch (e) { - Chat.addSystemMessage(_t("settings.readFailed", { msg: e.message })); - } - // rant 18:55:09 v0.2:设置改为工作区视图(不再弹窗);已在视图内(nav 打开后刷新)则不再 toggle - if (App.state.activeView !== "settings") App.switchView("settings"); - } - - async function saveSettings() { - const config = { - apiKey: $("set-api-key").value.trim(), - baseUrl: $("set-base-url").value.trim(), - model: defaultName, - theme, - models: extraModels, - }; - if (!config.apiKey) { - showConfirm(_t("dlg.stepTitle"), _t("app.authKeyRequired"), { - okText: _t("dlg.gotIt"), - danger: false, - }); - return; - } - try { - await window.emrg.saveSettings(config); - Chat.addSystemMessage(_t("dlg.saved")); - App.state.apiKeyConfigured = true; - App.state.model = defaultName; - App.updateModelSwitcher(); - await App.boot(); - } catch (e) { - Chat.addSystemMessage(_t("settings.saveFailed", { msg: e.message })); - } - } - - // ── 首启引导 ───────────────────────────── - function showWelcome() { - $("welcome-api-key").value = ""; - $("welcome-base-url").value = ""; - $("welcome-model").innerHTML = ``; - $("welcome-dialog").showModal(); - window.emrg.getSettings().then((s) => { - const sel = $("welcome-model"); - sel.innerHTML = ""; - const models = s.modelDetails?.length - ? s.modelDetails.map((m) => m.name) - : s.models?.length - ? s.models - : ["deepseek-chat", "deepseek-v3", "gpt-4o"]; - for (const m of models) { - const opt = el("option", { value: m }, m); - if (m === s.model) opt.selected = true; // G144 - sel.appendChild(opt); - } - if (sel.selectedIndex < 0) sel.selectedIndex = 0; - }); - } - - async function saveWelcome() { - const key = $("welcome-api-key").value.trim(); - if (!key) { - showConfirm(_t("dlg.stepTitle"), _t("app.authKeyRequired"), { - okText: _t("dlg.gotIt"), - danger: false, - }); - return; - } - const config = { - apiKey: key, - baseUrl: $("welcome-base-url").value.trim(), - model: $("welcome-model").value || "deepseek-chat", - }; - try { - await window.emrg.saveSettings(config); - if ($("welcome-dialog").open) $("welcome-dialog").close(); - Chat.addSystemMessage(_t("dlg.starting")); - App.state.apiKeyConfigured = true; - await App.boot(); - } catch (e) { - Chat.addSystemMessage(_t("dlg.initFailed", { msg: e.message })); - } - } - - // ── 重命名对话(右键菜单 → 重命名) ──── - let renameSid = null; - function showRename(sid, currentTitle) { - renameSid = sid; - const input = $("rename-input"); - input.value = (currentTitle && currentTitle !== sid) ? currentTitle : ""; - $("rename-dialog").showModal(); - input.focus(); - input.select(); - } - - async function submitRename() { - if (!renameSid) return; - const input = $("rename-input"); - const title = input.value.trim(); - if (!title) return; // 空名不提交 - try { - await window.emrg.renameSession({ sessionId: renameSid, title }); - renameSid = null; - $("rename-dialog").close(); - await App.refreshSessions(); - } catch (e) { - Chat.addSystemMessage(_t("dlg.renameFailed", { msg: e.message })); - } - } - - function initRenameDialog() { - $("rename-cancel").addEventListener("click", () => { - renameSid = null; - $("rename-dialog").close(); - }); - $("rename-ok").addEventListener("click", submitRename); - $("rename-input").addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - submitRename(); - } - }); - } - - // ── GitHub 连接(Windows GCM rant Stage 2:设置页授权) ──── - function initGithubSection() { - $("github-connect-btn").addEventListener("click", async () => { - const token = $("set-github-token").value.trim(); - if (!token) { - // 首选 device flow(不开终端、不弹 GCM);PAT 为受限环境兜底 - await startDeviceFlow(); - return; - } - $("github-connect-btn").disabled = true; - $("github-connect-btn").textContent = _t("settings.githubConnecting"); - try { - const res = await window.emrg.githubConnect({ token }); - if (res && res.ok) { - $("set-github-token").value = ""; - Chat.addSystemMessage(_t("settings.githubConnected", { user: res.user || "" })); - await refreshGithubStatus(); - } else { - Chat.addSystemMessage(_t("settings.githubConnectFailed", { msg: (res && res.error) || _t("app.unknownError") })); - } - } catch (e) { - Chat.addSystemMessage(_t("settings.githubConnectFailed", { msg: e.message })); - } finally { - $("github-connect-btn").disabled = false; - $("github-connect-btn").textContent = _t("settings.githubConnect"); - } - }); - $("github-disconnect-btn").addEventListener("click", async () => { - try { - const res = await window.emrg.githubDisconnect(); - if (res && res.ok) { - Chat.addSystemMessage(_t("settings.githubDisconnected")); - await refreshGithubStatus(); - } else { - Chat.addSystemMessage(_t("settings.githubDisconnectFailed", { msg: (res && res.error) || _t("app.unknownError") })); - } - } catch (e) { - Chat.addSystemMessage(_t("settings.githubDisconnectFailed", { msg: e.message })); - } - }); - } - - // ── GitHub device flow(Stage 2b:gh auth login --web) ──── - let _devicePollTimer = null; - function stopDevicePolling() { - if (_devicePollTimer) { - clearInterval(_devicePollTimer); - _devicePollTimer = null; - } - } - - async function startDeviceFlow() { - const dlg = $("github-device-dialog"); - const codeEl = $("github-device-code"); - if (!dlg || !codeEl) return; // 元素缺失(测试桩)时忽略 - stopDevicePolling(); - codeEl.textContent = "…"; - dlg.showModal(); - try { - const res = await window.emrg.githubConnectWeb(); - if (!res || !res.ok) { - dlg.close(); - Chat.addSystemMessage(_t("settings.githubDeviceFailed", { msg: (res && res.error) || _t("app.unknownError") })); - return; - } - if (res.code && res.url) { - codeEl.textContent = res.code; - const openBtn = $("github-device-open"); - if (openBtn) { - openBtn.onclick = () => window.emrg.openExternal({ url: res.url }); - } - } else { - // already authenticated - dlg.close(); - Chat.addSystemMessage(_t("settings.githubConnected", { user: res.user || "" })); - await refreshGithubStatus(); - return; - } - } catch (e) { - dlg.close(); - Chat.addSystemMessage(_t("settings.githubDeviceFailed", { msg: e.message })); - return; - } - // 轮询 github_status 直到授权完成(daemon 侧 300s 超时兜底) - _devicePollTimer = setInterval(async () => { - try { - const s = await window.emrg.githubStatus(); - if (s && s.authenticated) { - stopDevicePolling(); - if (dlg.open) dlg.close(); - Chat.addSystemMessage(_t("settings.githubConnected", { user: s.user || "" })); - await refreshGithubStatus(); - } - } catch { /* 网络抖动忽略,下一轮再试 */ } - }, 3000); - } - - function initDeviceDialog() { - const dlg = $("github-device-dialog"); - if (!dlg) return; - $("github-device-close").addEventListener("click", () => { - stopDevicePolling(); - dlg.close(); - }); - } - - async function refreshGithubStatus() { - const statusEl = $("github-status"); - const authRow = $("github-auth-row"); - if (!statusEl || !authRow) return; // 元素缺失(测试桩)时忽略 - statusEl.textContent = _t("settings.githubChecking"); - try { - const s = await window.emrg.githubStatus(); - const connected = Boolean(s && s.authenticated); - const user = (s && s.user) || ""; - statusEl.textContent = connected - ? _t("settings.githubConnectedStatus", { user }) - : _t("settings.githubNotConnected"); - $("github-disconnect-btn").classList.toggle("hidden", !connected); - $("set-github-token").classList.toggle("hidden", connected); - $("github-connect-btn").classList.toggle("hidden", connected); - } catch { - statusEl.textContent = _t("settings.githubStatusFailed"); - } - } - - - // ── 定时任务管理(rant 2026-08-12T18:23:15 P3:GUI 任务/自定义类型 CRUD) ── - // 决策点执行:①内置类型只读 ②被任务引用的自定义类型拒绝删除 ③项目仅限已注册 - // ④热重载(daemon apply_tasks)⑤间隔 ≥60 ⑥内置模板只读。 - let taskTypes = []; // [{name, builtin}] — 类型下拉选项(内置 + 自定义) - let taskProjects = []; // listProjects 结果 — 项目下拉仅已注册项目(决策点③) - let editingTask = null; // 正在编辑的任务名(null = 新增;daemon 以 name 定位 → 名称不可改) - let editingTemplate = null; // 正在编辑的自定义类型名(null = 新增;名称不可改) - // rant 2026-08-15T10:36:39:任务状态 + 下次运行倒计时 - // 倒计时以"渲染时快照的 deadline"为基准每秒递减(直接改 DOM 文本,不重渲染整行防闪烁/滚动丢失) - let taskCountdownTimer = null; // setInterval id(面板激活时启动,离开视图时清除防泄漏) - let taskCountdowns = []; // [{name, deadline, span} | {name, startedAt, span}] — 每 1s 由 updateTaskCountdowns 更新 - // rant 2026-08-22T07:18:35:面板激活时每 5s 轮询任务状态(loadTaskMeta + renderTaskList), - // 让"运行中→待运行"切换和下次倒计时 ≤5s 内自动更新(宿主确认轮询方案,不引入事件推送)。 - let taskPollTimer = null; // setInterval id(任务面板激活时启动,离开视图时清除防泄漏) - - // 倒计时格式化:≤60s "43s";≤1h "1m23s";>1h "1h05m";负数/非数钳制为 0 - function formatCountdown(totalSeconds) { - const s = Math.max(0, Math.floor(Number(totalSeconds) || 0)); - if (s < 60) return `${s}s`; - if (s < 3600) { - const m = Math.floor(s / 60); - const sec = s % 60; - return `${m}m${String(sec).padStart(2, "0")}s`; - } - const h = Math.floor(s / 3600); - const m = Math.floor((s % 3600) / 60); - return `${h}h${String(m).padStart(2, "0")}m`; - } - - // rant 2026-08-18T10:45:52:相对时间显示("5m ago" / "3h ago" / 日期)—— - // 用于任务行"上次运行"元信息。ISO 时间戳解析失败时回退为原文。 - function formatRelativeTime(isoStr) { - if (!isoStr) return ""; - const t = Date.parse(isoStr); - if (Number.isNaN(t)) return String(isoStr); - const diffSec = Math.max(0, Math.floor((Date.now() - t) / 1000)); - if (diffSec < 60) return `${diffSec}s ago`; - if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`; - if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`; - return `${Math.floor(diffSec / 86400)}d ago`; - } - - function updateTaskCountdowns() { - let expired = false; - for (const e of taskCountdowns) { - // rant 2026-08-22T07:18:35:running 时长与待运行倒计时同机制、反向—— - // startedAt 固定、now 递增即时长(elapsed);deadline 固定、now 递增则剩余递减。 - if (e.startedAt != null) { - const el = Math.max(0, Math.floor((Date.now() - e.startedAt) / 1000)); - e.span.textContent = _t("app.taskRunningDuration", { n: formatCountdown(el) }); - continue; - } - const rem = Math.max(0, Math.ceil((e.deadline - Date.now()) / 1000)); - e.span.textContent = _t("app.taskNextRun", { n: formatCountdown(rem) }); - // rant 2026-08-18T11:16:32:倒计时归零后任务状态不会自动更新(pending → running 永不反映到 UI)。 - // 检测到 deadline 已过 → 重新拉取任务状态;e.expired 防止同一任务重复触发。 - if (rem <= 0 && !e.expired) { - e.expired = true; - expired = true; - } - } - if (expired) { - renderTaskList().catch(() => {}); - } - } - - function stopTaskCountdown() { - if (taskCountdownTimer !== null) { - clearInterval(taskCountdownTimer); - taskCountdownTimer = null; - } - taskCountdowns = []; - } - - function startTaskCountdown() { - // 幂等重启:只清旧计时器,不清 taskCountdowns(stopTaskCountdown 会清引用,供离开视图用) - if (taskCountdownTimer !== null) { - clearInterval(taskCountdownTimer); - taskCountdownTimer = null; - } - if (taskCountdowns.length === 0) return; - taskCountdownTimer = setInterval(updateTaskCountdowns, 1000); - } - - // rant 2026-08-22T07:18:35:5s 状态轮询(方案 B:宿主确认轮询、可接受几秒滞后)。 - // 幂等:重复启动先清旧定时器;renderTaskList 不主动重启它(避免轮询自驱死循环)。 - function startTaskPoll() { - if (taskPollTimer !== null) { - clearInterval(taskPollTimer); - taskPollTimer = null; - } - taskPollTimer = setInterval(async () => { - try { - await loadTaskMeta(); - await renderTaskList(); - } catch { /* 轮询失败静默,下轮重试 */ } - }, 5000); - } - - function stopTaskPoll() { - if (taskPollTimer !== null) { - clearInterval(taskPollTimer); - taskPollTimer = null; - } - } - - async function loadTaskMeta() { - window.emrg?.log?.("info", "[taskPanel] loadTaskMeta start"); - try { - const templates = await window.emrg.taskTemplateList(); - taskTypes = Array.isArray(templates) - ? templates.map((t) => ({ name: t.name, builtin: Boolean(t.builtin) })) - : []; - window.emrg?.log?.("info", `[taskPanel] loadTaskMeta templates=${taskTypes.length}`); - } catch (e) { window.emrg?.log?.("warn", `[taskPanel] taskTemplateList FAILED ${e?.message || e}`); taskTypes = []; } - try { - const projects = await window.emrg.listProjects(); - taskProjects = Array.isArray(projects) ? projects : []; - window.emrg?.log?.("info", `[taskPanel] loadTaskMeta projects=${taskProjects.length}`); - } catch (e) { window.emrg?.log?.("warn", `[taskPanel] listProjects FAILED ${e?.message || e}`); taskProjects = []; } - } - - // rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表 - // (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs - // (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再 - // fallback impact 机器串——空则显示 "-";无记录 → 占位文案。 - // Rant 2026-08-20T22:59:16(宿主)三处改进: - // 1) work/slowdown_reason 按 Markdown 渲染(emrgMarkdown.renderMarkdown, - // 不再纯文本 el("span", text)); - // 2) 子表按时间倒序渲染(最新在最上面;后端 recent_runs 语义——最近 5 条——不变); - // 3) work/reason cell 多行截断(CSS line-clamp)+ 点击 cell 在本条记录下方 - // 展开完整内容(Markdown 渲染),再点收起。 - async function buildTaskRunDetail(t) { - const wrap = el("div", { class: "task-run-detail hidden" }); - const runs = Array.isArray(t.recent_runs) ? [...t.recent_runs] : []; - if (runs.length === 0) { - wrap.appendChild(el("div", { class: "task-run-empty" }, _t("app.taskRunsEmpty"))); - return wrap; - } - // rant 2026-08-20T22:59:16 #2:GUI 渲染前按 timestamp 倒序(最新在最上面) - runs.sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || ""))); - const head = el("div", { class: "task-run-head" }); - head.appendChild(el("span", {}, _t("app.taskRunsColTime"))); - head.appendChild(el("span", {}, _t("app.taskRunsColDone"))); - head.appendChild(el("span", {}, _t("app.taskRunsColThrottle"))); - head.appendChild(el("span", {}, _t("app.taskRunsColReason"))); - wrap.appendChild(head); - for (const r of runs) { - const row = el("div", { class: "task-run-row" }); - row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp))); - // rant 2026-08-20T10:58:55:字段统一 work(原 summary/done) - // rant 2026-08-20T22:59:16 #1/#3:Markdown 渲染 + 点击展开完整内容 - const done = (typeof r.work === "string" && r.work) ? r.work : ""; - const doneCell = el("span", { class: "task-run-done task-run-cell" }); - const doneExpand = el("div", { class: "task-run-expand hidden" }); - if (done) { - const md = await window.emrgMarkdown.renderMarkdown(done); - doneCell.innerHTML = md; - doneExpand.innerHTML = md; - doneCell.addEventListener("click", (e) => { - if (e && typeof e.stopPropagation === "function") e.stopPropagation(); - doneExpand.classList.toggle("hidden"); - }); - } else { - doneCell.textContent = "-"; - } - row.appendChild(doneCell); - const flagCell = el("span", { class: "task-run-flag" }); - if (r.recommend_slowdown) { - flagCell.appendChild(el("span", { class: "task-badge task-run-badge-warn" }, _t("app.taskRunThrottle"))); - } - row.appendChild(flagCell); - // rant 2026-08-19T18:25:14:原因列 —— 降频判断的自然语言理由(vibe check - // slowdown_reason,rant 2026-08-20T10:58:55 改名)。 - const reason = (typeof r.slowdown_reason === "string" && r.slowdown_reason) ? r.slowdown_reason : ""; - const reasonCell = el("span", { class: "task-run-reason task-run-cell" }); - const reasonExpand = el("div", { class: "task-run-expand hidden" }); - if (reason) { - const md = await window.emrgMarkdown.renderMarkdown(reason); - reasonCell.innerHTML = md; - reasonExpand.innerHTML = md; - reasonCell.addEventListener("click", (e) => { - if (e && typeof e.stopPropagation === "function") e.stopPropagation(); - reasonExpand.classList.toggle("hidden"); - }); - } else { - reasonCell.textContent = "-"; - } - row.appendChild(reasonCell); - wrap.appendChild(row); - wrap.appendChild(doneExpand); - wrap.appendChild(reasonExpand); - } - return wrap; - } - - async function refreshTaskListIfOpen() { - // rant 2026-08-21T17:41:23:触发任务后表格不刷新(状态/上次运行/倒计时滞后)。 - // 聊天 /trigger 路径复用:任务面板开着才重渲染;没开就不做(renderTaskList 无谓开销 + 倒计时定时器)。 - if (App.state.activeView === "tasks") { - await renderTaskList(); - } - } - - async function renderTaskList() { - const list = $("task-list"); - if (!list) return; // 元素缺失(测试桩)时忽略 - stopTaskCountdown(); // 清旧计时器 + 旧倒计时引用(空列表/失败路径也不会泄漏) - list.innerHTML = ""; - let tasks = []; - try { - tasks = await window.emrg.listTasks(); - window.emrg?.log?.("info", `[taskPanel] listTasks OK count=${tasks?.length ?? 0}`); - } catch (e) { - window.emrg?.log?.("error", `[taskPanel] listTasks FAILED ${e?.message || e}`); - list.innerHTML = `
${_t("settings.taskEmpty")}
`; - return; - } - if (!tasks || tasks.length === 0) { - list.innerHTML = `
${_t("settings.taskEmpty")}
`; - return; - } - const countdowns = []; // 本地收集 → 渲染完成后一次性挂载(startTaskCountdown 幂等重启) - for (const t of tasks) { - const row = el("div", { class: "task-row" }); - row.appendChild(el("span", { class: "task-name" }, t.name || "?")); - row.appendChild(el("span", { class: "task-badge" }, t.type || "evolution")); - // rant 09:23:10:running 状态徽标(演化任务 60s 一轮几乎常驻 running,从源头减少误点) - // rant 10:36:39:等待中 → "待运行"淡色徽标 + 下次运行倒计时;无 next 且启用 → "待调度" - if (t.running) { - const runBadge = el("span", { class: "task-badge task-running-badge" }, _t("app.taskRunningBadge")); - row.appendChild(runBadge); - // rant 2026-08-22T07:18:35:running 行显示已运行时长(started_at 由 scheduler - // status() 暴露;startedAt 固定、每秒递增 → 与待运行倒计时同机制反向跳动)。 - if (t.started_at != null) { - const elapsedSpan = el("span", { class: "task-next-run" }, _t("app.taskRunningDuration", { n: formatCountdown(0) })); - row.appendChild(elapsedSpan); - countdowns.push({ name: t.name, startedAt: Number(t.started_at) * 1000, span: elapsedSpan }); - } - } else if (t.next_run_in_seconds != null) { - row.appendChild(el("span", { class: "task-badge task-pending-badge" }, _t("app.taskPendingBadge"))); - const nextSpan = el("span", { class: "task-next-run" }, _t("app.taskNextRun", { n: formatCountdown(t.next_run_in_seconds) })); - row.appendChild(nextSpan); - countdowns.push({ name: t.name, deadline: Date.now() + Math.max(0, t.next_run_in_seconds) * 1000, span: nextSpan }); - } else if (t.enabled !== false) { - row.appendChild(el("span", { class: "task-badge task-idle-badge" }, _t("app.taskIdleBadge"))); - } - const cfg = (t.config && typeof t.config === "object") ? t.config : {}; - const hints = []; - if (cfg.project) hints.push(cfg.project); - if (t.interval != null) hints.push(_t("app.taskInterval", { n: t.interval ?? "-" })); - if (t.enabled === false) hints.push(_t("app.taskDisabled")); - row.appendChild(el("span", { class: "task-hint" }, hints.join(" · "))); - // rant 2026-08-18T10:45:52:上次执行维度 —— 最近一次运行时间 + 降频(saturation)标识。 - // rant 2026-08-19T18:25:14:一级列表不再显示"干了什么"(last_cycle_summary)—— - // 执行记录(时间/工作/降频建议/原因)统一由点击展开的二级列表(buildTaskRunDetail)呈现。 - // 数据来自 daemon list_tasks → handler.status()(last_run_at/saturation/recent_runs)。 - const meta = el("div", { class: "task-meta" }); - if (t.last_run_at) { - const runTxt = el("span", { class: "task-meta-item" }, _t("app.taskLastRun", { n: formatRelativeTime(t.last_run_at) })); - meta.appendChild(runTxt); - } else { - meta.appendChild(el("span", { class: "task-meta-item" }, _t("app.taskNoRunYet"))); - } - // rant 2026-08-20T10:58:55:降频徽标改为只认 heartbeat_active(不再用 - // empty_cycles)—— 文案「已降频 · heartbeat {m}s」(app.taskThrottled)。 - const sat = (t.saturation && typeof t.saturation === "object") ? t.saturation : null; - if (sat && sat.heartbeat_active) { - meta.appendChild(el("span", { class: "task-badge task-saturation-badge" }, _t("app.taskThrottled", { m: formatCountdown(sat.heartbeat_interval) }))); - } - row.appendChild(meta); - const actions = el("span", { class: "task-actions" }); - // 触发(复用 /trigger 语义;rant 09:23:10:running 时给 info 反馈而非假成功) - const trigBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.taskTrigger") }, _t("settings.taskTrigger")); - if (t.running) trigBtn.disabled = true; - trigBtn.addEventListener("click", async () => { - try { - const res = await window.emrg.triggerTask({ name: t.name }); - if (res && res.error) { - Chat.addSystemMessage(_t("app.triggerFailed", { msg: res.error })); - showToast(_t("app.triggerFailed", { msg: res.error }), { type: "error" }); - } else if (res && res.result === "running") { - Chat.addSystemMessage(_t("app.taskRunning", { n: t.name })); - showToast(_t("app.taskRunning", { n: t.name }), { type: "info", durationMs: 4000 }); - await renderTaskList(); // rant 2026-08-21T17:41:23:触发后立即刷新表格(状态/上次运行/倒计时不滞后) - } else { - Chat.addSystemMessage(_t("app.triggered", { n: t.name })); - showToast(_t("app.triggered", { n: t.name }), { type: "success" }); - await renderTaskList(); // rant 2026-08-21T17:41:23:触发成功后立即刷新表格 - } - } catch (e) { - Chat.addSystemMessage(_t("app.triggerFailed", { msg: e.message })); - showToast(_t("app.triggerFailed", { msg: e.message }), { type: "error" }); - } - }); - actions.appendChild(trigBtn); - // 打开会话(rant 2026-08-21T17:46:12:任务行加"打开会话"按钮——直接打开该任务 - // 的固定会话 emrg-evolution-{name},效果与"打开会话"弹窗选择一致(左侧列表出现并激活)。 - // session_id/project_path 来自 daemon list_tasks(TaskHandler.status() 新增字段)。 - const sessBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.taskOpenSession") }, _t("settings.taskOpenSession")); - if (!t.session_id) sessBtn.disabled = true; - sessBtn.addEventListener("click", async () => { - window.emrg?.log?.("info", `[taskPanel] open-session CLICK task=${t.name} session_id=${t.session_id} project_path=${t.project_path} disabled=${!t.session_id}`); - if (!t.session_id) { - window.emrg?.log?.("warn", `[taskPanel] open-session SKIP no session_id task=${t.name}`); - return; - } - try { - window.emrg?.log?.("info", `[taskPanel] open-session → switchSession sid=${t.session_id} projectPath=${t.project_path}`); - await App.switchSession(t.session_id, { projectPath: t.project_path }); - window.emrg?.log?.("info", `[taskPanel] open-session switchSession resolved sid=${t.session_id}`); - } catch (e) { - window.emrg?.log?.("error", `[taskPanel] open-session switchSession THREW sid=${t.session_id} ${e?.message || e}`); - Chat.addSystemMessage(_t("openSession.loadFailed", { msg: e.message })); - showToast(_t("openSession.loadFailed", { msg: e.message }), { type: "error" }); - } - }); - actions.appendChild(sessBtn); - // 编辑(表单预填 → taskUpdate) - const editBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.taskEdit") }, _t("settings.taskEdit")); - editBtn.addEventListener("click", () => { openTaskForm(t).catch(() => {}); }); - actions.appendChild(editBtn); - // 删除(确认弹窗 → taskDelete → 热重载由 daemon apply_tasks 完成) - const delBtn = el("button", { type: "button", class: "model-action-btn danger", title: _t("settings.taskDelete") }, _t("settings.taskDelete")); - delBtn.addEventListener("click", () => { - showConfirm(_t("settings.taskDelete"), _t("settings.taskDeleteConfirm", { name: t.name }), { - okText: _t("settings.taskDelete"), - danger: true, - onOk: async () => { - try { - await window.emrg.taskDelete({ name: t.name }); - Chat.addSystemMessage(_t("settings.taskDeleted")); - showToast(_t("settings.taskDeleted"), { type: "success" }); - await renderTaskList(); - } catch (e) { - Chat.addSystemMessage(_t("app.tasksFailed", { msg: e.message })); - showToast(_t("app.tasksFailed", { msg: e.message }), { type: "error" }); - } - }, - }); - }); - actions.appendChild(delBtn); - row.appendChild(actions); - // rant 2026-08-18T21:32:32:点击任务卡(非按钮)→ 手风琴展开最近运行子表。 - // 真实 DOM 中按钮点击通过 e.target.closest("button") 拦截(不触发展开); - // 测试沙箱的 click() 无 target → 直接切换(沙箱中按钮点击不冒泡,互不影响)。 - const runDetail = await buildTaskRunDetail(t); - row.appendChild(runDetail); - let runDetailOpen = false; - row.addEventListener("click", (e) => { - if (e && e.target && typeof e.target.closest === "function" && e.target.closest("button")) return; - runDetailOpen = !runDetailOpen; - runDetail.classList.toggle("hidden", !runDetailOpen); - }); - list.appendChild(row); - } - taskCountdowns = countdowns; - startTaskCountdown(); // rant 10:36:39:面板激活即启动 1s 倒计时(离开视图由 app.js stopTaskCountdown) - } - - async function openTaskForm(task = null) { - // rant 2026-08-14T15:41:52:点"+ 添加任务"时元数据未加载完 → 下拉为空 → 保存报 invalid type - // 幂等快路径:openTasksPanel/保存刷新已加载过 → 同步填(无闪烁);未完成 → 等 IPC 返回再填 - if (taskTypes.length === 0 && taskProjects.length === 0) { - await loadTaskMeta(); - } - editingTask = task ? task.name : null; - $("task-form-name").value = task ? task.name : ""; - $("task-form-name").disabled = Boolean(task); // 决策点:daemon 以 name 定位 → 不可改名 - // 类型下拉(内置 + 自定义;决策点⑥内置模板只读体现在模板管理区) - const typeSel = $("task-form-type"); - typeSel.innerHTML = ""; - for (const tp of taskTypes) { - const opt = el("option", { value: tp.name }, tp.name); - typeSel.appendChild(opt); - } - const curType = (task && task.type) || (taskTypes[0] && taskTypes[0].name) || "evolution"; - typeSel.value = taskTypes.some((tp) => tp.name === curType) ? curType : (taskTypes[0] ? taskTypes[0].name : "evolution"); - // 项目下拉(决策点③:仅已注册项目) - const projSel = $("task-form-project"); - projSel.innerHTML = ""; - for (const p of taskProjects) { - const pv = p.name || p.path || ""; - projSel.appendChild(el("option", { value: pv }, pv)); - } - const cfg = (task && task.config && typeof task.config === "object") ? task.config : {}; - const curProj = cfg.project || (taskProjects[0] ? taskProjects[0].name || taskProjects[0].path || "" : ""); - projSel.value = taskProjects.some((p) => (p.name || p.path || "") === curProj) - ? curProj - : (taskProjects[0] ? taskProjects[0].name || taskProjects[0].path || "" : ""); - $("task-form-interval").value = task && task.interval != null ? task.interval : 1800; - $("task-form-enabled").checked = task ? task.enabled !== false : true; - $("task-form-repo").value = cfg.repo || ""; - // rant 2026-08-20T18:06:44:沙箱下拉预填(编辑读 task.sandbox;新增默认 workspace-write 与后端默认一致) - $("task-form-sandbox").value = (task && task.sandbox) || "workspace-write"; - $("task-form").classList.remove("hidden"); - $("task-form-name").focus(); - } - - function closeTaskForm() { - $("task-form").classList.add("hidden"); - editingTask = null; - $("task-form-name").disabled = false; - } - - async function saveTaskForm() { - const name = $("task-form-name").value.trim(); - if (!name) { - showConfirm(_t("dlg.stepTitle"), _t("dlg.nameRequiredBody"), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - // 决策点⑤:间隔最小值 60s(客户端先行校验,daemon 兜底) - const interval = parseInt($("task-form-interval").value, 10); - if (!Number.isFinite(interval) || interval < 60) { - showConfirm(_t("dlg.stepTitle"), _t("settings.taskIntervalInvalid"), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - const payload = { - name, - type: $("task-form-type").value, - project: $("task-form-project").value, - interval, - enabled: $("task-form-enabled").checked, - repo: $("task-form-repo").value.trim() || undefined, - sandbox: $("task-form-sandbox").value, - }; - try { - if (editingTask === null) await window.emrg.taskCreate(payload); - else await window.emrg.taskUpdate(payload); - Chat.addSystemMessage(_t("settings.taskSaved")); - showToast(_t("settings.taskSaved"), { type: "success" }); - closeTaskForm(); - await renderTaskList(); - } catch (e) { - Chat.addSystemMessage(_t("app.tasksFailed", { msg: e.message })); - showToast(_t("app.tasksFailed", { msg: e.message }), { type: "error" }); - } - } - - // ── 自定义类型管理(决策点①⑥:内置只读;决策点②:被引用拒绝删除) ── - // rant 2026-08-15T09:17:45/09:20:12:提示词编辑器升级 Monaco(本地 vendor,零网络)。 - // 测试/无 monaco 环境回退轻量 shim(读写 host 元素的 .value),行为等价。 - let templateEditor = null; // monaco.editor 实例或 shim - let templateMonacoInit = false; - const templateEditorWaiters = []; - - function currentMonacoTheme() { - try { - const root = document.documentElement; - const t = root && root.getAttribute ? root.getAttribute("data-theme") : null; - if (t === "light") return "vs"; - if (t === "dark") return "vs-dark"; - return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "vs-dark" : "vs"; - } catch { return "vs"; } - } - - function createTemplateEditorShim() { - const host = $("task-template-prompt"); - return { - setValue(v) { if (host) host.value = v || ""; }, - getValue() { return host ? (host.value || "") : ""; }, - setReadOnly() {}, - focus() { if (host && host.focus) host.focus(); }, - layout() {}, - dispose() {}, - }; - } - - function flushTemplateEditorWaiters() { - const waiters = templateEditorWaiters.splice(0); - waiters.forEach((fn) => { try { fn(templateEditor); } catch { /* ignore */ } }); - } - - function fallbackTemplateEditorShim(reason) { - // eslint-disable-next-line no-console - console.warn("[dialogs] Monaco unavailable (" + reason + ") — using textarea shim"); - templateEditor = createTemplateEditorShim(); - flushTemplateEditorWaiters(); - } - - function initTemplateMonaco() { - if (templateMonacoInit) return; - templateMonacoInit = true; - try { - // preferScriptTags: sandboxed renderer (sandbox:true, nodeIntegration:false) exposes - // process.versions.electron + process.type==='renderer' → loader would pick the Node - // loader branch which needs nodeRequire (undefined in sandbox) → load fails silently. - // Script-tag loader needs no eval/nodeRequire and works under CSP 'self' + file://. - // (pm25coder Windows host review, #801) - window.require.config({ paths: { vs: "../vendor/monaco/vs" }, preferScriptTags: true }); - // errback + timeout: if editor.main fails to load, degrade to the shim instead of - // leaving a dead empty .monaco-host (waiters would accumulate, save reads "" → confusing - // "templateInvalid" toast). (#801 review finding 2) - const t = setTimeout(() => { - if (!templateEditor) fallbackTemplateEditorShim("timeout"); - }, 8000); - window.require(["vs/editor/editor.main"], () => { - clearTimeout(t); - const host = $("task-template-prompt"); - if (!host || !window.monaco || !window.monaco.editor) { - fallbackTemplateEditorShim("editor.main missing"); - return; - } - templateEditor = window.monaco.editor.create(host, { - value: "", - language: "markdown", - theme: currentMonacoTheme(), - lineNumbers: "on", - wordWrap: "on", - minimap: { enabled: false }, - scrollBeyondLastLine: false, - automaticLayout: true, - fontSize: 13, - lineHeight: 20, - padding: { top: 6, bottom: 6 }, - }); - // system 主题下跟随 OS 深浅切换(settings 内切换走 initThemeButtons → sync) - if (window.matchMedia) { - try { - window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", syncTemplateEditorTheme); - } catch { /* ignore */ } - } - flushTemplateEditorWaiters(); - }, (err) => { - clearTimeout(t); - fallbackTemplateEditorShim(String(err || "module load error")); - }); - } catch (e) { - fallbackTemplateEditorShim(String(e)); - } - } - - function withTemplateEditor(fn) { - if (templateEditor) return fn(templateEditor); - if (!(window.monaco && window.monaco.editor) && !window.require) { - // 无 monaco(测试沙箱等)→ 立即用 shim,行为与 textarea 时代等价 - templateEditor = createTemplateEditorShim(); - return fn(templateEditor); - } - templateEditorWaiters.push(fn); - initTemplateMonaco(); - return null; - } - - function syncTemplateEditorTheme() { - const ed = templateEditor; - if (ed && window.monaco && window.monaco.editor) { - try { window.monaco.editor.setTheme(currentMonacoTheme()); } catch { /* ignore */ } - } - } - - async function renderTemplateList() { - const wrap = $("task-template-list"); - if (!wrap) return; - wrap.innerHTML = ""; - let templates = []; - try { - templates = await window.emrg.taskTemplateList(); - } catch (e) { - wrap.innerHTML = `
${_t("settings.templateEmpty")}
`; - return; - } - if (!templates || templates.length === 0) { - wrap.innerHTML = `
${_t("settings.templateEmpty")}
`; - return; - } - for (const t of templates) { - const row = el("div", { class: "task-row" }); - row.appendChild(el("span", { class: "task-name" }, t.name)); - row.appendChild(el("span", { class: "task-badge" }, t.builtin ? _t("settings.taskBuiltin") : _t("settings.taskCustom"))); - const body = t.prompt || t.template || ""; - row.appendChild(el("span", { class: "task-hint" }, body.length > 60 ? body.slice(0, 60) + "…" : body)); - // rant 09:17:45:内置类型提供只读"查看"(Monaco readOnly 同样高亮/行号/滚动); - // 自定义类型在查看之外保留编辑/删除(决策点①⑥:内置不可增删改) - const actions = el("span", { class: "task-actions" }); - const viewBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.templateView") }, _t("settings.templateView")); - viewBtn.addEventListener("click", () => openTemplateForm(t)); - actions.appendChild(viewBtn); - if (!t.builtin) { - const editBtn = el("button", { type: "button", class: "model-action-btn", title: _t("settings.templateEdit") }, _t("settings.templateEdit")); - editBtn.addEventListener("click", () => openTemplateForm(t)); - actions.appendChild(editBtn); - const delBtn = el("button", { type: "button", class: "model-action-btn danger", title: _t("settings.templateDelete") }, _t("settings.templateDelete")); - delBtn.addEventListener("click", () => { - showConfirm(_t("settings.templateDelete"), _t("settings.templateDeleteConfirm", { name: t.name }), { - okText: _t("settings.templateDelete"), - danger: true, - onOk: async () => { - try { - await window.emrg.taskTemplateDelete({ name: t.name }); - Chat.addSystemMessage(_t("settings.templateDeleted")); - showToast(_t("settings.templateDeleted"), { type: "success" }); - await loadTaskMeta(); // 刷新类型选项(任务表单下拉) - await renderTemplateList(); - } catch (e) { - // 决策点②:被任务引用的类型 daemon 拒绝删除(错误信息含任务数) - Chat.addSystemMessage(_t("settings.templateDeleteFailed", { msg: e.message })); - showToast(_t("settings.templateDeleteFailed", { msg: e.message }), { type: "error" }); - } - }, - }); - }); - actions.appendChild(delBtn); - } - row.appendChild(actions); - wrap.appendChild(row); - } - // 底部"+ 添加类型"(仅自定义类型可增删改) - const addRow = el("div", { style: "display:flex;justify-content:center;margin-top:6px;" }); - const addBtn = el("button", { type: "button", class: "btn btn-ghost", style: "padding:4px 12px;" }, _t("settings.templateAdd")); - addBtn.addEventListener("click", () => openTemplateForm(null)); - addRow.appendChild(addBtn); - wrap.appendChild(addRow); - } - - function openTemplateForm(tpl = null) { - editingTemplate = tpl ? tpl.name : null; - const readOnly = Boolean(tpl && tpl.builtin); - $("task-template-name").value = tpl ? tpl.name : ""; - $("task-template-name").disabled = Boolean(tpl); // daemon 以 name 定位 → 不可改名 - $("task-template-form").classList.remove("hidden"); - // 内置类型只读查看:隐藏保存按钮(编辑器 readOnly 高亮/行号照常) - const saveBtn = $("task-template-save"); - if (saveBtn) saveBtn.classList.toggle("hidden", readOnly); - const applyEditor = (ed) => { - ed.setValue(tpl ? (tpl.prompt || tpl.template || "") : ""); - ed.setReadOnly(readOnly); - syncTemplateEditorTheme(); - try { ed.focus(); } catch { /* ignore */ } - }; - const ed = templateEditor; - if (ed) applyEditor(ed); - else withTemplateEditor(applyEditor); - if (!readOnly) $("task-template-name").focus(); - } - - function closeTemplateForm() { - $("task-template-form").classList.add("hidden"); - editingTemplate = null; - $("task-template-name").disabled = false; - $("task-template-name").value = ""; - const saveBtn = $("task-template-save"); - if (saveBtn) saveBtn.classList.remove("hidden"); - const ed = templateEditor; - if (ed) { - try { ed.setValue(""); ed.setReadOnly(false); } catch { /* ignore */ } - } else if ($("task-template-prompt")) { - $("task-template-prompt").value = ""; - } - } - - function getTemplateEditorValue() { - const ed = templateEditor; - if (ed) return ed.getValue(); - const host = $("task-template-prompt"); - return host ? (host.value || "") : ""; - } - - async function saveTemplateForm() { - const name = $("task-template-name").value.trim(); - const prompt = getTemplateEditorValue(); - if (!name || !prompt.trim()) { - showConfirm(_t("dlg.stepTitle"), _t("settings.templateInvalid"), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - try { - if (editingTemplate === null) await window.emrg.taskTemplateCreate({ name, prompt }); - else await window.emrg.taskTemplateUpdate({ name, prompt }); - Chat.addSystemMessage(_t("settings.templateSaved")); - showToast(_t("settings.templateSaved"), { type: "success" }); - closeTemplateForm(); - await loadTaskMeta(); - await renderTemplateList(); - } catch (e) { - Chat.addSystemMessage(_t("settings.templateSaveFailed", { msg: e.message })); - showToast(_t("settings.templateSaveFailed", { msg: e.message }), { type: "error" }); - } - } - - function initTaskManagement() { - const addBtn = $("task-add-btn"); - if (addBtn) addBtn.addEventListener("click", () => { openTaskForm(null).catch(() => {}); }); - const saveBtn = $("task-form-save"); - if (saveBtn) saveBtn.addEventListener("click", saveTaskForm); - const cancelBtn = $("task-form-cancel"); - if (cancelBtn) cancelBtn.addEventListener("click", closeTaskForm); - const tmgrBtn = $("task-template-mgr-btn"); - if (tmgrBtn) { - tmgrBtn.addEventListener("click", async () => { - const wrap = $("task-template-list"); - if (!wrap) return; - if (wrap.classList.contains("hidden")) { - wrap.classList.remove("hidden"); - closeTemplateForm(); // 收起表单,避免与列表并列 - await loadTaskMeta(); - await renderTemplateList(); - } else { - wrap.classList.add("hidden"); - } - }); - } - const tSave = $("task-template-save"); - if (tSave) tSave.addEventListener("click", saveTemplateForm); - const tCancel = $("task-template-cancel"); - if (tCancel) tCancel.addEventListener("click", closeTemplateForm); - const nameInput = $("task-form-name"); - if (nameInput) { - nameInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") { e.preventDefault(); saveTaskForm(); } - else if (e.key === "Escape") { e.preventDefault(); closeTaskForm(); } - }); - } - const tplInput = $("task-template-name"); - if (tplInput) { - tplInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") { e.preventDefault(); saveTemplateForm(); } - else if (e.key === "Escape") { e.preventDefault(); closeTemplateForm(); } - }); - } - } - - // ── 确认对话框(替代 confirm/alert) ──── - let confirmCb = null; - function showConfirm(title, message, opts = {}) { - $("confirm-title").textContent = title; - $("confirm-message").textContent = message; - const ok = $("confirm-ok"); - ok.textContent = opts.okText || _t("dlg.delete"); - ok.className = opts.danger === false ? "btn btn-primary" : "btn btn-danger"; - confirmCb = opts.onOk || null; - $("confirm-dialog").showModal(); - } - - // ── P5(rant 15:07:19):打开会话对话框(两步:选项目 → 选会话,跨项目) ── - async function showOpenSessionDialog() { - const list = $("open-session-list"); - const dialog = $("open-session-dialog"); - if (!list || !dialog) return; - list.innerHTML = `
${_t("dlg.loading")}
`; - dialog.showModal(); - try { - const projects = await window.emrg.listProjects(); - list.innerHTML = ""; - if (!projects || projects.length === 0) { - list.innerHTML = `
${_t("openSession.noProjects")}
`; - return; - } - // 第一步:项目列表(按最近活跃倒序——daemon 已排;底部"新建项目…"按钮) - projects.forEach((p) => { - const row = el("div", { class: "help-row", style: "display:flex;align-items:center;gap:var(--sp-2);" }); - const pick = el("button", { type: "button", style: "flex:1;text-align:left;cursor:pointer;background:none;border:none;display:flex;flex-direction:column;align-items:flex-start;padding:0;" }); - const name = el("span", { class: "help-cmd" }, p.name || p.path || ""); - const hint = el("span", { class: "help-hint" }, p.path || ""); - pick.appendChild(name); - pick.appendChild(hint); - // P6 验收补完:项目行显示"最近活跃"相对时间(消费 daemon latest_session_at) - const act = relTime(p.latest_session_at); - if (act) pick.appendChild(el("span", { class: "help-hint" }, act)); - // P5 slice 2:点击打开会话(带项目路径 → resume 用该项目 cwd) - pick.addEventListener("click", () => showProjectSessions(p)); - row.appendChild(pick); - // 删除项目(受保护项目不可删 → 提示;非受保护 → 确认弹窗) - const del = el("button", { type: "button", class: "btn btn-ghost", style: "padding:2px 8px;flex-shrink:0;", title: _t("deleteProject.delete") }, _t("deleteProject.delete")); - del.addEventListener("click", () => confirmDeleteProject(p)); - row.appendChild(del); - list.appendChild(row); - }); - } catch (e) { - list.innerHTML = `
${_t("openSession.loadFailed", { msg: e.message })}
`; - } - } - - // P5 slice 2:受保护项目(内置 project emrg / 内置 task emrg-task → 演化依赖,不可删) - function isProtectedProject(p) { - return p && (p.name === "emrg" || p.name === "emrg-task"); - } - - function confirmDeleteProject(p, onDone) { - if (isProtectedProject(p)) { - showConfirm(_t("deleteProject.title"), _t("deleteProject.protectedBody", { name: p.name }), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - showConfirm(_t("deleteProject.title"), _t("deleteProject.body", { name: p.name }), { - okText: _t("dlg.delete"), - danger: true, - onOk: async () => { - try { - const res = await window.emrg.removeProject({ name: p.name, path: p.path }); - if (!res || !res.ok) { - Chat.addSystemMessage(_t("deleteProject.failed", { msg: (res && res.error) || _t("app.unknownError") })); - return; - } - Chat.addSystemMessage(_t("deleteProject.removed", { name: p.name })); - // 该项目已打开的会话被关闭 → 激活会话被关则切相邻 - const closed = res.closed || []; - if (closed.length > 0 && closed.includes(App.state.sessionId)) { - const remaining = (App.state.openSessions || []).filter((s) => !closed.includes(s.sid)); - if (remaining.length > 0) await App.switchSession(remaining[0].sid, { silent: true }); - else await App.newSession(); - } - if (typeof onDone === "function") onDone(); - else showOpenSessionDialog(); // 刷新项目列表 - } catch (e) { - Chat.addSystemMessage(_t("deleteProject.failed", { msg: e.message })); - } - }, - }); - } - - // 第二步:该项目会话列表(created_at 倒序)→ 点击打开(switchSession 复用连接) - async function showProjectSessions(project) { - const list = $("open-session-list"); - list.innerHTML = `
${_t("dlg.loading")}
`; - $("open-session-title").textContent = _t("openSession.titleProject", { project: project.name || project.path || "" }); - try { - const frame = await window.emrg.listProjectSessions({ projectPath: project.path }); - const sessions = frame.sessions || []; - list.innerHTML = ""; - if (sessions.length === 0) { - list.innerHTML = `
${_t("openSession.noSessions")}
`; - return; - } - sessions.forEach((s) => { - const row = el("button", { class: "help-row", type: "button", style: "width:100%;text-align:left;cursor:pointer;background:none;border:none;" }); - // rant 11:41:07:有 title 显 title,无 title 显完整 session id(name-or-id 规则,不截短) - const name = el("span", { class: "help-cmd" }, s.title || s.session_id); - const marks = []; - if (s.session_id === App.state.sessionId) marks.push(_t("app.current")); - const act = relTime(s.updated_at); // 最后活跃时间(Session.list_sessions 已返回 updated_at) - if (act) marks.push(act); - const hint = el("span", { class: "help-hint" }, marks.join(" · ")); - row.appendChild(name); - row.appendChild(hint); - row.addEventListener("click", async () => { - $("open-session-dialog").close(); - // P5 slice 2:带项目路径切换(resume 用该项目 cwd,非全局 projectDir) - await App.switchSession(s.session_id, { projectPath: project.path }); - }); - list.appendChild(row); - }); - } catch (e) { - list.innerHTML = `
${_t("openSession.loadFailed", { msg: e.message })}
`; - } - } - - function initOpenSessionDialog() { - $("open-session-cancel").addEventListener("click", () => $("open-session-dialog").close()); - $("open-session-new").addEventListener("click", async () => { - // P5:新建项目 = 选目录 → 轻量命令注册(daemon 隐式 _touch_project) - try { - const res = await window.emrg.pickProjectDir(); - if (res && res.path) { - await window.emrg.registerProject({ path: res.path }); - Chat.addSystemMessage(_t("openSession.projectCreated", { path: res.path })); - showOpenSessionDialog(); // 刷新项目列表 - } - } catch (e) { - Chat.addSystemMessage(_t("openSession.projectFailed", { msg: e.message })); - } - }); - } - - // ── Rant 面板(rant 14:10:14 P4:列表筛选 + 详情 + 新建) ── - let rantFilter = ""; // "" | pending | in_progress | completed - - function setRantFilter(status) { - rantFilter = status || ""; - // tab 高亮 - for (const f of ["all", "pending", "inprogress", "completed"]) { - const btn = $(`rant-filter-${f}`); - if (btn) btn.classList.toggle("active", f === (status === "" ? "all" : status === "in_progress" ? "inprogress" : status)); - } - renderRantList(); - } - - async function renderRantList() { - const list = $("rant-list"); - if (!list) return; - list.innerHTML = ""; - list.innerHTML = `
${_t("dlg.loading")}
`; - try { - const rants = await window.emrg.listRants({ status: rantFilter }); - list.innerHTML = ""; - if (!rants || rants.length === 0) { - list.innerHTML = `
${_t(rantFilter ? "rants.emptyFiltered" : "rants.empty")}
`; - return; - } - for (const r of rants) { - // rant 21:36:01:5 列对齐(时间/项目/状态/进度/内容),与 .rant-head 列头同列宽类 - const row = el("div", { class: "task-row rant-row" }); - const ts = String(r.timestamp || "").slice(0, 16).replace("T", " "); - row.appendChild(el("span", { class: "rant-col-time" }, ts || "—")); - row.appendChild(el("span", { class: "rant-col-project" }, r.project || "—")); - // 状态徽标三态配色(completed 绿 / in_progress 琥珀 / pending 灰) - const st = r.status || "pending"; - const stText = st === "completed" ? _t("rants.statusCompleted") : st === "in_progress" ? _t("rants.statusInProgress") : _t("rants.statusPending"); - const badgeCls = st === "completed" ? "badge-done" : st === "in_progress" ? "badge-warn" : "badge-muted"; - row.appendChild(el("span", { class: `task-badge ${badgeCls}` }, stText)); - row.appendChild(el("span", { class: "rant-col-progress" }, r.progress ? String(r.progress) : "—")); - // 内容列:message 首行摘要(去 md 标题/列表标记) - const msg = r.message || ""; - const firstLine = (msg.split("\n").find((l) => l.trim() !== "") || "").replace(/^#{1,6}\s+/, "").replace(/^[>*\-\s]+/, ""); - row.appendChild(el("span", { class: "rant-col-content" }, firstLine || "—")); - // 详情展开/收起(rant 10:41:43:点击已展开的本行 → 收起,不再无条件重建) - row.addEventListener("click", async () => { - const existing = row.nextElementSibling; - const isSelfDetail = existing && existing.classList.contains("rant-detail"); - // 先移除所有已展开的详情(含其他行的) - list.querySelectorAll(".rant-detail").forEach((d) => d.remove()); - if (isSelfDetail) return; // 点自己已展开 → 收起 - const detailRow = el("div", { class: "rant-detail", style: "padding:6px 8px;border-top:1px solid var(--border);font-size:var(--fs-secondary);" }); - detailRow.appendChild(el("div", { class: "rant-meta" }, `${ts || "—"} · ${r.project || "—"} · ${stText}`)); - const md = el("div", { class: "msg-body rant-md" }); - try { - if (window.emrgMarkdown && window.emrgMarkdown.renderMarkdown) { - // rant 10:41:43:【】标题预处理(markdown 视觉层次;不破坏原文) - md.innerHTML = await window.emrgMarkdown.renderMarkdown(preprocessRantMarkdown(msg)); - } else { - md.textContent = msg; - } - } catch { - md.textContent = msg; - } - detailRow.appendChild(md); - if (r.progress) detailRow.appendChild(el("div", { class: "rant-progress" }, `${_t("rants.statusInProgress")}: ${r.progress}`)); - else detailRow.appendChild(el("div", { class: "rant-progress" }, _t("rants.noProgress"))); - row.after(detailRow); - }); - list.appendChild(row); - } - } catch (e) { - list.innerHTML = `
${_t("rants.loadFailed", { msg: e.message })}
`; - } - } - - // rant 10:41:43:rant 详情 markdown 预处理 —— 行首 【xxx】 段标记映射为标题级 - // (【任务】→####),让整篇有视觉层次;仅短行(≤60 字符)转标题,长内容行保持正文。 - // 原文保留(只加 #### 前缀,不删除/改写原文)。 - function preprocessRantMarkdown(text) { - if (!text) return text || ""; - return String(text).split("\n").map((line) => { - const t = line.trim(); - if (/^【[^】]{1,24}】/.test(t) && t.length <= 60) { - return `#### ${t}`; - } - return line; - }).join("\n"); - } - - function openRantForm() { - const form = $("rant-form"); - if (!form) return; - if (!form.classList.contains("hidden")) { form.classList.add("hidden"); return; } - $("rant-form-message").value = ""; - // 项目下拉(复用 listProjects) - const sel = $("rant-form-project"); - sel.innerHTML = ""; - const opt = el("option", { value: "" }, _t("rants.project")); - sel.appendChild(opt); - window.emrg.listProjects().then((projects) => { - for (const p of projects || []) { - const o = el("option", { value: p.name || p.path || "" }, p.name || p.path || ""); - sel.appendChild(o); - } - }).catch(() => {}); - form.classList.remove("hidden"); - $("rant-form-message").focus(); - } - - async function submitRantForm() { - const msg = $("rant-form-message").value.trim(); - const proj = $("rant-form-project").value; - if (!msg) { - showConfirm(_t("dlg.stepTitle"), _t("rants.message"), { okText: _t("dlg.gotIt"), danger: false }); - return; - } - try { - const res = await window.emrg.sendRant({ message: msg, project: proj }); - Chat.addSystemMessage(_t("rants.sent", { count: res && res.count ? res.count : "" })); - $("rant-form").classList.add("hidden"); - $("rant-form-message").value = ""; - await renderRantList(); - } catch (e) { - Chat.addSystemMessage(_t("rants.sendFailed", { msg: e.message })); - } - } - - function initRantPanel() { - // 状态筛选 tab - for (const f of ["all", "pending", "inprogress", "completed"]) { - const btn = $(`rant-filter-${f}`); - if (btn) btn.addEventListener("click", () => setRantFilter(btn.dataset.rantFilter)); - } - const newBtn = $("rant-new-btn"); - if (newBtn) newBtn.addEventListener("click", openRantForm); - const submit = $("rant-form-submit"); - if (submit) submit.addEventListener("click", submitRantForm); - const cancel = $("rant-form-cancel"); - if (cancel) cancel.addEventListener("click", () => $("rant-form").classList.add("hidden")); - } - - // ── 项目面板(rant 14:10:14 P5:侧边栏项目入口,复用项目 IPC) ── - /** 项目面板:列表(名称/路径/auto_evolve 徽标/最近活跃)+ 查看会话 + 删除 + 添加。 */ - async function renderProjectList() { - const list = $("project-list"); - if (!list) return; - list.innerHTML = ""; // 显式清空(测试沙箱 innerHTML 非空赋值不清 children;真实 DOM 语义一致) - list.innerHTML = `
${_t("dlg.loading")}
`; - try { - // auto_evolve 徽标:tasks.yml 中有 config.project === 项目名的任务(含 evolution 类型) - const [projects, tasks] = await Promise.all([ - window.emrg.listProjects(), - window.emrg.listTasks().catch(() => []), - ]); - list.innerHTML = ""; - if (!projects || projects.length === 0) { - list.innerHTML = `
${_t("projects.empty")}
`; - return; - } - const evolveProjects = new Set( - (tasks || []).filter((t) => t && t.config && t.config.project).map((t) => t.config.project) - ); - projects.forEach((p) => { - const row = el("div", { class: "task-row" }); - row.appendChild(el("span", { class: "task-name" }, p.name || p.path || "?")); - if (evolveProjects.has(p.name)) { - row.appendChild(el("span", { class: "task-badge" }, `⚡ ${_t("projects.autoEvolve")}`)); - } - const hints = []; - if (p.path) hints.push(p.path); - const act = relTime(p.latest_session_at); - if (act) hints.push(act); - row.appendChild(el("span", { class: "task-hint" }, hints.join(" · "))); - const actions = el("span", { class: "task-actions" }); - // 查看会话(复用 listProjectSessions) - const sessBtn = el("button", { type: "button", class: "model-action-btn", title: _t("projects.viewSessions") }, _t("projects.viewSessions")); - sessBtn.addEventListener("click", () => showProjectSessionsInPanel(p)); - actions.appendChild(sessBtn); - // 删除(受保护守卫 + 确认;删除后刷新面板) - const delBtn = el("button", { type: "button", class: "model-action-btn danger", title: _t("deleteProject.delete") }, _t("deleteProject.delete")); - delBtn.addEventListener("click", () => confirmDeleteProject(p, () => renderProjectList())); - actions.appendChild(delBtn); - row.appendChild(actions); - list.appendChild(row); - }); - } catch (e) { - list.innerHTML = `
${_t("projects.addFailed", { msg: e.message })}
`; - } - } - - /** 项目面板内嵌:该项目会话列表(created_at 倒序)→ 点击切换会话(带项目 cwd)。 */ - async function showProjectSessionsInPanel(project) { - const list = $("project-list"); - if (!list) return; - list.innerHTML = ""; // 显式清空(测试沙箱 innerHTML 非空赋值不清 children;真实 DOM 语义一致) - list.innerHTML = `
${_t("dlg.loading")}
`; - // 返回按钮 + 标题 - const title = el("div", { class: "task-row" }); - const backBtn = el("button", { type: "button", class: "model-action-btn", style: "flex-shrink:0;" }, _t("projects.back")); - backBtn.addEventListener("click", () => renderProjectList()); - title.appendChild(backBtn); - title.appendChild(el("span", { class: "task-name" }, _t("projects.sessionsOf", { project: project.name || project.path || "" }))); - list.appendChild(title); - try { - const frame = await window.emrg.listProjectSessions({ projectPath: project.path }); - const sessions = frame.sessions || []; - if (sessions.length === 0) { - list.appendChild(el("div", { class: "task-empty" }, _t("projects.noSessions"))); - return; - } - sessions.forEach((s) => { - const row = el("div", { class: "task-row", style: "cursor:pointer;" }); - row.appendChild(el("span", { class: "task-name" }, s.title || _t("app.unnamed"))); - if (s.session_id === App.state.sessionId) row.appendChild(el("span", { class: "task-badge" }, _t("projects.current"))); - const hint = el("span", { class: "task-hint" }, s.session_id || ""); - row.appendChild(hint); - row.addEventListener("click", async () => { - // 带项目路径切换(resume 用该项目 cwd,非全局 projectDir) - await App.switchSession(s.session_id, { projectPath: project.path }); - }); - list.appendChild(row); - }); - } catch (e) { - list.appendChild(el("div", { class: "task-empty" }, _t("openSession.loadFailed", { msg: e.message }))); - } - } - - // ── P5 slice 2:新建会话对话框(选已有项目 → 新建;或新建项目 → 新建) ── - async function showNewSessionDialog() { - const list = $("new-session-list"); - const dialog = $("new-session-dialog"); - if (!list || !dialog) return; - list.innerHTML = `
${_t("dlg.loading")}
`; - dialog.showModal(); - try { - const projects = await window.emrg.listProjects(); - list.innerHTML = ""; - if (!projects || projects.length === 0) { - list.innerHTML = `
${_t("openSession.noProjects")}
`; - return; - } - // 已有项目(活跃排序)→ 点选即在该项目新建会话 - projects.forEach((p) => { - const row = el("button", { class: "help-row", type: "button", style: "width:100%;text-align:left;cursor:pointer;background:none;border:none;" }); - const name = el("span", { class: "help-cmd" }, p.name || p.path || ""); - const hint = el("span", { class: "help-hint" }, p.path || ""); - row.appendChild(name); - row.appendChild(hint); - // P6 验收补完:项目行显示"最近活跃"相对时间 - const act = relTime(p.latest_session_at); - if (act) row.appendChild(el("span", { class: "help-hint" }, act)); - row.addEventListener("click", async () => { - $("new-session-dialog").close(); - await App.newSession({ projectPath: p.path }); - }); - list.appendChild(row); - }); - } catch (e) { - list.innerHTML = `
${_t("newSession.loadFailed", { msg: e.message })}
`; - } - } - - function initNewSessionDialog() { - $("new-session-cancel").addEventListener("click", () => $("new-session-dialog").close()); - $("new-session-new").addEventListener("click", async () => { - // 新建项目 = 选目录 → 注册(轻量命令隐式 _touch_project,不调 init_auto_evolve)→ 新建会话 - try { - const res = await window.emrg.pickProjectDir(); - if (res && res.path) { - await window.emrg.registerProject({ path: res.path }); - $("new-session-dialog").close(); - await App.newSession({ projectPath: res.path }); - } - } catch (e) { - Chat.addSystemMessage(_t("newSession.projectFailed", { msg: e.message })); - } - }); - } - - function closeConfirm() { - $("confirm-dialog").close(); - confirmCb = null; - } - - /** 确认按钮:执行回调后关闭 */ - function confirmOk() { - const cb = confirmCb; - $("confirm-dialog").close(); - confirmCb = null; - if (cb) cb(); - } - - return { - initThemeButtons, - initLangButtons, - renderLangOptions, - initModelForm, - initRenameDialog, - initGithubSection, - initDeviceDialog, - refreshGithubStatus, - initOpenSessionDialog, // P5:打开会话对话框初始化 - showOpenSessionDialog, // P5:两步打开会话 - initNewSessionDialog, // P5 slice 2:新建会话对话框初始化 - showNewSessionDialog, // P5 slice 2:新建会话(选项目) - initTaskManagement, // rant 18:23:15 P3:定时任务/自定义类型管理初始化 - loadTaskMeta, // rant 18:23:15 P3:任务/类型元数据加载(面板打开/测试复用) - renderTaskList, // rant 18:23:15 P3:任务列表渲染(测试/刷新复用) - refreshTaskListIfOpen, // rant 2026-08-21T17:41:23:任务面板开着才重渲染(聊天 /trigger 路径复用) - formatCountdown, // rant 10:36:39:倒计时格式化(≤60s "43s" / ≤1h "1m23s" / >1h "1h05m";测试复用) - updateTaskCountdowns, // rant 10:36:39:1s tick 更新倒计时文本(测试直接调用模拟走秒) - startTaskCountdown, // rant 10:36:39:启动 1s 倒计时(幂等;renderTaskList 自动调用) - stopTaskCountdown, // rant 10:36:39:停止并清引用(离开任务视图时 app.js 调用防泄漏) - startTaskPoll, // rant 2026-08-22T07:18:35:启动 5s 状态轮询(任务面板激活时 app.js 调用) - stopTaskPoll, // rant 2026-08-22T07:18:35:停止轮询(离开任务视图时 app.js 调用防泄漏) - renderProjectList, // rant 14:10:14 P5:项目面板列表渲染(测试/刷新复用) - showProjectSessionsInPanel, // rant 14:10:14 P5:项目面板内嵌会话列表(测试复用) - initRantPanel, // rant 14:10:14 P4:rant 面板初始化 - renderRantList, // rant 14:10:14 P4:rant 列表渲染(测试/刷新复用) - preprocessRantMarkdown, // rant 10:41:43:rant 详情 markdown 【】标题预处理(测试复用) - setRantFilter, // rant 14:10:14 P4:rant 状态筛选(测试复用) - openRantForm, // rant 14:10:14 P4:新建 rant 表单(测试复用) - submitRantForm, // rant 14:10:14 P4:rant 表单提交(测试复用) - saveTaskForm, // rant 18:23:15 P3:任务表单提交(测试复用) - saveTemplateForm, // rant 18:23:15 P3:类型表单提交(测试复用) - openTemplateForm, // rant 09:17:45:类型表单打开(内置只读查看 / 自定义编辑,测试复用) - closeTemplateForm, // rant 09:17:45:类型表单关闭(测试复用) - showRename, - submitRename, - showSettings, - saveSettings, - showWelcome, - saveWelcome, - showConfirm, - closeConfirm, - confirmOk, - get theme() { - return theme; - }, - set theme(v) { - theme = v; - }, - }; -})(); - -window.EMRG_Dialogs = Dialogs; diff --git a/emrg/gui/renderer/js/error-boundary.js b/emrg/gui/renderer/js/error-boundary.js deleted file mode 100644 index 5881ca99..00000000 --- a/emrg/gui/renderer/js/error-boundary.js +++ /dev/null @@ -1,209 +0,0 @@ -"use strict"; -/** - * error-boundary.js — 渲染进程全局错误边界(rant 2026-08-25T21:13:18)。 - * - * 最小侵入设计:不包裹/改写任何现有模块,只在全局监听层兜底。 - * - 独立文件,最先加载(排在 utils.js 之前);挂 window "error" + "unhandledrejection" 两个全局监听 - * - 捕获未处理错误:保留 console.error 原样日志 → 显示 fixed 全屏覆盖层(平时 hidden 零干扰) - * - 覆盖层:标题「渲染器出错」+ 错误摘要(截断 500 字符)+ 时间戳 + 两按钮(复制错误 / 重新加载) - * - 不做自动重载(防崩溃循环,参考 grok-bot error-boundary 只提供手动按钮) - * - 监听器不阻止错误传播(不 preventDefault / 不返回 false),页面其余行为不变 - * - 错误 handler 自身全程 try/catch,绝不在错误处理里再抛错 - * - i18n 双语(zh/en):show 时经 window.EMRG_I18N.t 解析(i18n.js 已加载); - * 若 i18n 未就绪则用内建 fallback 词典(navigator.language 判定) - * - 覆盖层 z-index 置顶;非错误时 DOM 隐藏,不拦截现有 UI 事件 - * - * DOM 获取统一走 getElementById(存在则复用)|| createElement(兜底创建), - * 使真实 DOM 与 vm 沙箱(getElementById 返回稳定元素)行为一致。 - */ - -(function () { - const OVERLAY_ID = "error-boundary-overlay"; - const TITLE_ID = "error-boundary-title"; - const SUMMARY_ID = "error-boundary-summary"; - const STAMP_ID = "error-boundary-timestamp"; - const COPY_ID = "error-boundary-copy"; - const RELOAD_ID = "error-boundary-reload"; - const MAX_SUMMARY = 500; - - // ── i18n fallback(window.EMRG_I18N 未就绪时兜底;正常路径走 EMRG_I18N.t) ── - const FALLBACK_DICTS = { - zh: { - "errorBoundary.title": "渲染器出错", - "errorBoundary.copy": "复制错误", - "errorBoundary.reload": "重新加载", - }, - en: { - "errorBoundary.title": "Renderer error", - "errorBoundary.copy": "Copy error", - "errorBoundary.reload": "Reload", - }, - }; - - let overlay = null; - - /** 取词:优先 window.EMRG_I18N(i18n.js 已加载),否则内建 fallback。 */ - function _t(key) { - try { - if (typeof window !== "undefined" && window.EMRG_I18N && window.EMRG_I18N.t) { - const s = window.EMRG_I18N.t(key); - if (s && s !== key) return s; - } - } catch { /* fall through */ } - const lang = (typeof navigator !== "undefined" && navigator.language || "").toLowerCase().startsWith("zh") - ? "zh" : "en"; - const dict = FALLBACK_DICTS[lang] || FALLBACK_DICTS.en; - return dict[key] || key; - } - - /** 取或建元素(getElementById 优先,createElement 兜底)。 */ - function _getOrCreate(id, tag) { - try { - const existing = document.getElementById(id); - if (existing) return existing; - } catch { /* fall through */ } - const el = document.createElement(tag || "div"); - el.id = id; - return el; - } - - /** 构建覆盖层 DOM(惰性,仅首次出错时创建;失败则仅保留日志)。 */ - function _ensureOverlay() { - if (overlay) return overlay; - try { - if (typeof document === "undefined") return null; - overlay = _getOrCreate(OVERLAY_ID, "div"); - overlay.style.position = "fixed"; - overlay.style.inset = "0"; - overlay.style.zIndex = "2147483647"; - overlay.style.display = "none"; - overlay.style.background = "rgba(0, 0, 0, 0.82)"; - overlay.style.color = "#fff"; - overlay.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"; - overlay.style.padding = "40px"; - overlay.style.boxSizing = "border-box"; - overlay.style.overflow = "auto"; - - const title = _getOrCreate(TITLE_ID, "div"); - title.style.fontSize = "22px"; - title.style.fontWeight = "600"; - title.style.marginBottom = "16px"; - overlay.appendChild(title); - - const summary = _getOrCreate(SUMMARY_ID, "pre"); - summary.style.whiteSpace = "pre-wrap"; - summary.style.wordBreak = "break-word"; - summary.style.fontSize = "13px"; - summary.style.lineHeight = "1.5"; - summary.style.maxHeight = "50vh"; - summary.style.overflow = "auto"; - summary.style.background = "rgba(255, 255, 255, 0.08)"; - summary.style.padding = "12px"; - summary.style.borderRadius = "6px"; - overlay.appendChild(summary); - - const stamp = _getOrCreate(STAMP_ID, "div"); - stamp.style.fontSize = "12px"; - stamp.style.opacity = "0.7"; - stamp.style.marginTop = "12px"; - overlay.appendChild(stamp); - - const actions = document.createElement("div"); - actions.style.marginTop = "20px"; - actions.style.display = "flex"; - actions.style.gap = "12px"; - - const copyBtn = _getOrCreate(COPY_ID, "button"); - copyBtn.type = "button"; - copyBtn.textContent = _t("errorBoundary.copy"); - copyBtn.style.padding = "8px 16px"; - copyBtn.style.borderRadius = "6px"; - copyBtn.style.border = "1px solid rgba(255,255,255,0.4)"; - copyBtn.style.background = "rgba(255,255,255,0.12)"; - copyBtn.style.color = "#fff"; - copyBtn.style.cursor = "pointer"; - copyBtn.addEventListener("click", function () { - try { - const summaryEl = document.getElementById(SUMMARY_ID); - const text = (summaryEl && summaryEl.textContent) || ""; - if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text).catch(function () {}); - } - } catch { /* never throw in handler */ } - }); - actions.appendChild(copyBtn); - - const reloadBtn = _getOrCreate(RELOAD_ID, "button"); - reloadBtn.type = "button"; - reloadBtn.textContent = _t("errorBoundary.reload"); - reloadBtn.style.padding = "8px 16px"; - reloadBtn.style.borderRadius = "6px"; - reloadBtn.style.border = "none"; - reloadBtn.style.background = "#2563eb"; - reloadBtn.style.color = "#fff"; - reloadBtn.style.cursor = "pointer"; - reloadBtn.addEventListener("click", function () { - try { - if (typeof location !== "undefined" && location.reload) location.reload(); - } catch { /* never throw in handler */ } - }); - actions.appendChild(reloadBtn); - - overlay.appendChild(actions); - // 沙箱/无 body 环境容忍:附加失败不阻断(真实 DOM 中挂到 body) - if (typeof document !== "undefined" && document.body && document.body.appendChild && !overlay.parentNode) { - document.body.appendChild(overlay); - } - } catch { overlay = null; } - return overlay; - } - - /** 显示覆盖层(惰性建 DOM,全程 try/catch;重复出错时更新内容)。 */ - function _show(rawMessage, rawError) { - try { - const el = _ensureOverlay(); - if (!el) return; - const msg = String(rawMessage || (rawError && rawError.message) || "unknown error"); - const detail = rawError && rawError.stack ? String(rawError.stack) : ""; - const full = (detail ? detail + "\n" : "") + msg; - const truncated = full.length > MAX_SUMMARY ? full.slice(0, MAX_SUMMARY) + "\n…" : full; - - const titleEl = document.getElementById(TITLE_ID) || el; - const summaryEl = document.getElementById(SUMMARY_ID) || el; - const stampEl = document.getElementById(STAMP_ID) || el; - titleEl.textContent = _t("errorBoundary.title"); - summaryEl.textContent = truncated; - try { - stampEl.textContent = new Date().toLocaleString(); - } catch { - stampEl.textContent = String(Date.now()); - } - // 重新渲染按钮文案(语言可能已切换) - const copyBtn = document.getElementById(COPY_ID); - if (copyBtn) copyBtn.textContent = _t("errorBoundary.copy"); - const reloadBtn = document.getElementById(RELOAD_ID); - if (reloadBtn) reloadBtn.textContent = _t("errorBoundary.reload"); - - el.style.display = "block"; - } catch { /* 错误处理里绝不抛错 */ } - } - - // ── 全局监听:保留 console.error 原样日志;不 preventDefault / 不返回 false(不阻断传播) ── - if (typeof window !== "undefined") { - window.addEventListener("error", function (event) { - try { - console.error("uncaught error:", event && event.error, event && event.message); - _show((event && event.message) || "", event && event.error); - } catch { /* never throw in handler */ } - }); - - window.addEventListener("unhandledrejection", function (event) { - try { - const reason = event && event.reason; - const msg = reason instanceof Error ? reason.message : String(reason); - console.error("unhandled rejection:", reason); - _show(msg, reason instanceof Error ? reason : null); - } catch { /* never throw in handler */ } - }); - } -})(); diff --git a/emrg/gui/renderer/js/file-tree.js b/emrg/gui/renderer/js/file-tree.js deleted file mode 100644 index ff1c7a64..00000000 --- a/emrg/gui/renderer/js/file-tree.js +++ /dev/null @@ -1,233 +0,0 @@ -"use strict"; -/** - * file-tree.js — 工作区文件浏览器(workspace panel P3.1,rant 2026-08-11T12:20:35; - * VS Code 风格 UI 对齐,rant 2026-08-12T17:28:19) - * - * Tab「文件」内容:懒加载目录树,视觉对齐 VS Code Explorer: - * - 根 = 当前激活会话 projectPath(app.js 经 FileTree.setSession(sid, path) 注入) - * - 目录行点击 → emrg.listFiles(path) 懒加载子项(已加载目录缓存,折叠不重新拉取) - * - 文件行点击 → ResultPanel.openFileTab(sid, path)(P2.2 框架 Tab + P3.3 查看器) - * - 文件类型图标(mono 内联 SVG,fill=currentColor 跟随文字色):目录折叠/展开 - * 文件夹图标 + 按扩展名映射文件图标(js/ts/py/md/json/css/html/yml/图片/文本), - * 默认通用文件图标 - * - 递归渲染 + 深度缩进(每层 padding-left = 8 + depth*16,行内 --indent CSS 变量) - * - 选中态:文件行点击 → .active(背景 --bg-soft + 左侧 2px accent 竖条,单选) - * - 紧凑 24px 行高 + 超长名省略号(.ft-name ellipsis) - * - 缩进引导线(.ft-kids::before 1px 竖线,嵌套结构天然对齐父箭头列) - * - 展开态持久:Map(重新 render 不丢,切会话清空) - * - 滚动条 hover 显示(.result-files overlay thumb,默认透明) - * - 排序(目录在前)/5000 截断/符号链接不展开/绝对路径校验由 daemon 保证(P1 #661) - * 切会话 → setSession 重设根 + 清缓存(per-session 隔离) - */ - -const FileTree = (() => { - let sid = null; - let root = ""; - const cache = new Map(); // path -> { loaded, loading, error, entries } - const expanded = new Map(); // path -> bool(展开态持久,VS Code 行为) - let selectedPath = null; // 当前选中文件行(单选) - - function container() { return $("result-files"); } - function t(key) { return window.EMRG_I18N ? window.EMRG_I18N.t(key) : key; } - - // ── mono 内联 SVG 图标(16x16 viewBox,fill: currentColor 跟随文字色) ── - const ICON = { - chevronRight: '', // 折叠 ▸(目录可展开提示) - chevronDown: '', // 展开 ▾ - dirClosed: '', - dirOpen: '', - fileDefault: '', - fileImg: '', - fileMd: '', - fileJson: '', - fileCode: '', - fileYml: '', - fileTxt: '', - }; - const EXT_ICON = { - png: "fileImg", jpg: "fileImg", jpeg: "fileImg", gif: "fileImg", svg: "fileImg", - webp: "fileImg", bmp: "fileImg", ico: "fileImg", - md: "fileMd", markdown: "fileMd", - json: "fileJson", - js: "fileCode", jsx: "fileCode", mjs: "fileCode", cjs: "fileCode", - ts: "fileCode", tsx: "fileCode", - html: "fileCode", htm: "fileCode", css: "fileCode", scss: "fileCode", - py: "fileCode", rb: "fileCode", go: "fileCode", rs: "fileCode", java: "fileCode", c: "fileCode", cpp: "fileCode", - yml: "fileYml", yaml: "fileYml", toml: "fileYml", cfg: "fileYml", ini: "fileYml", - txt: "fileTxt", log: "fileTxt", csv: "fileTxt", tsv: "fileTxt", - }; - - function iconSvg(d, cls) { - const svg = el("svg", { class: cls || "ft-icon", viewBox: "0 0 16 16", width: 14, height: 14, "aria-hidden": "true" }); - svg.innerHTML = d; // 静态白名单路径,无注入风险 - return svg; - } - function chevronFor(entry) { - return expanded.get(entry.path) ? ICON.chevronDown : ICON.chevronRight; - } - function iconFor(entry) { - if (entry.type === "dir") return expanded.get(entry.path) ? ICON.dirOpen : ICON.dirClosed; - const ext = String(entry.name).split(".").pop().toLowerCase(); - return ICON[EXT_ICON[ext] || "fileDefault"]; - } - - function ensure(path) { - let st = cache.get(path); - if (!st) { st = { loaded: false, loading: false, error: false, entries: [] }; cache.set(path, st); } - return st; - } - - /** 设置会话根目录(切会话调用:重设根 + 清缓存 + 清展开态/选中态) */ - function setSession(s, rootPath) { - sid = s || null; - setRoot(rootPath); - } - - function setRoot(path) { - root = path || ""; - cache.clear(); - expanded.clear(); - selectedPath = null; - render(); - } - - /** 拉取 + 渲染目录子项(st 为 ensure(path) 的缓存态;childDepth = 子项缩进层级) */ - async function expandDir(path, kidsEl, st, childDepth) { - if (!st.loaded && !st.loading) { - st.loading = true; - kidsEl.innerHTML = ""; - kidsEl.appendChild(el("div", { class: "ft-hint" }, t("result.treeLoading"))); - try { - const res = await window.emrg.listFiles({ path }); - st.entries = (res && res.entries) || []; - st.loaded = true; - st.error = false; - } catch { - st.entries = []; - st.loaded = true; - st.error = true; - } finally { - st.loading = false; - } - } - kidsEl.innerHTML = ""; - if (st.error) { - kidsEl.appendChild(el("div", { class: "ft-hint" }, t("result.treeLoadFailed"))); - return; - } - for (const e of st.entries) kidsEl.appendChild(renderEntry(e, childDepth)); - } - - /** 渲染一条目行(depth 相对其父容器;padding-left = 8 + depth*16 对齐箭头列) */ - function renderEntry(entry, depth) { - const row = el("div", { - class: "ft-row" + (entry.type === "dir" ? " ft-dir" : " ft-file"), - dataset: { path: entry.path }, - }); - row.style.paddingLeft = (8 + depth * 16) + "px"; - // 行头(图标+名称)单独包装:.ft-row 为块级容器(head + kids 纵向排布), - // 修复定高 flex-wrap 下兄弟行与展开子项重叠的布局 bug(headless Chrome 像素实证) - const head = el("div", { class: "ft-head" }); - if (entry.type === "dir") { - head.appendChild(iconSvg(chevronFor(entry), "ft-chevron")); - } - head.appendChild(iconSvg(iconFor(entry))); - head.appendChild(el("span", { class: "ft-name" }, entry.name)); - row.appendChild(head); - if (entry.type === "dir") { - const kids = el("div", { class: "ft-kids" + (expanded.get(entry.path) ? "" : " hidden") }); - row.appendChild(kids); - const st = ensure(entry.path); - row.addEventListener("click", (e) => { - if (e && e.stopPropagation) e.stopPropagation(); - toggleDir(entry.path, kids, st, depth + 1); - }); - if (expanded.get(entry.path) && !st.loaded) expandDir(entry.path, kids, st, depth + 1); - } else { - if (selectedPath === entry.path) row.classList.add("active"); - row.addEventListener("click", (e) => { - if (e && e.stopPropagation) e.stopPropagation(); - selectFile(entry.path); - openFile(entry.path); - }); - } - return row; - } - - /** 目录展开/折叠:状态持久到 expanded Map(VS Code 行为,重新 render 不丢) */ - async function toggleDir(path, kidsEl, st, childDepth) { - const wasHidden = kidsEl.classList.contains("hidden"); - if (wasHidden) { - expanded.set(path, true); - await expandDir(path, kidsEl, st, childDepth); - kidsEl.classList.remove("hidden"); - } else { - expanded.set(path, false); - kidsEl.classList.add("hidden"); - } - // 目录行图标随展开态切换(重新渲染子行不影响兄弟行) - const row = kidsEl.parentNode; - if (row) { - const ic = row.querySelector(".ft-icon"); - if (ic) ic.innerHTML = expanded.get(path) ? ICON.dirOpen : ICON.dirClosed; - const chev = row.querySelector(".ft-chevron"); - if (chev) chev.innerHTML = expanded.get(path) ? ICON.chevronDown : ICON.chevronRight; - } - } - - /** 文件行选中(单选):去旧 active 加新 active */ - function selectFile(path) { - const c = container(); - if (!c) return; - const rows = c.querySelectorAll(".ft-file"); - for (const r of rows) { - if (r.dataset && r.dataset.path === selectedPath) r.classList.remove("active"); - } - selectedPath = path; - for (const r of rows) { - if (r.dataset && r.dataset.path === path) r.classList.add("active"); - } - } - - function openFile(path) { - if (window.ResultPanel && window.ResultPanel.openFileTab) { - window.ResultPanel.openFileTab(sid, path); - } - } - - function render() { - const c = container(); - if (!c) return; - c.innerHTML = ""; - if (!root) { - c.appendChild(el("div", { class: "result-empty" }, t("result.filesEmpty"))); - return; - } - const rootName = String(root).split(/[\\/]/).filter(Boolean).pop() || root; - expanded.set(root, true); // 根默认展开 - const rootRow = el("div", { class: "ft-row ft-dir ft-root", dataset: { path: root } }); - rootRow.style.paddingLeft = "8px"; - const rootHead = el("div", { class: "ft-head" }); - rootHead.appendChild(iconSvg(ICON.chevronDown, "ft-chevron")); // 根默认展开 → ▾ - rootHead.appendChild(iconSvg(ICON.dirOpen)); - rootHead.appendChild(el("span", { class: "ft-name" }, rootName)); - rootRow.appendChild(rootHead); - const kids = el("div", { class: "ft-kids" }); - rootRow.appendChild(kids); - // 根目录行与普通目录行一致的可展开/收起(rant 2026-08-13T12:47:18:render() 手写根行漏绑 - // click → 点击根目录无反应永远展开;toggleDir 已负责 expanded Map 持久 + 图标切换) - const st = ensure(root); - rootRow.addEventListener("click", (e) => { - if (e && e.stopPropagation) e.stopPropagation(); - toggleDir(root, kids, st, 1); - }); - c.appendChild(rootRow); - // 根自动展开(fire-and-forget;根子项 depth=1) - expandDir(root, kids, st, 1); - } - - return { setRoot, setSession }; -})(); - -// ⚠️ 必须暴露到 window(app.js 独立