@chanlerdev/scorel 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +356 -69
  2. package/dist/index.js +4237 -1759
  3. package/dist/index.js.map +4 -4
  4. package/docs/CHANGELOG.md +62 -0
  5. package/docs/ROADMAP.md +93 -9
  6. package/docs/SHIP.md +9 -3
  7. package/docs/spec/channels.md +97 -100
  8. package/docs/spec/client.md +11 -5
  9. package/docs/spec/extensions.md +115 -43
  10. package/docs/spec/ship/S0062-npm-package-and-release-workflow.md +3 -0
  11. package/docs/spec/ship/S0063-ai-release-notes.md +129 -0
  12. package/docs/spec/ship/S0064-gui-product-intent-and-boundary.md +79 -0
  13. package/docs/spec/ship/S0065-gui-electron-shell-and-embedded-host.md +73 -0
  14. package/docs/spec/ship/S0066-gui-local-project-workspace.md +79 -0
  15. package/docs/spec/ship/S0067-gui-relay-device-and-remote-project-selection.md +97 -0
  16. package/docs/spec/ship/S0068-gui-codex-app-polish-and-e2e.md +102 -0
  17. package/docs/spec/ship/S0068-gui-e2e-verification.md +50 -0
  18. package/docs/spec/ship/S0069-gui-codex-ui-refactor.md +371 -0
  19. package/docs/spec/ship/S0070-gui-streaming-and-tool-blocks.md +202 -0
  20. package/docs/spec/ship/S0071-gui-visual-fidelity-and-settings-shell.md +360 -0
  21. package/docs/spec/ship/S0072-gui-glass-sidebar-and-picker-anchoring.md +116 -0
  22. package/docs/spec/ship/S0073-provider-model-profile-contract.md +241 -0
  23. package/docs/spec/ship/S0074-gui-model-provider-settings-split.md +113 -0
  24. package/docs/spec/ship/S0075-provider-catalog-model-cards.md +93 -0
  25. package/docs/spec/ship/S0076-provider-modal-search-and-direct-key.md +70 -0
  26. package/docs/spec/ship/S0077-auxiliary-session-title-generation.md +95 -0
  27. package/docs/spec/ship/S0078-gui-provider-settings-forward-config-and-simplification.md +150 -0
  28. package/docs/spec/ship/S0079-gui-sidebar-layout-controls.md +49 -0
  29. package/docs/spec/ship/S0080-session-title-hook-and-gui-markdown-dark-code.md +58 -0
  30. package/docs/spec/ship/S0081-automatic-memory.md +117 -0
  31. package/docs/spec/ship/S0082-memory-journal-tool-and-idle-dream.md +107 -0
  32. package/docs/spec/ship/S0083-extension-manifest-and-im-channel-runtime.md +338 -0
  33. package/docs/spec/ship/S0084-built-in-telegram-im-extension.md +188 -0
  34. package/docs/spec/ship/S0085-gui-im-extension-settings.md +47 -0
  35. package/docs/spec/ship/S0086-auto-compact-and-session-memory.md +124 -0
  36. package/extensions/builtin/loopback/adapter.js +13 -0
  37. package/extensions/builtin/loopback/scorel.extension.json +7 -0
  38. package/extensions/builtin/loopback/skills/loopback/SKILL.md +7 -0
  39. package/extensions/builtin/telegram/adapter.d.ts +43 -0
  40. package/extensions/builtin/telegram/adapter.js +252 -0
  41. package/extensions/builtin/telegram/scorel.extension.json +7 -0
  42. package/extensions/builtin/telegram/skills/telegram/SKILL.md +9 -0
  43. package/package.json +6 -2
@@ -0,0 +1,97 @@
1
+ # S0067: GUI Relay Device And Remote Project Selection
2
+
3
+ ## Goal
4
+
5
+ Add the remote half of the GUI product model: Settings can add Relay Devices, and Add Project can browse a selected Relay Device and add only explicitly chosen remote Projects to the GUI Project list.
6
+
7
+ ## Scope
8
+
9
+ - Add Settings UI for Relay Device management.
10
+ - Support the existing Relay pairing / authorization model.
11
+ - Persist GUI Device records and connector metadata locally.
12
+ - Connect to a Relay Device through `RelayTransport` / `DaemonClient`.
13
+ - Add Project flow can choose:
14
+ - Local embedded Host.
15
+ - A configured Relay Device.
16
+ - Browse remote directories through the selected Relay Device Host.
17
+ - Register the selected remote Project on that Host.
18
+ - Persist GUI-visible remote Project selection as `deviceId + projectId`.
19
+ - Show selected remote Projects in the main Project list alongside local Projects.
20
+ - Remote Project list must not automatically include every Project in the remote Host Registry.
21
+
22
+ ## Non-Goals
23
+
24
+ - Do not implement SSH Remote Device.
25
+ - Do not implement direct WS + token as a GUI path.
26
+ - Do not add account login or OAuth.
27
+ - Do not move remote Project authority into Relay.
28
+ - Do not let Relay store Project, Session, prompt, tool result, Runtime, JSONL, or replay cache.
29
+
30
+ ## Acceptance Criteria
31
+
32
+ - Settings can add a Relay Device using the official Relay default unless overridden for development.
33
+ - GUI can connect to the configured Relay Device and show connection status.
34
+ - Add Project can browse directories on the selected Relay Device.
35
+ - Registering a remote Project stores the returned `projectId` and marks it visible in GUI.
36
+ - The main Project list shows only GUI-selected remote Projects for that Relay Device.
37
+ - Removing or hiding a GUI-selected remote Project does not delete the remote Host Project unless a later spec explicitly adds destructive management.
38
+ - Local Projects remain all-visible.
39
+
40
+ ## Test Requirements
41
+
42
+ Run:
43
+
44
+ ```bash
45
+ pnpm --filter @scorel/app-gui build
46
+ pnpm --filter @scorel/app-gui typecheck
47
+ pnpm --filter @scorel/app-gui test
48
+ pnpm typecheck
49
+ pnpm test
50
+ git diff --check
51
+ ```
52
+
53
+ Manual smoke must use:
54
+
55
+ - real Relay service or local Relay process
56
+ - real Host connected outbound to Relay
57
+ - real Relay Device configuration in GUI
58
+ - real remote directory browsing through Relay
59
+ - real remote Project registration
60
+
61
+ ## Implementation Notes
62
+
63
+ - GUI Settings creates Relay pair codes as the entry client `client_gui`.
64
+ - The existing Host-side pairing path remains authoritative: the remote Host still runs `scorel pair <pair-code>` or the same `redeem_pair` flow to authorize the GUI client.
65
+ - GUI Relay Device records and GUI-visible remote Project selections are stored locally in `~/.scorel/gui/gui-store.json`.
66
+ - Remote Project visibility is explicit:
67
+ - local Projects are listed from the embedded local Host Registry.
68
+ - remote Projects are listed only from GUI-selected `deviceId + projectId` records.
69
+ - `list_projects` from the remote Host is not used as the GUI main Project list.
70
+ - Relay connections are owned by Electron main process through `RelayTransport` and `DaemonClient`; renderer only receives sanitized device/project/session data through IPC.
71
+
72
+ ## Verification
73
+
74
+ - `pnpm --filter @scorel/app-gui typecheck` passed.
75
+ - `pnpm --filter @scorel/app-gui test` passed.
76
+ - `pnpm --filter @scorel/app-gui build` passed.
77
+ - `pnpm typecheck` passed.
78
+ - `pnpm test` passed.
79
+ - `pnpm pack:smoke` passed.
80
+ - `git diff --check` passed.
81
+ - `pnpm gui` loaded Electron without a main/renderer load error and exited cleanly after SIGTERM.
82
+ - Focused Relay tests use a real local Relay server, a real `HostRelayClient`, real remote directory browsing, and real remote Project registration.
83
+ - Full visual Relay e2e is intentionally left to S0068, which owns Codex App polish and local + Relay end-to-end verification.
84
+
85
+ ## Affected Paths
86
+
87
+ - `apps/gui/**`
88
+ - `packages/client/**`
89
+ - `packages/protocol/**` only if GUI-visible selection requires shared types
90
+ - `docs/ROADMAP.md`
91
+ - `docs/spec/ship/S0067-gui-relay-device-and-remote-project-selection.md`
92
+
93
+ ## Risks
94
+
95
+ - Remote Project visibility can accidentally collapse back to WebUI behavior. The invariant is: remote Host Registry is not the GUI main Project list.
96
+ - Pairing and Device persistence can leak secrets. Do not log Relay tokens or full connector secrets.
97
+ - Relay may be mistaken for a cloud backend. It remains only proxy + authorization registry.
@@ -0,0 +1,102 @@
1
+ # S0068: GUI Codex App Polish And E2E
2
+
3
+ ## Goal
4
+
5
+ Bring the first GUI milestone to a credible Codex App-style desktop experience and verify both local and Relay Project paths end to end with real product resources.
6
+
7
+ ## Scope
8
+
9
+ - Polish the GUI visual system and interaction model against Codex App expectations:
10
+ - quiet workbench layout
11
+ - dense but readable Project / Session navigation
12
+ - restrained controls
13
+ - clear connection and running states
14
+ - no marketing-style landing page
15
+ - Align reusable styling with `docs/design.md` where it still applies.
16
+ - Add explicit empty, loading, offline, disconnected, and error states.
17
+ - Validate local Project path end to end.
18
+ - Validate Relay Project path end to end.
19
+ - Document verification evidence.
20
+ - Mark M9 Done when all prior M9 specs are complete and verified.
21
+
22
+ ## Non-Goals
23
+
24
+ - Do not add SSH Remote Device.
25
+ - Do not add HTTP API.
26
+ - Do not add plugin / automation / file explorer / diff viewer unless already required by a prior M9 spec.
27
+ - Do not ship production desktop installers unless a dedicated release spec is created.
28
+
29
+ ## Acceptance Criteria
30
+
31
+ - GUI feels like a desktop workbench, not a browser page wrapped in a window.
32
+ - Local Project: add/select Project, create Session, send prompt, see persisted response.
33
+ - Relay Project: add Relay Device, select remote Project, create/open Session, send prompt, see persisted response.
34
+ - Offline Relay Device and disconnected Host states are visible and recoverable without app crash.
35
+ - Text fits in project/session rows and composer controls across expected desktop window sizes.
36
+ - M9 ROADMAP status can be moved to Done only after this spec and earlier M9 specs pass verification.
37
+
38
+ ## Test Requirements
39
+
40
+ Run:
41
+
42
+ ```bash
43
+ pnpm --filter @scorel/app-gui build
44
+ pnpm --filter @scorel/app-gui typecheck
45
+ pnpm --filter @scorel/app-gui test
46
+ pnpm typecheck
47
+ pnpm test
48
+ pnpm pack:smoke
49
+ pnpm verify:m9-gui
50
+ git diff --check
51
+ ```
52
+
53
+ Manual e2e must use:
54
+
55
+ - real GUI app
56
+ - real embedded local Host
57
+ - real Relay path
58
+ - real JSONL sessions
59
+ - real provider
60
+
61
+ If browser or screenshot tooling is used for renderer verification, record viewport/window sizes and include screenshots or notes in the verification artifact.
62
+
63
+ ## Affected Paths
64
+
65
+ - `apps/gui/**`
66
+ - `docs/design.md` only if GUI-specific design rules need to be added
67
+ - `docs/ROADMAP.md`
68
+ - `docs/spec/ship/S0068-gui-codex-app-polish-and-e2e.md`
69
+ - optional verification artifact under `docs/spec/ship/`
70
+
71
+ ## Implementation Notes
72
+
73
+ - The GUI uses the `docs/design.md` token set directly in `apps/gui/src/index.html`.
74
+ - Renderer states now distinguish:
75
+ - no Project
76
+ - Project without Session
77
+ - busy/loading
78
+ - local Host error
79
+ - Relay Device online/offline
80
+ - Relay Device absent before remote browse
81
+ - Composer and Project/Session rows use fixed dimensions, text truncation, focus states, and restrained status chips.
82
+ - The S0067 product model is unchanged: local Projects remain all-visible; remote Projects are still shown only after GUI selection.
83
+
84
+ ## Verification
85
+
86
+ See [`S0068-gui-e2e-verification.md`](S0068-gui-e2e-verification.md).
87
+
88
+ Current status:
89
+
90
+ - GUI polish: complete.
91
+ - Automated local product-path verification: complete.
92
+ - Automated Relay product-path verification: complete.
93
+ - Electron launch smoke: complete.
94
+ - Real-provider GUI e2e: complete.
95
+
96
+ M9 can be marked Done because all S0064-S0068 specs are complete and `pnpm verify:m9-gui` passed with provider credentials.
97
+
98
+ ## Risks
99
+
100
+ - Codex App parity can become an unbounded visual target. S0068 should polish the first M9 workflow, not invent every Codex feature.
101
+ - E2E can pass locally while desktop packaging is still unsolved. Packaging is separate unless explicitly added.
102
+ - UI reuse from WebUI can carry browser-specific assumptions. GUI should reuse implementation only when it supports the desktop product model.
@@ -0,0 +1,50 @@
1
+ # S0068 GUI E2E Verification
2
+
3
+ Date: 2026-06-08
4
+
5
+ ## Automated Product-Path Evidence
6
+
7
+ - `pnpm --filter @scorel/app-gui typecheck` passed.
8
+ - `pnpm --filter @scorel/app-gui test` passed.
9
+ - `pnpm --filter @scorel/app-gui build` passed.
10
+ - GUI local Host tests cover Project registration, Session creation, prompt send, persisted assistant response, and session-scoped transcript filtering.
11
+ - GUI Relay tests cover a real local Relay server, real `HostRelayClient`, authorized Relay Device discovery, remote directory browsing, explicit remote Project selection, remote Session creation, prompt send through Relay, and persisted assistant response.
12
+
13
+ Automated prompt tests use a deterministic test provider so CI stays stable. They still use the real Host, Relay, `DaemonClient`, JSONL, and Project registry path.
14
+
15
+ ## Desktop Smoke
16
+
17
+ - `pnpm gui` launched Electron without main-process or renderer load errors.
18
+ - Electron exited cleanly after SIGTERM.
19
+ - Screenshot capture was attempted at desktop size, but the macOS automation permission dialog covered the app window. The screenshot was not committed because it contained unrelated desktop/browser content.
20
+
21
+ ## Real Provider Smoke
22
+
23
+ Formal verifier:
24
+
25
+ ```bash
26
+ pnpm verify:m9-gui
27
+ ```
28
+
29
+ The verifier covers:
30
+
31
+ - real provider config loaded through `loadScorelConfig`.
32
+ - GUI embedded local Host service.
33
+ - local Project registration, Session creation, prompt send, and persisted assistant response.
34
+ - local Relay server.
35
+ - GUI Relay pair session creation.
36
+ - Host-side Relay pair redemption.
37
+ - remote Host outbound Relay connection through `HostRelayClient`.
38
+ - GUI Relay Device discovery.
39
+ - explicit remote Project selection.
40
+ - remote Session creation, prompt send through Relay, and persisted assistant response.
41
+
42
+ Result:
43
+
44
+ ```text
45
+ ok: true
46
+ local projectId=prj_109119eb-9401-4d62-ba46-052e1a128fae sessionId=ses_62d6865d-67d7-4f77-9e27-375946ae4582 eventCount=5
47
+ relay deviceId=device_gui_m9_remote projectId=prj_d57f2c62-44dc-42ab-b21c-0fe151053109 sessionId=ses_d85f7028-daa9-406a-bfd7-77f159d0d031 eventCount=7
48
+ ```
49
+
50
+ This proves the real-provider GUI prompt smoke completed for both the embedded local Host path and the Relay remote Project path. The verification command did not persist provider credentials into the repository.
@@ -0,0 +1,371 @@
1
+ # S0069: GUI Codex App UI Refactor (Skeleton + Markdown + Tool Block Registry)
2
+
3
+ ## Goal
4
+
5
+ Rebuild the M9 GUI renderer to match Codex App reference screenshots (image2-6): three-segment sidebar with project-inline sessions, pill composer, project picker pill with overlay, Add Remote Project modal, dedicated Settings view. Establish the foundation for Markdown rendering and an event-driven tool-block component registry. Add a streaming IPC channel so future streaming UX (S0070) does not require another IPC reshape.
6
+
7
+ This spec replaces the single-file `apps/gui/src/renderer.tsx` (559 lines, character-icon, two-list sidebar, embedded settings popover) with a modular renderer tree under `apps/gui/src/renderer/`.
8
+
9
+ Source of truth for product model is M9 (`S0064`–`S0068`); this spec is M9 follow-up polish, not a product re-scoping.
10
+
11
+ Reference screenshots:
12
+
13
+ - image2 — populated session view with topbar title, action chips, review banner.
14
+ - image3 — empty state with large H1, pill composer, project picker mini pill below.
15
+ - image4 — project picker overlay: search, list, "add local / add remote / no project".
16
+ - image5 — Add Remote Project modal: host dropdown, path input + reset, directory list, cancel / add.
17
+ - image6 — sidebar with each project inline-expanding its own sessions.
18
+
19
+ User-explicit deletions from the Codex reference (do **not** ship):
20
+
21
+ - empty-state "connect messaging / email / files" plugin recommendation cards.
22
+ - bottom-of-sidebar global "对话" history group.
23
+ - in-composer review banner (`2 个文件已更改 +95 -1` / `审查`).
24
+ - "不使用项目" (null project / workspace mode) — GUI is Project-first per `S0064`.
25
+ - real model-picker / mic / "完全访问" toggling — all stay disabled placeholders (Codex semantic).
26
+
27
+ ---
28
+
29
+ ## Scope
30
+
31
+ ### 1. Renderer tree
32
+
33
+ Replace `apps/gui/src/renderer.tsx` and the inline `<style>` block in `apps/gui/src/index.html` with:
34
+
35
+ ```
36
+ apps/gui/src/renderer/
37
+ ├── main.tsx # ReactDOM root
38
+ ├── App.tsx # view-mode router (workspace | settings)
39
+ ├── styles.css # tokens + base + layout, replaces 553-line inline style
40
+ ├── tokens.ts # token names exported for component literals
41
+ ├── shell/
42
+ │ ├── Sidebar.tsx
43
+ │ ├── ProjectTree.tsx # one project row + inline session list + collapse
44
+ │ ├── SidebarActionRow.tsx # `+ 新对话` / 搜索 / 插件 / 自动化
45
+ │ └── use-collapsed.ts # localStorage["scorel.gui.collapsed"]: { [key]: boolean }
46
+ ├── workspace/
47
+ │ ├── Workspace.tsx # empty | session
48
+ │ ├── EmptyState.tsx # H1 + composer + project picker pill
49
+ │ ├── SessionView.tsx # transcript + composer
50
+ │ └── Topbar.tsx # right-only stack icon (no title bar)
51
+ ├── composer/
52
+ │ ├── Composer.tsx
53
+ │ ├── ProjectPickerPill.tsx
54
+ │ ├── ProjectPickerMenu.tsx # overlay: search + list + add local / add remote
55
+ │ └── AddRemoteProjectDialog.tsx # image5 modal
56
+ ├── chatbox/
57
+ │ ├── Transcript.tsx
58
+ │ ├── projector.ts # copy of apps/webui/lib/events/projector.ts (independent)
59
+ │ ├── delta-batch.ts # copy (S0070 will activate)
60
+ │ ├── TurnUser.tsx
61
+ │ ├── TurnAssistant.tsx
62
+ │ ├── TurnHarness.tsx
63
+ │ ├── Markdown.tsx # react-markdown + remark-gfm + rehype-sanitize
64
+ │ ├── ShikiCodeBlock.tsx # lazy shiki, github-light-default
65
+ │ └── tool-blocks/
66
+ │ ├── registry.ts
67
+ │ ├── ToolBlock.tsx # routes (call, result?) → registry hit or DefaultJsonBlock
68
+ │ └── DefaultJsonBlock.tsx
69
+ ├── settings/
70
+ │ ├── SettingsPage.tsx
71
+ │ └── RelayDevicesPanel.tsx
72
+ └── icons/
73
+ └── index.ts # lucide-react re-exports (Plus, Search, Puzzle, Clock,
74
+ # Settings, Folder, ChevronRight, ChevronDown, X, Mic,
75
+ # ArrowUp, Square, Globe, RotateCcw, Check)
76
+ ```
77
+
78
+ Renderer entry moves to `apps/gui/src/renderer/main.tsx`. Old `apps/gui/src/renderer.tsx` deleted.
79
+
80
+ ### 2. Design tokens
81
+
82
+ Token values copied verbatim from `docs/design.md` §2 (locked palette, sans-only fonts, five-step type ladder, spacing, radii, shadow disabled). Tokens defined in `:root` of `styles.css`. Dark-mode placeholder via `@media (prefers-color-scheme: dark)` block kept commented out.
83
+
84
+ ### 3. Sidebar — three-segment, project inline sessions
85
+
86
+ ```
87
+ ┌─ 280px Sidebar (--color-surface) ──────────┐
88
+ │ + 新对话 [active route /] │
89
+ │ 🔍 搜索 [disabled] │
90
+ │ 🧩 插件 [disabled] │
91
+ │ 🤖 自动化 [disabled] │
92
+ │ │
93
+ │ PROJECTS (12px caps) │
94
+ │ │
95
+ │ ▾ workspace │
96
+ │ 梳理对比分析框架 ⌘1 │
97
+ │ ▸ warp │
98
+ │ ▸ Scorel │
99
+ │ ▸ Scorel Chanler... ●online │
100
+ │ ▸ Tickel │
101
+ │ ▸ docx-pure │
102
+ │ ... │
103
+ │ │
104
+ │ ⚙ 设置 (mobile icon) │
105
+ └────────────────────────────────────────────┘
106
+ ```
107
+
108
+ - Project row click = toggle collapse, **not** navigate. Active project derives from active session route only.
109
+ - Session row click = open session in workspace.
110
+ - Collapse persists to `localStorage["scorel.gui.collapsed"]`.
111
+ - `+ 新对话` row routes to workspace empty state with current project preselected.
112
+ - No `+` button next to "PROJECTS" header in this segment — add-project entrypoint is also exposed as a row at the bottom of the project picker overlay (sidebar `+` is dual-entry per user decision: keep one `+` button at the projects header for sidebar discoverability).
113
+ - No "对话" global history group. No theme toggle row.
114
+
115
+ ### 4. Composer
116
+
117
+ Pill, 24px radius, 1px `--color-border`, no shadow.
118
+
119
+ ```
120
+ ┌──────────────────────────────────────────────────┐
121
+ │ 随心输入 │
122
+ │ │
123
+ │ ⊕ ⚠ 完全访问 ▾ 5.5 超高 ▾ 🎤 ●│
124
+ └──────────────────────────────────────────────────┘
125
+ ▱ workspace ▾ ← project picker pill, BELOW composer
126
+ ```
127
+
128
+ - `⚠ 完全访问 ▾` rendered with `--color-status-warn`. Disabled placeholder.
129
+ - `5.5 超高 ▾` rendered as plain pill. Disabled placeholder.
130
+ - `⊕` and `🎤` disabled placeholders.
131
+ - Send button: 32×32 circle, `--color-accent` background, `↑` glyph. While `inFlight`, swaps to red square (`--color-status-err`) acting as cancel.
132
+ - `meta-row` removed from composer shell. Project picker pill is a sibling component below composer.
133
+
134
+ ### 5. Project picker pill + overlay
135
+
136
+ Pill: `▱ {project.displayName} ▾` rendered below composer. Click opens an overlay positioned beneath the pill (image4):
137
+
138
+ - Top: search input `🔍 搜索项目` (filters list).
139
+ - Middle: project list, each row `▱ name`, current selection trailing `✓`.
140
+ - Divider.
141
+ - `▱+ 添加本地项目 ›` → triggers `window.scorel.addLocalProject()` (existing IPC).
142
+ - `🌐 添加远程项目` → opens `AddRemoteProjectDialog`.
143
+ - "不使用项目" row **omitted** (user decision: GUI is Project-first).
144
+
145
+ ### 6. Add Remote Project modal
146
+
147
+ `AddRemoteProjectDialog.tsx` reproduces image5:
148
+
149
+ - Title `添加远程项目` + close (×).
150
+ - Subtitle `选择已连接的远程主机,并输入此项目的文件夹。`
151
+ - `远程主机` dropdown sourced from `state.relayDevices`.
152
+ - `文件夹路径` input + reset (`RotateCcw`) button.
153
+ - Directory listing (entries from `window.scorel.listRemoteDirectories(deviceId, path)`), click = navigate into.
154
+ - Footer hint `此远程文件夹将作为单独项目显示在侧边栏中。`
155
+ - `取消` / `添加项目` buttons. `添加项目` calls `window.scorel.addRemoteProject(deviceId, workDir)`.
156
+
157
+ ### 7. Settings independent view
158
+
159
+ In-renderer view-mode switch via `useState<"workspace" | "settings">("workspace")`. Sidebar `⚙ 设置` row sets mode to `"settings"`. Settings header has a back button (`ChevronLeft` + `返回`) restoring `"workspace"`. **No `react-router`**.
160
+
161
+ `SettingsPage.tsx` sections (S0069 ships only Relay Devices; future settings sections are future specs):
162
+
163
+ - Relay Devices: relay URL input, `Pair` button, `Refresh` button, displayed pair code on success, device list with online/offline pills. Functionality 1:1 from current `SettingsPopover` in `apps/gui/src/renderer.tsx:476-555`.
164
+
165
+ The Relay popover at the sidebar bottom is removed.
166
+
167
+ ### 8. Markdown pipeline
168
+
169
+ `Markdown.tsx`:
170
+
171
+ - `react-markdown` 10.x with `remark-gfm` 4.x and `rehype-sanitize` 6.x.
172
+ - Sanitize schema duplicates the allowlist in `apps/webui/components/chatbox/markdown-view.tsx` (allow `className` attribute, allow code-block class for highlighter).
173
+ - Code blocks dispatch to `ShikiCodeBlock.tsx`. Shiki 4.x lazy `createHighlighterCore` shared across renders, language loaded on demand, theme `github-light-default`. Fallback to plain `<pre><code>` while loading.
174
+
175
+ GUI-only adjustments vs webui copy: drop Tailwind class names; use plain CSS class names defined in `styles.css`.
176
+
177
+ ### 9. Tool-block registry (event-driven, generic)
178
+
179
+ `tool-blocks/registry.ts`:
180
+
181
+ ```ts
182
+ import type {
183
+ ToolCallContentBlock,
184
+ ToolResultContentBlock,
185
+ } from "@scorel/protocol";
186
+
187
+ export type ToolBlockProps = {
188
+ call: ToolCallContentBlock; // packages/protocol/src/messages.ts:13
189
+ result?: ToolResultContentBlock; // packages/protocol/src/messages.ts:21
190
+ pending: boolean; // result not yet arrived
191
+ };
192
+
193
+ export type ToolBlockComponent = React.ComponentType<ToolBlockProps>;
194
+
195
+ const registry = new Map<string, ToolBlockComponent>();
196
+ export function registerToolBlock(toolName: string, component: ToolBlockComponent): void;
197
+ export function lookupToolBlock(toolName: string): ToolBlockComponent;
198
+ ```
199
+
200
+ `ToolBlock.tsx` looks up by `call.toolName`; falls back to `DefaultJsonBlock` (renders `tool_call.args` and `tool_result.result` as JSON fences via `Markdown.tsx`).
201
+
202
+ `Transcript.tsx` projects `PersistentEvent[]` via `projector.ts`. The projector pairs `tool_call` content blocks with their `tool_result` blocks by `toolCallId`. Transcript renders each pair through `<ToolBlock />`.
203
+
204
+ S0069 ships **only** `DefaultJsonBlock`. Specialized blocks (Read / Glob / Grep / Edit / Write / Bash / TodoWrite) are S0070.
205
+
206
+ Adding a new tool renderer in any future spec is one line: `registerToolBlock("MyTool", MyToolBlock)` in module init. Main rendering path unchanged.
207
+
208
+ ### 10. Streaming IPC channel (forward-compatible)
209
+
210
+ Add channels and types so S0070 can light up streaming without another IPC pass:
211
+
212
+ `apps/gui/src/shared/ipc.ts`:
213
+
214
+ ```ts
215
+ export type GuiSessionEventPayload = {
216
+ sessionId: SessionId;
217
+ event: ScorelEvent;
218
+ };
219
+
220
+ export const guiIpcChannels = {
221
+ ...,
222
+ attachSession: "scorel:attachSession", // (project, sessionId) → void
223
+ detachSession: "scorel:detachSession", // (sessionId) → void
224
+ sessionEvent: "scorel:sessionEvent", // main → renderer push
225
+ } as const;
226
+
227
+ export type GuiApi = {
228
+ ...,
229
+ attachSession(project: GuiProjectRef, sessionId: SessionId): Promise<void>;
230
+ detachSession(sessionId: SessionId): Promise<void>;
231
+ onSessionEvent(handler: (payload: GuiSessionEventPayload) => void): () => void;
232
+ };
233
+ ```
234
+
235
+ `apps/gui/src/main.ts` keeps a `BrowserWindow` reference. On `attachSession`, look up the relevant `DaemonClient` and call `client.subscribe((event) => webContents.send("scorel:sessionEvent", { sessionId, event }))`; remember the unsubscribe by `sessionId`. On `detachSession`, run the saved unsubscribe.
236
+
237
+ `local-host.ts` and `relay-service.ts` expose internal `getClient()` / `subscribe(sessionId, handler)` helpers; no domain logic moves there.
238
+
239
+ `sendMessage` semantics narrow: it **starts** the prompt and resolves with `{ accepted: true }` once the daemon accepts. Live events arrive via the channel push. Renderer must `attachSession` before calling `sendMessage` (Transcript mounts → attach → send → receive events → unmount → detach).
240
+
241
+ Renderer behavior in S0069: attach + receive `PersistentEvent` snapshots and push them through the projector each frame (no batching, no cursor). Transient events (`text_delta` etc.) are ingested by the projector but not visualized — S0070 lights them up.
242
+
243
+ ### 11. esbuild build script
244
+
245
+ `apps/gui/scripts/build.mjs`:
246
+
247
+ - entry `src/renderer/main.tsx` (was `src/renderer.tsx`).
248
+ - add CSS loader: import `./styles.css` from `main.tsx`, esbuild bundles it to `.dist/renderer.css`. `index.html` adds `<link rel="stylesheet" href="renderer.css">`.
249
+
250
+ ### 12. package.json
251
+
252
+ Add to `apps/gui/package.json` dependencies:
253
+
254
+ ```
255
+ "lucide-react": "^0.488.0"
256
+ "react-markdown": "^10.1.0"
257
+ "remark-gfm": "^4.0.1"
258
+ "rehype-sanitize": "^6.0.0"
259
+ "shiki": "^4.1.0"
260
+ "@shikijs/rehype": "^4.1.0"
261
+ ```
262
+
263
+ Versions match `apps/webui/package.json`.
264
+
265
+ ---
266
+
267
+ ## Non-Goals
268
+
269
+ - No streaming cursor, RAF batcher, IntersectionObserver autoscroll, jump-to-bottom button. (S0070.)
270
+ - No specialized tool blocks (Read / Glob / Grep / Edit / Write / Bash / TodoWrite). All routes go through `DefaultJsonBlock`. (S0070.)
271
+ - No diff viewer. (S0070.)
272
+ - No model picker / 🎤 / `完全访问` real toggling. Disabled placeholders only.
273
+ - No dark-mode implementation (placeholder block only).
274
+ - No `react-router`. View-mode switch is local React state.
275
+ - No WebUI changes. Reverse-reuse of GUI components in WebUI is a future product direction, not a S0069 deliverable.
276
+ - No new Electron menu, no auto-update, no installer packaging.
277
+ - No new Host / Daemon / Relay / Protocol package change beyond the additive IPC channels.
278
+ - No removal of any existing IPC handler. `sendMessage` keeps responding (now with `{ accepted: true }` ack), so any callers staying on the old polling shape (none in this repo) would still build.
279
+
280
+ ---
281
+
282
+ ## Acceptance Criteria
283
+
284
+ - `apps/gui/src/renderer.tsx` deleted; `apps/gui/src/renderer/main.tsx` is the entry.
285
+ - `apps/gui/src/index.html` has no `<style>` block; layout/colors come from `renderer.css`.
286
+ - Empty state matches image3: H1 reads `我们应该在 {project.displayName} 中做些什么?` (project-aware, falls back to `我们应该构建什么?` when no project resolves), pill composer, project picker pill below, no recommendation cards, no topbar title.
287
+ - Project picker overlay matches image4: search box, list with current `✓`, `添加本地项目` and `添加远程项目` rows, no "不使用项目".
288
+ - Add Remote Project modal matches image5: host dropdown, path input + reset, directory listing, footer hint, cancel / add buttons.
289
+ - Sidebar matches image6: 4 fixed action rows on top, project list with each project's sessions inline-expanded under it; collapse persists.
290
+ - Sidebar bottom shows only `⚙ 设置` (mobile icon kept). No theme toggle row, no "对话" history group.
291
+ - Settings is an in-renderer view: `⚙ 设置` switches to settings, back button restores workspace. Relay Devices section reproduces existing pair / refresh / device-list functionality.
292
+ - Markdown rendering visible in transcript: GFM tables, code blocks with Shiki highlight (lazy), sanitized HTML (no script execution).
293
+ - Tool calls render via `ToolBlock`: with `DefaultJsonBlock`, both `tool_call.args` and `tool_result.result` shown as fenced JSON. `ReadBlock` etc. registered as `DefaultJsonBlock` (visual identity), confirming the registry path is wired.
294
+ - IPC channels `attachSession` / `detachSession` / `sessionEvent` exposed via `window.scorel`, exercised by `Transcript` mount/unmount.
295
+ - `sendMessage` returns `{ accepted: true }` shape; renderer no longer awaits a `PersistentEvent[]` from it.
296
+ - All existing existing local + Relay project flows continue to function: add local project (via picker), add Relay device (via Settings), add remote project (via picker → modal), open session, send prompt, receive response.
297
+ - Lucide icons used everywhere they appear in screenshots; no Unicode-character icons remain in the renderer source.
298
+
299
+ ---
300
+
301
+ ## Test Requirements
302
+
303
+ ```bash
304
+ pnpm --filter @scorel/app-gui build
305
+ pnpm --filter @scorel/app-gui typecheck
306
+ pnpm --filter @scorel/app-gui test
307
+ pnpm typecheck
308
+ pnpm test
309
+ pnpm pack:smoke
310
+ git diff --check
311
+ ```
312
+
313
+ Manual e2e:
314
+
315
+ - `pnpm --filter @scorel/app-gui dev` launches Electron.
316
+ - Visual diff against image2-6 for sidebar, empty state, picker overlay, add-remote modal, settings.
317
+ - Real provider single-prompt smoke: open existing local project, create new session, send a prompt that triggers at least one tool call, see `DefaultJsonBlock` render the tool_call/tool_result pair, see assistant text rendered as Markdown.
318
+ - Settings flow: pair a Relay device, refresh, add remote project via picker → modal, send prompt against remote, confirm response.
319
+
320
+ Existing automated suites under `apps/gui/src/main/*.test.ts` and any `verify:m9-gui` pipeline continue to pass.
321
+
322
+ ---
323
+
324
+ ## Affected Paths
325
+
326
+ - `apps/gui/src/renderer.tsx` — deleted.
327
+ - `apps/gui/src/renderer/**` — new tree.
328
+ - `apps/gui/src/index.html` — strip inline `<style>`, add stylesheet link, point script at `renderer.js` (unchanged build artifact name).
329
+ - `apps/gui/src/main.ts` — add `BrowserWindow` reference for `webContents.send`, attach/detach handlers, narrowed `sendMessage` ack.
330
+ - `apps/gui/src/preload.ts` — expose new channels.
331
+ - `apps/gui/src/shared/ipc.ts` — channels, types, narrowed `GuiApi.sendMessage` return type.
332
+ - `apps/gui/src/main/local-host.ts` — `subscribe(sessionId, handler)` helper; `sendLocalMessage` no longer awaits all events.
333
+ - `apps/gui/src/main/relay-service.ts` — same shape.
334
+ - `apps/gui/scripts/build.mjs` — entry rename + CSS loader.
335
+ - `apps/gui/package.json` — new deps.
336
+ - `docs/ROADMAP.md` — append M9 Follow-up section listing S0069 / S0070.
337
+ - `docs/spec/ship/S0069-gui-codex-ui-refactor.md` — this file.
338
+
339
+ ---
340
+
341
+ ## Implementation Notes
342
+
343
+ - `projector.ts` and `delta-batch.ts` are copied from `apps/webui/lib/events/` verbatim, then the Tailwind / Next-specific assumptions are stripped. Follow-up product direction (rebasing WebUI onto GUI components) is out of scope here.
344
+ - `Markdown.tsx` sanitize schema mirrors `apps/webui/components/chatbox/markdown-view.tsx` to reuse the audit done there. Any deviation is recorded in this spec.
345
+ - Tool block registry registration happens in `apps/gui/src/renderer/chatbox/tool-blocks/registry.ts` module init: `registerToolBlock("Read", DefaultJsonBlock)` etc. for now. S0070 swaps the right-hand side to specialized components without touching `Transcript.tsx`.
346
+ - Sidebar `+` button placement: header row of the PROJECTS section keeps a small `+` (lucide `Plus`) for fast add (user decision: dual-entry). Same handler as picker overlay's `添加本地项目`.
347
+ - `EmptyState.tsx` resolves the H1 project name by reading the active picker selection. Falls back to the brand-neutral question when the active selection has no `displayName`.
348
+ - `Topbar.tsx` renders only the right-side stack icon (matches image3). When a session is open it can also display the session title left-aligned (image2). No 64px row, no dividing border.
349
+ - `useCollapsed("project:{projectKey}")` is the single hook for ProjectTree expansion. Default state: collapsed = false (expanded) for the active project on first load, collapsed = true for others. Persistence is best-effort, missing key = expanded.
350
+
351
+ ---
352
+
353
+ ## Risks
354
+
355
+ - IPC reshape: existing `sendMessage` contract change (Promise<PersistentEvent[]> → Promise<{ accepted: true }>) is a breaking renderer-side change. Acceptable because GUI and renderer ship together and are not pre-1.0.
356
+ - Adding lucide-react / react-markdown / shiki to GUI bundle increases renderer size; verified against build smoke.
357
+ - Shiki lazy load in Electron renderer must not assume CDN — same `createHighlighterCore` + bundle-shipped grammar approach as webui.
358
+ - `tool-blocks/registry` must not introduce a circular import. Keep registration module a leaf; `Transcript.tsx` imports `lookupToolBlock`, registry imports nothing renderer-specific beyond `Default*Block`.
359
+ - `webContents.send` from main with high-frequency events (`text_delta`) can saturate IPC. S0069 only forwards `PersistentEvent`; transient flooding is S0070's concern (RAF batcher solves it).
360
+
361
+ ---
362
+
363
+ ## Out Of Scope (Reaffirmed)
364
+
365
+ Codex screenshot elements explicitly excluded:
366
+
367
+ - empty-state plugin recommendation cards.
368
+ - bottom-sidebar global "对话" history group.
369
+ - composer review banner (`2 个文件已更改 +95 -1` / `审查`).
370
+ - "不使用项目" picker option.
371
+ - functional model picker / mic / "完全访问" toggle.