@chanlerdev/scorel 0.0.1
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.
- package/README.md +110 -0
- package/dist/index.js +6675 -0
- package/dist/index.js.map +7 -0
- package/docs/CHANGELOG.md +12 -0
- package/docs/README.md +116 -0
- package/docs/ROADMAP.md +669 -0
- package/docs/SHIP.md +242 -0
- package/docs/spec/channels.md +156 -0
- package/docs/spec/client.md +326 -0
- package/docs/spec/daemon.md +408 -0
- package/docs/spec/events.md +423 -0
- package/docs/spec/extensions.md +255 -0
- package/docs/spec/relay.md +391 -0
- package/docs/spec/runtime.md +251 -0
- package/docs/spec/session.md +380 -0
- package/docs/spec/ship/S0001-docs-baseline.md +41 -0
- package/docs/spec/ship/S0002-package-skeleton.md +56 -0
- package/docs/spec/ship/S0003-protocol-contracts.md +49 -0
- package/docs/spec/ship/S0004-session-core.md +50 -0
- package/docs/spec/ship/S0005-runtime-loop.md +48 -0
- package/docs/spec/ship/S0006-embedded-daemon-client.md +51 -0
- package/docs/spec/ship/S0007-cli-alpha.md +49 -0
- package/docs/spec/ship/S0008-coding-tools.md +107 -0
- package/docs/spec/ship/S0009-code-discovery-tools.md +82 -0
- package/docs/spec/ship/S0010-todo-tool-and-cli.md +81 -0
- package/docs/spec/ship/S0011-coding-agent-alpha-smoke.md +110 -0
- package/docs/spec/ship/S0012-coding-tools-maturity.md +143 -0
- package/docs/spec/ship/S0013-local-daemon-protocol.md +57 -0
- package/docs/spec/ship/S0014-local-daemon-lifecycle.md +64 -0
- package/docs/spec/ship/S0015-local-attach-and-broadcast.md +58 -0
- package/docs/spec/ship/S0016-local-daemon-resync-smoke.md +60 -0
- package/docs/spec/ship/S0017-grep-files-output-mode.md +49 -0
- package/docs/spec/ship/S0018-daemon-entrypoint-smoke.md +48 -0
- package/docs/spec/ship/S0019-remote-transport-contract.md +59 -0
- package/docs/spec/ship/S0020-remote-websocket-server.md +56 -0
- package/docs/spec/ship/S0021-remote-websocket-client-transport.md +55 -0
- package/docs/spec/ship/S0022-remote-daemon-cli-lifecycle.md +60 -0
- package/docs/spec/ship/S0023-remote-control-e2e-validation.md +66 -0
- package/docs/spec/ship/S0024-remote-attach-interactive-stream.md +49 -0
- package/docs/spec/ship/S0025-remote-attach-session-event-view.md +57 -0
- package/docs/spec/ship/S0026-attach-project-cache-and-dual-seq-reconnect.md +87 -0
- package/docs/spec/ship/S0027-session-diagnostics-log.md +77 -0
- package/docs/spec/ship/S0028-client-attach-diagnostics-log.md +70 -0
- package/docs/spec/ship/S0029-project-index-for-session-lookup.md +119 -0
- package/docs/spec/ship/S0030-webui-product-intent.md +73 -0
- package/docs/spec/ship/S0031-daemon-projectslug-rule.md +72 -0
- package/docs/spec/ship/S0032-daemon-protocol-completion.md +123 -0
- package/docs/spec/ship/S0033-webui-skeleton-routing.md +92 -0
- package/docs/spec/ship/S0034-webui-device-settings.md +121 -0
- package/docs/spec/ship/S0035-webui-device-handshake.md +83 -0
- package/docs/spec/ship/S0036-webui-project-session-sync.md +70 -0
- package/docs/spec/ship/S0037-webui-chatbox-v1.md +97 -0
- package/docs/spec/ship/S0038-webui-cancel-multiclient.md +65 -0
- package/docs/spec/ship/S0039-webui-e2e-newchat.md +74 -0
- package/docs/spec/ship/S0040-webui-codex-visual-tokens.md +227 -0
- package/docs/spec/ship/S0041-webui-markdown-and-tool-block.md +248 -0
- package/docs/spec/ship/S0042-webui-streaming-ux-autoscroll.md +130 -0
- package/docs/spec/ship/S0043-startup-ergonomics.md +278 -0
- package/docs/spec/ship/S0044-webui-chatbox-rebuild.md +556 -0
- package/docs/spec/ship/S0045-webui-card-sidebar-and-session-fixes.md +469 -0
- package/docs/spec/ship/S0046-webui-empty-composer-and-lazy-session.md +428 -0
- package/docs/spec/ship/S0047-webui-project-hover-newchat-and-dynamic-greeting.md +176 -0
- package/docs/spec/ship/S0048-device-level-host-project-registry.md +253 -0
- package/docs/spec/ship/S0049-webui-add-project-directory-browser.md +217 -0
- package/docs/spec/ship/S0050-instruction-snapshot-and-agents-assembly.md +338 -0
- package/docs/spec/ship/S0051-harness-item-and-system-reminder.md +190 -0
- package/docs/spec/ship/S0052-follow-up-queue-and-dual-loop.md +195 -0
- package/docs/spec/ship/S0053-skill-index-and-skill-tool.md +252 -0
- package/docs/spec/ship/S0054-webui-running-message-behavior.md +72 -0
- package/docs/spec/ship/S0055-webui-composer-acceptance-and-queue-strip.md +68 -0
- package/docs/spec/ship/S0056-relay-and-hosted-webui-contract.md +106 -0
- package/docs/spec/ship/S0057-relay-service-protocol-skeleton.md +161 -0
- package/docs/spec/ship/S0058-host-outbound-relay-and-pair-command.md +138 -0
- package/docs/spec/ship/S0059-relay-transport-and-hosted-webui-connector.md +140 -0
- package/docs/spec/ship/S0060-relay-hosted-webui-e2e-validation.md +132 -0
- package/docs/spec/ship/S0060-relay-hosted-webui-e2e-validation.verification.md +90 -0
- package/docs/spec/ship/S0061-hosted-defaults-and-cli-command-surface.md +208 -0
- package/docs/spec/ship/S0062-npm-package-and-release-workflow.md +166 -0
- package/docs/spec/tools.md +173 -0
- package/package.json +51 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# S0044: WebUI Chatbox-Style Rebuild (ChatGPT Philosophy)
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Rebuild the entire WebUI surface to a Chatbox-style three-segment sidebar with ChatGPT-philosophy visuals: pure white background, near-black text, single-axis accent, sans-only typography, no shadows, no decorations. Replaces M5.5's warm-paper + ink-blue + Newsreader serif baseline (S0040–S0042) wholesale. Adds project-node collapse, pill composer, and Codex-style disabled placeholder buttons for unimplemented features.
|
|
6
|
+
|
|
7
|
+
Locked decisions live in `self/discussions/2026-05-31-webui-chatbox-rebuild-brainstorm.md` §5 and `docs/design.md`. This spec is the implementation contract.
|
|
8
|
+
|
|
9
|
+
Mood board (user-supplied screenshot):
|
|
10
|
+
|
|
11
|
+
- Sidebar three segments: top fixed actions (`+ New Chat` active, Search/Plugins/Automation grayed) → middle device/project tree with per-project ▸/▾ collapse → bottom Settings + grayed theme toggle.
|
|
12
|
+
- Main area: **no topbar**. Empty state shows large H1 greeting; populated state shows transcript only.
|
|
13
|
+
- User turn: max-width 70%, right-aligned, soft-gray bubble (`#F4F4F4`), 16px radius. Assistant turn: no bubble, flush against `bg`, left-aligned.
|
|
14
|
+
- Composer: pill with 24px radius, 1px `#E7EAEC` border, `⊕` left, `model ▾` + `🎤` + circular `●` send right. All non-send buttons grayed (Codex semantic).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
### 1. Tokens — `apps/webui/app/globals.css`
|
|
21
|
+
|
|
22
|
+
Replace every `:root` value (S0040 Codex palette) with the ChatGPT palette. **Variable names stay**, only values change, so all `bg-surface` / `text-muted` / etc. semantic classes flow through automatically. Final `:root`:
|
|
23
|
+
|
|
24
|
+
```css
|
|
25
|
+
:root {
|
|
26
|
+
/* color */
|
|
27
|
+
--color-bg: #FFFFFF;
|
|
28
|
+
--color-surface: #F7F7F8; /* sidebar / popover */
|
|
29
|
+
--color-surface-raised: #FFFFFF; /* composer / modal */
|
|
30
|
+
--color-surface-hover: #EFEFF1; /* hover reaction (NEW token) */
|
|
31
|
+
--color-border: #E7EAEC;
|
|
32
|
+
--color-border-strong: #9CA3AF;
|
|
33
|
+
--color-text: #0D0D0D;
|
|
34
|
+
--color-text-muted: #5D5D5D;
|
|
35
|
+
--color-text-faint: #9CA3AF; /* placeholder / disabled */
|
|
36
|
+
--color-accent: #0D0D0D; /* send / active emphasis = black */
|
|
37
|
+
--color-accent-hover: #1F1F1F;
|
|
38
|
+
--color-accent-soft: #F4F4F4; /* user bubble */
|
|
39
|
+
--color-status-ok: #16A34A;
|
|
40
|
+
--color-status-warn: #D97706;
|
|
41
|
+
--color-status-err: #DC2626;
|
|
42
|
+
--color-status-idle: #9CA3AF;
|
|
43
|
+
|
|
44
|
+
/* typography — sans-only */
|
|
45
|
+
--font-body: -apple-system, BlinkMacSystemFont, "PingFang SC",
|
|
46
|
+
"Segoe UI", "Helvetica Neue", system-ui, sans-serif;
|
|
47
|
+
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
48
|
+
/* --font-display REMOVED — no serif anywhere */
|
|
49
|
+
|
|
50
|
+
/* sizes — locked five-step ladder */
|
|
51
|
+
--text-xs: 12px; --leading-xs: 17px;
|
|
52
|
+
--text-sm: 13px; --leading-sm: 20px;
|
|
53
|
+
--text-base: 14px; --leading-base: 21px;
|
|
54
|
+
--text-md: 16px; --leading-md: 26px; /* conversation body */
|
|
55
|
+
--text-lg: 20px; --leading-lg: 26px; /* markdown h2/h3 */
|
|
56
|
+
--text-xl: 30px; --leading-xl: 36px; /* empty-state H1 */
|
|
57
|
+
|
|
58
|
+
/* spacing — unchanged */
|
|
59
|
+
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
|
|
60
|
+
--space-5: 20px; --space-6: 24px; --space-8: 32px;
|
|
61
|
+
|
|
62
|
+
/* radius */
|
|
63
|
+
--radius-sm: 6px;
|
|
64
|
+
--radius-md: 12px;
|
|
65
|
+
--radius-lg: 16px; /* user bubble */
|
|
66
|
+
--radius-pill: 24px; /* composer */
|
|
67
|
+
--radius-full: 9999px; /* circular send / icon button */
|
|
68
|
+
|
|
69
|
+
/* shadow — disabled globally */
|
|
70
|
+
--shadow-sm: none;
|
|
71
|
+
--shadow-md: none;
|
|
72
|
+
--shadow-focus: none;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Remove `--font-display`. Remove `--shadow-*` *values* (set to `none`) but **keep the variable names** so existing `shadow-sm` / `shadow-md` / `shadow-focus` Tailwind utilities don't break the build — they simply render `box-shadow: none`. Components must migrate off `shadow-focus` (see §6).
|
|
77
|
+
|
|
78
|
+
Remove these CSS rules from `globals.css`:
|
|
79
|
+
|
|
80
|
+
- `.prose-tweak h1..h4 { font-family: var(--font-display); }` → drop the rule entirely (no serif). Headings inherit `--font-body`.
|
|
81
|
+
- The bare `*:focus-visible { box-shadow: var(--shadow-focus); }` block — replace with the new outline-based focus ring (see §6).
|
|
82
|
+
|
|
83
|
+
Add these new CSS rules:
|
|
84
|
+
|
|
85
|
+
```css
|
|
86
|
+
/* Disabled placeholder buttons (Codex semantic): visible but inert. */
|
|
87
|
+
.btn-disabled {
|
|
88
|
+
opacity: 0.4;
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
pointer-events: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Empty-state H1 used on `app/page.tsx` and similar landings. */
|
|
94
|
+
.greeting {
|
|
95
|
+
font-size: var(--text-xl);
|
|
96
|
+
line-height: var(--leading-xl);
|
|
97
|
+
font-weight: 500;
|
|
98
|
+
color: var(--color-text);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Tailwind theme — `apps/webui/tailwind.config.ts`
|
|
103
|
+
|
|
104
|
+
- Remove `fontFamily.display` (the old Newsreader serif slot).
|
|
105
|
+
- Add `colors["surface-hover"]: "var(--color-surface-hover)"`.
|
|
106
|
+
- Add `borderRadius.pill: "var(--radius-pill)"` and `borderRadius.full: "var(--radius-full)"`.
|
|
107
|
+
- Keep all other mappings as-is. `font-display` referenced anywhere in components must be removed (see §5 sweep).
|
|
108
|
+
|
|
109
|
+
### 3. Fonts — drop Newsreader
|
|
110
|
+
|
|
111
|
+
- Remove `@fontsource/newsreader` dependency from `apps/webui/package.json`.
|
|
112
|
+
- Drop the two `import "@fontsource/newsreader/*"` lines from `apps/webui/app/layout.tsx`.
|
|
113
|
+
- Keep `@fontsource/jetbrains-mono` (still used for `--font-mono`).
|
|
114
|
+
- `apps/webui/src/package-boundaries.test.ts`: remove `@fontsource/newsreader` from the allowed externals list. Keep `@fontsource/jetbrains-mono` and the `@fontsource/*` import-prefix rule (if any).
|
|
115
|
+
|
|
116
|
+
### 4. Layout — drop topbar
|
|
117
|
+
|
|
118
|
+
- `apps/webui/app/layout.tsx`: remove the `<Topbar />` element. The shell collapses to `<body><Sidebar /><main>{children}</main></body>` with the same flex layout. Body classes: `h-screen flex bg-bg text-text font-sans`. The outer `flex flex-col` wrapper is no longer needed.
|
|
119
|
+
- Delete `apps/webui/components/shell/topbar.tsx`.
|
|
120
|
+
|
|
121
|
+
### 5. Sidebar — three-segment + project collapse
|
|
122
|
+
|
|
123
|
+
`apps/webui/components/shell/sidebar.tsx` rewrite. Final structure:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<aside className="w-[280px] shrink-0 bg-surface flex flex-col">
|
|
127
|
+
{/* Segment 1: top fixed action rows */}
|
|
128
|
+
<div className="px-3 pt-4 pb-2 space-y-1">
|
|
129
|
+
<NewChatRow active={isHomeRoute} /> {/* + New Chat — active, navigates to / */}
|
|
130
|
+
<DisabledRow icon="🔍" label="搜索" />
|
|
131
|
+
<DisabledRow icon="🧩" label="插件" />
|
|
132
|
+
<DisabledRow icon="🤖" label="自动化" />
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Segment 2: middle device/project tree */}
|
|
136
|
+
<div className="px-3 pt-4 pb-1 text-xs font-medium uppercase tracking-wide text-faint">
|
|
137
|
+
Devices
|
|
138
|
+
</div>
|
|
139
|
+
<div className="flex-1 overflow-auto px-3 pb-3 text-sm">
|
|
140
|
+
{devices.length === 0 ? <EmptyDevicesHint /> : <ul>{devices.map(d => <DeviceTree … />)}</ul>}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Segment 3: bottom fixed actions */}
|
|
144
|
+
<div className="px-3 py-3 space-y-1">
|
|
145
|
+
<SettingsLink /> {/* navigates to /settings */}
|
|
146
|
+
<DisabledRow icon="☀" label="主题" />
|
|
147
|
+
</div>
|
|
148
|
+
</aside>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Row components:
|
|
152
|
+
|
|
153
|
+
- `NewChatRow`: `<Link href="/">+ 新对话</Link>`. Class: `flex items-center gap-2 rounded-sm px-2 py-2 text-sm font-medium text-text hover:bg-surface-hover`. Active = `bg-surface-hover`. Replaces the existing `NewChatButton` "sidebar" variant; the old component can be deleted if no other route uses it.
|
|
154
|
+
- `DisabledRow({ icon, label })`: `<button type="button" disabled className="btn-disabled flex items-center gap-2 rounded-sm px-2 py-2 text-sm text-text">{icon}<span>{label}</span></button>`. Native `disabled` + `.btn-disabled` class composes the Codex semantic.
|
|
155
|
+
- `SettingsLink`: `<Link href="/settings" className="flex items-center gap-2 rounded-sm px-2 py-2 text-sm text-muted hover:bg-surface-hover hover:text-text">⚙ Settings</Link>`.
|
|
156
|
+
|
|
157
|
+
Device row in `DeviceTree`:
|
|
158
|
+
|
|
159
|
+
- Replace the current `border-l-2 px-2 py-1.5 hover:bg-accent-soft` styling.
|
|
160
|
+
- New: `<button type="button" onClick={toggle} className="..."><span>▾/▸</span>{device.name}<DeviceStatus /></button>` followed by `<Link href="/devices/:id" />` semantic — but for simplicity keep it as a single `<Link>` with the `▾/▸` rendered as a sibling `<button>` that toggles collapse without navigating. Concrete pattern:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<li>
|
|
164
|
+
<div className="flex items-center gap-1">
|
|
165
|
+
<CollapseToggle id={`device:${device.id}`} />
|
|
166
|
+
<Link href={`/devices/${encodeURIComponent(device.id)}`} aria-current={…} className="…">
|
|
167
|
+
<span className="truncate">{device.name}</span>
|
|
168
|
+
<DeviceStatus />
|
|
169
|
+
</Link>
|
|
170
|
+
</div>
|
|
171
|
+
{!collapsed && <ProjectList … />}
|
|
172
|
+
</li>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Project row in `ProjectNode` mirrors the same pattern, with `id={`project:${deviceId}/${slug}`}`. Sessions render only when the project is **not collapsed**. The previous "auto-expand if active or sessions.length > 0" rule is removed — collapse state alone drives expansion. Active project starts expanded **on first ever encounter** (default state), but a user collapse persists.
|
|
176
|
+
|
|
177
|
+
Active row visuals (replaces the old `border-accent bg-accent-soft text-accent` block):
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
const activeClass = isActive
|
|
181
|
+
? "bg-surface-hover font-medium text-text"
|
|
182
|
+
: "text-text";
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Drop the 2px left accent border. Sessions: same active style on `SessionNode`.
|
|
186
|
+
|
|
187
|
+
#### 5.1 Collapse persistence
|
|
188
|
+
|
|
189
|
+
New module `apps/webui/lib/store/collapsed.ts`:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const KEY = "scorel.ui.collapsed";
|
|
193
|
+
|
|
194
|
+
export function readCollapsed(): Record<string, boolean> {
|
|
195
|
+
if (typeof window === "undefined") return {};
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(window.localStorage.getItem(KEY) ?? "{}") as Record<string, boolean>;
|
|
198
|
+
} catch {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function writeCollapsed(next: Record<string, boolean>): void {
|
|
204
|
+
if (typeof window === "undefined") return;
|
|
205
|
+
window.localStorage.setItem(KEY, JSON.stringify(next));
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
New hook `apps/webui/lib/store/use-collapsed.ts`:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
"use client";
|
|
213
|
+
import { useSyncExternalStore } from "react";
|
|
214
|
+
import { readCollapsed, writeCollapsed } from "./collapsed";
|
|
215
|
+
|
|
216
|
+
const listeners = new Set<() => void>();
|
|
217
|
+
let snapshot: Record<string, boolean> | null = null;
|
|
218
|
+
|
|
219
|
+
function getSnapshot(): Record<string, boolean> {
|
|
220
|
+
if (snapshot === null) snapshot = readCollapsed();
|
|
221
|
+
return snapshot;
|
|
222
|
+
}
|
|
223
|
+
function emit(): void { for (const l of listeners) l(); }
|
|
224
|
+
|
|
225
|
+
export function useCollapsed(id: string): [boolean, () => void] {
|
|
226
|
+
const map = useSyncExternalStore(
|
|
227
|
+
(l) => { listeners.add(l); return () => listeners.delete(l); },
|
|
228
|
+
() => getSnapshot(),
|
|
229
|
+
() => ({}),
|
|
230
|
+
);
|
|
231
|
+
const collapsed = Boolean(map[id]);
|
|
232
|
+
const toggle = (): void => {
|
|
233
|
+
const next = { ...getSnapshot(), [id]: !collapsed };
|
|
234
|
+
snapshot = next;
|
|
235
|
+
writeCollapsed(next);
|
|
236
|
+
emit();
|
|
237
|
+
};
|
|
238
|
+
return [collapsed, toggle];
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`CollapseToggle` component (`apps/webui/components/shell/collapse-toggle.tsx`):
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
"use client";
|
|
246
|
+
import { useCollapsed } from "../../lib/store/use-collapsed";
|
|
247
|
+
|
|
248
|
+
export function CollapseToggle({ id }: { id: string }): JSX.Element {
|
|
249
|
+
const [collapsed, toggle] = useCollapsed(id);
|
|
250
|
+
return (
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
onClick={toggle}
|
|
254
|
+
aria-expanded={!collapsed}
|
|
255
|
+
aria-label={collapsed ? "Expand" : "Collapse"}
|
|
256
|
+
className="text-faint hover:text-text shrink-0 w-4 text-center text-xs select-none"
|
|
257
|
+
>
|
|
258
|
+
{collapsed ? "▸" : "▾"}
|
|
259
|
+
</button>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### 6. Composer — pill + grayed buttons
|
|
265
|
+
|
|
266
|
+
`apps/webui/components/chatbox/composer.tsx` rewrite:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
<form className="px-4 pb-6 pt-3 bg-bg" onSubmit={…}>
|
|
270
|
+
<div className="mx-auto max-w-3xl rounded-pill border border-subtle bg-bg
|
|
271
|
+
focus-within:border-text-strong focus-within:border-text">
|
|
272
|
+
<textarea
|
|
273
|
+
className="block w-full resize-none bg-transparent px-5 pt-3 pb-1 text-md
|
|
274
|
+
text-text placeholder:text-faint outline-none"
|
|
275
|
+
placeholder="Message Scorel…"
|
|
276
|
+
value={…} onChange={…} onKeyDown={onKeyDown}
|
|
277
|
+
rows={1} ref={autoResizeRef}
|
|
278
|
+
/>
|
|
279
|
+
<div className="flex items-center justify-between px-3 pb-2">
|
|
280
|
+
<button type="button" disabled
|
|
281
|
+
className="btn-disabled rounded-full w-7 h-7 flex items-center justify-center text-text">
|
|
282
|
+
<span aria-hidden>⊕</span>
|
|
283
|
+
<span className="sr-only">Attach</span>
|
|
284
|
+
</button>
|
|
285
|
+
<div className="flex items-center gap-2">
|
|
286
|
+
<button type="button" disabled
|
|
287
|
+
className="btn-disabled rounded-md px-2 py-1 text-sm text-muted">
|
|
288
|
+
{modelLabel} <span aria-hidden>▾</span>
|
|
289
|
+
</button>
|
|
290
|
+
<button type="button" disabled
|
|
291
|
+
className="btn-disabled rounded-full w-7 h-7 flex items-center justify-center text-text">
|
|
292
|
+
<span aria-hidden>🎤</span>
|
|
293
|
+
<span className="sr-only">Voice</span>
|
|
294
|
+
</button>
|
|
295
|
+
{inFlight ? (
|
|
296
|
+
<button type="button" data-testid="composer-cancel"
|
|
297
|
+
className="rounded-full w-7 h-7 flex items-center justify-center
|
|
298
|
+
bg-status-err text-bg disabled:opacity-60"
|
|
299
|
+
onClick={fireCancel} disabled={cancelling}>
|
|
300
|
+
<span aria-hidden>■</span>
|
|
301
|
+
<span className="sr-only">{cancelling ? "Cancelling" : "Cancel"}</span>
|
|
302
|
+
</button>
|
|
303
|
+
) : (
|
|
304
|
+
<button type="submit" data-testid="composer-send"
|
|
305
|
+
className="rounded-full w-7 h-7 flex items-center justify-center
|
|
306
|
+
bg-accent text-bg hover:bg-accent-hover
|
|
307
|
+
disabled:opacity-40 disabled:cursor-not-allowed"
|
|
308
|
+
disabled={sendDisabled}>
|
|
309
|
+
<span aria-hidden>↑</span>
|
|
310
|
+
<span className="sr-only">Send</span>
|
|
311
|
+
</button>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
{errorBanner && <p data-testid="composer-error" role="alert"
|
|
317
|
+
className="mx-auto max-w-3xl mt-2 text-xs text-status-err">{errorBanner}</p>}
|
|
318
|
+
</form>
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Behavior unchanged:
|
|
322
|
+
|
|
323
|
+
- Enter submits, Shift+Enter newline, Esc cancels (existing logic in `onKeyDown` preserved).
|
|
324
|
+
- `inFlight`/`cancelling`/`onCancel`/`disabled`/`errorBanner` props unchanged.
|
|
325
|
+
- New: textarea auto-resizes up to ~5 lines (max-height ~120px) via a small ref-driven height adjust on input. Keep the existing min-height of one line.
|
|
326
|
+
|
|
327
|
+
`modelLabel` is a new prop with default `"Default"` — Composer caller (chatbox page) passes the active model name when known, else default. Type:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
export type ComposerProps = {
|
|
331
|
+
…,
|
|
332
|
+
modelLabel?: string;
|
|
333
|
+
};
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Caller updates: `apps/webui/components/chatbox/chatbox-page.tsx` (or wherever composer is mounted) — pass through whatever model identifier is available; if none, omit the prop. **Do not introduce new daemon calls** to fetch the model name in this spec.
|
|
337
|
+
|
|
338
|
+
### 7. Turns — bubble formation
|
|
339
|
+
|
|
340
|
+
`apps/webui/components/chatbox/turn-user.tsx`:
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
<div className="flex justify-end px-4">
|
|
344
|
+
<article data-testid="turn-user" data-pending={…}
|
|
345
|
+
className="max-w-[70%] rounded-lg bg-accent-soft px-4 py-3 text-md text-text">
|
|
346
|
+
<MarkdownView text={text} />
|
|
347
|
+
{turn.pending && (
|
|
348
|
+
<span className="mt-1 block text-xs text-faint">sending…</span>
|
|
349
|
+
)}
|
|
350
|
+
</article>
|
|
351
|
+
</div>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Drop the `<header>You · sending…</header>` block — pending state moves to a small footer line. No border, no `font-display`.
|
|
355
|
+
|
|
356
|
+
`apps/webui/components/chatbox/turn-assistant.tsx`:
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
<article data-testid="turn-assistant" data-streaming={…}
|
|
360
|
+
className="px-4 py-2 text-md text-text">
|
|
361
|
+
{turn.stopReason && turn.stopReason !== "end_turn" && (
|
|
362
|
+
<p className="mb-1 text-xs text-status-warn">{turn.stopReason}</p>
|
|
363
|
+
)}
|
|
364
|
+
<div className="space-y-2">
|
|
365
|
+
{turn.parts.map((part, idx) => <PartView key={idx} part={part} />)}
|
|
366
|
+
{turn.streaming && <StreamingCursor />}
|
|
367
|
+
</div>
|
|
368
|
+
</article>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Drop the `<header>Assistant</header>` row entirely. No bubble, no border, no surface bg.
|
|
372
|
+
|
|
373
|
+
`apps/webui/components/chatbox/turn-tool.tsx` and the thinking `<details>` block in `turn-assistant.tsx`:
|
|
374
|
+
|
|
375
|
+
- Remove `font-display` from the `<summary>`.
|
|
376
|
+
- Replace `bg-surface` with `bg-surface` (sidebar gray = ok for visual layering against pure white bg). No change in semantics, just verify the contrast still reads.
|
|
377
|
+
|
|
378
|
+
`apps/webui/components/chatbox/transcript.tsx`:
|
|
379
|
+
|
|
380
|
+
- Drop `border-t border-subtle/50` between turns — rely on 24px gap (`space-y-6`).
|
|
381
|
+
- Wrap content in `<div className="mx-auto max-w-3xl py-6 space-y-6">` for centered reading column matching ChatGPT.
|
|
382
|
+
|
|
383
|
+
### 8. Empty state — `app/page.tsx`
|
|
384
|
+
|
|
385
|
+
When `devices.length === 0`:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
<div className="flex h-full flex-col items-center justify-center gap-4 p-8">
|
|
389
|
+
<h1 className="greeting">欢迎使用 Scorel</h1>
|
|
390
|
+
<p className="text-md text-muted">先添加一个设备开始</p>
|
|
391
|
+
<Link href="/settings"
|
|
392
|
+
className="rounded-pill bg-accent px-5 py-2 text-bg hover:bg-accent-hover">
|
|
393
|
+
打开 Settings
|
|
394
|
+
</Link>
|
|
395
|
+
</div>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
When devices exist but no active session: similar centered greeting "今天聊点什么?" with no CTA (composer below carries the action).
|
|
399
|
+
|
|
400
|
+
### 9. Focus ring
|
|
401
|
+
|
|
402
|
+
Replace the global `*:focus-visible { box-shadow: var(--shadow-focus); }` in `globals.css` with:
|
|
403
|
+
|
|
404
|
+
```css
|
|
405
|
+
*:focus { outline: none; }
|
|
406
|
+
*:focus-visible {
|
|
407
|
+
outline: 2px solid var(--color-text);
|
|
408
|
+
outline-offset: 2px;
|
|
409
|
+
border-radius: var(--radius-sm);
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Composer wrapper uses `focus-within:border-text` to flip the pill border to black instead of `outline`. Sweep all `focus-visible:shadow-focus` utility usages across `apps/webui/components/**` and either delete (covered by the global rule) or replace with the same `focus-visible:outline-*` pattern. Remove `--shadow-focus` non-zero value (already `none` in §1).
|
|
414
|
+
|
|
415
|
+
### 10. Markdown prose tweak — drop serif
|
|
416
|
+
|
|
417
|
+
`globals.css` `.prose-tweak`:
|
|
418
|
+
|
|
419
|
+
- Remove the `h1..h4 { font-family: var(--font-display); }` block.
|
|
420
|
+
- Verify `--tw-prose-headings` is `var(--color-text)` (already correct).
|
|
421
|
+
- Update H1/H2/H3 sizes to match the locked ladder: `prose-tweak h1 { font-size: var(--text-xl); }`, `prose-tweak h2, prose-tweak h3 { font-size: var(--text-lg); }`. Use direct CSS rules in `globals.css` since `@tailwindcss/typography` defaults won't match the ChatGPT scale.
|
|
422
|
+
- `prose-tweak p { font-size: var(--text-md); line-height: var(--leading-md); }` — confirm the body math text aligns.
|
|
423
|
+
- Inline code: keep current pill styling but switch background to `var(--color-surface)` so it stands out against pure white `bg`.
|
|
424
|
+
|
|
425
|
+
### 11. Boundary tests — `apps/webui/src/package-boundaries.test.ts`
|
|
426
|
+
|
|
427
|
+
- Remove `@fontsource/newsreader` from allowed externals.
|
|
428
|
+
- Keep the literal-palette ban (zinc/emerald/red/amber/sky/stone/slate/gray). Add `neutral` to the banned list (since ChatGPT-ish UIs sometimes regress to `neutral-*`).
|
|
429
|
+
- Add a new rule: scan `apps/webui/{app,components}/**/*.{ts,tsx}` and fail if `font-display` className appears (the serif token is gone). Allow it only inside `*.test.tsx` (excluded from the scan).
|
|
430
|
+
- Add a rule: fail if any source file imports `@fontsource/newsreader` (cross-check the package removal).
|
|
431
|
+
|
|
432
|
+
### 12. Component tests
|
|
433
|
+
|
|
434
|
+
- `device-status.test.tsx`: status dot tests stay — colors unchanged.
|
|
435
|
+
- `topbar.test.tsx` (if it exists): delete the file; topbar is removed.
|
|
436
|
+
- Add `collapse-toggle.test.tsx`: render `<CollapseToggle id="x" />`, click, expect `▾` ↔ `▸` swap and `localStorage["scorel.ui.collapsed"]` write.
|
|
437
|
+
- Add `sidebar.test.tsx` smoke (or extend existing): render with 1 device + 1 project + 2 sessions; click project toggle; assert sessions hide/show; reload sim (re-render with same `localStorage`); assert collapsed state persists.
|
|
438
|
+
- Add `composer.test.tsx` extension or new: assert `data-testid="composer-send"` exists, the three placeholder buttons (`⊕`, `model ▾`, `🎤`) each carry `disabled` attribute and `.btn-disabled` class. Assert `<textarea placeholder="Message Scorel…">`.
|
|
439
|
+
- Update `turn-user.test.tsx` / `turn-assistant.test.tsx` if they assert `font-display` or `bg-accent-soft border` — adjust to the new bubble class set.
|
|
440
|
+
- All other existing tests should keep passing; if any fails on a removed `font-display` / topbar import, fix the assertion.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Not In Scope
|
|
445
|
+
|
|
446
|
+
- Dark mode implementation (future spec). Tokens are sized to allow a `prefers-color-scheme: dark` block without component churn.
|
|
447
|
+
- Cmd+K / global keyboard shortcuts / command palette.
|
|
448
|
+
- Sidebar collapse to 56px icon-only (Cmd+B).
|
|
449
|
+
- Composer `+` / `🎤` / model picker real functionality (placeholders only).
|
|
450
|
+
- Theme toggle real functionality (placeholder only).
|
|
451
|
+
- Tool block specialization (Bash/Edit/diff viewer); keep the unified JSON-fence renderer from S0041.
|
|
452
|
+
- Composer prompt history (↑ key recall).
|
|
453
|
+
- Streamdown migration; `react-markdown` stays.
|
|
454
|
+
- Daemon protocol changes — model name passthrough is best-effort UI display only.
|
|
455
|
+
- ROADMAP.md milestone restructure beyond appending `M5.7: WebUI Chatbox Rebuild` and flipping S0044 status.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Acceptance Criteria
|
|
460
|
+
|
|
461
|
+
1. `apps/webui/app/globals.css` `:root` matches §1 verbatim. No `--font-display` definition. `--shadow-*` set to `none`.
|
|
462
|
+
2. `apps/webui/tailwind.config.ts` no longer references `font-display`. New `surface-hover` color, `pill` and `full` border radii.
|
|
463
|
+
3. `apps/webui/package.json` no longer contains `@fontsource/newsreader`. `pnpm-lock.yaml` regenerated.
|
|
464
|
+
4. `apps/webui/app/layout.tsx` does not import any Newsreader file and does not render `<Topbar />`.
|
|
465
|
+
5. `apps/webui/components/shell/topbar.tsx` deleted. No remaining import of it anywhere under `apps/webui/`.
|
|
466
|
+
6. Sidebar visibly composed of three segments per §5: top 4 rows (1 active New Chat + 3 grayed), middle device tree with project ▸/▾ collapse, bottom Settings + grayed theme toggle. Tested by `sidebar.test.tsx`.
|
|
467
|
+
7. Project / Device collapse state persists to `localStorage["scorel.ui.collapsed"]` and survives a fresh component mount. Tested by `collapse-toggle.test.tsx` and `sidebar.test.tsx`.
|
|
468
|
+
8. Composer matches §6: 24px pill, `Message Scorel…` placeholder, three `.btn-disabled` placeholders (`⊕`, `model ▾`, `🎤`), circular black send (white `↑`) / red cancel (white `■`). `inFlight` / `cancelling` / `errorBanner` behavior unchanged. Auto-resize textarea up to 5 lines.
|
|
469
|
+
9. User turn renders a right-aligned `max-w-[70%] rounded-lg bg-accent-soft` bubble. Assistant turn renders flush against `bg`, no bubble, no border, no header row. No `font-display` className anywhere in `apps/webui/components/chatbox/`.
|
|
470
|
+
10. Empty state on `/` shows the `.greeting` H1 plus CTA when no devices; populated state shows centered greeting without CTA.
|
|
471
|
+
11. Focus rings everywhere use the new outline rule (`outline: 2px solid var(--color-text)`). No remaining `focus-visible:shadow-focus` className in source.
|
|
472
|
+
12. `apps/webui/src/package-boundaries.test.ts`:
|
|
473
|
+
- `@fontsource/newsreader` removed from allowed externals.
|
|
474
|
+
- `font-display` className ban active and verified.
|
|
475
|
+
- `@fontsource/newsreader` import ban active.
|
|
476
|
+
- Existing literal-palette ban still passes.
|
|
477
|
+
13. `pnpm --filter @scorel/app-webui typecheck && pnpm --filter @scorel/app-webui test` passes.
|
|
478
|
+
14. `pnpm --filter @scorel/app-webui build` succeeds; bundle reduction (Newsreader removal) noted in PR description.
|
|
479
|
+
15. Repo-level `pnpm typecheck && pnpm test` passes.
|
|
480
|
+
16. Manual visual verification (browser, after `pnpm dev`):
|
|
481
|
+
- `/` empty state shows greeting + CTA.
|
|
482
|
+
- `/settings` form renders with the new tokens (no warm paper, no serif).
|
|
483
|
+
- `/devices/:id/projects/:slug/sessions/:id` chatbox shows: no topbar, transcript centered (max-w-3xl), user bubbles right-aligned soft-gray, assistant flush left, composer pill at bottom with 4 disabled placeholders + black send.
|
|
484
|
+
- Project node ▸/▾ click toggles session list and persists across page reload.
|
|
485
|
+
- All grayed buttons unclickable (no hover color change, cursor not-allowed).
|
|
486
|
+
- One screenshot of populated chatbox + one of empty state pasted into PR description.
|
|
487
|
+
17. CLI-pair smoke: `pnpm dev` (or `scorel up`) → CLI `scorel chat` and WebUI on the same session both show user prompt, assistant streaming, autoscroll, jump-to-bottom, tool block — visual unchanged from M5.5 except for the new palette / structure.
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Tests
|
|
492
|
+
|
|
493
|
+
Adds:
|
|
494
|
+
|
|
495
|
+
- `apps/webui/components/shell/collapse-toggle.test.tsx` — toggle + persistence.
|
|
496
|
+
- `apps/webui/components/shell/sidebar.test.tsx` — three-segment structure + collapse persistence + grayed rows have `disabled` attr + active New Chat highlight.
|
|
497
|
+
- `apps/webui/components/chatbox/composer.test.tsx` (or extension) — `Message Scorel…` placeholder, four buttons (3 disabled + 1 send), inFlight swaps to red cancel.
|
|
498
|
+
|
|
499
|
+
Modifies:
|
|
500
|
+
|
|
501
|
+
- `apps/webui/components/shell/device-status.test.tsx` — no change unless it tested topbar; verify still green.
|
|
502
|
+
- `apps/webui/components/chatbox/turn-user.test.tsx` and `turn-assistant.test.tsx` — adjust class assertions to new bubble shape.
|
|
503
|
+
- `apps/webui/src/package-boundaries.test.ts` — three new sub-checks (see §11).
|
|
504
|
+
|
|
505
|
+
Removes:
|
|
506
|
+
|
|
507
|
+
- `apps/webui/components/shell/topbar.test.tsx` (if present).
|
|
508
|
+
|
|
509
|
+
Manual:
|
|
510
|
+
|
|
511
|
+
- Real LLM e2e against local daemon: `scorel up`, add a device in Settings via WebUI, open `/devices/local/projects/scorel/sessions/<new>`, type `你好`, observe streaming reply, click cancel mid-stream, send another prompt, verify composer pill, sidebar tree collapse, autoscroll. CLI on the same session shows synchronized state.
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Affected Paths
|
|
516
|
+
|
|
517
|
+
- `apps/webui/app/globals.css`
|
|
518
|
+
- `apps/webui/app/layout.tsx`
|
|
519
|
+
- `apps/webui/app/page.tsx`
|
|
520
|
+
- `apps/webui/tailwind.config.ts`
|
|
521
|
+
- `apps/webui/package.json` (+ `pnpm-lock.yaml`) — remove `@fontsource/newsreader`
|
|
522
|
+
- `apps/webui/components/shell/sidebar.tsx`
|
|
523
|
+
- `apps/webui/components/shell/topbar.tsx` — **deleted**
|
|
524
|
+
- `apps/webui/components/shell/device-status.tsx`
|
|
525
|
+
- `apps/webui/components/shell/project-node.tsx`
|
|
526
|
+
- `apps/webui/components/shell/session-node.tsx`
|
|
527
|
+
- `apps/webui/components/shell/new-chat-button.tsx`
|
|
528
|
+
- `apps/webui/components/shell/collapse-toggle.tsx` — **new**
|
|
529
|
+
- `apps/webui/lib/store/collapsed.ts` — **new**
|
|
530
|
+
- `apps/webui/lib/store/use-collapsed.ts` — **new**
|
|
531
|
+
- `apps/webui/components/chatbox/composer.tsx`
|
|
532
|
+
- `apps/webui/components/chatbox/transcript.tsx`
|
|
533
|
+
- `apps/webui/components/chatbox/turn-user.tsx`
|
|
534
|
+
- `apps/webui/components/chatbox/turn-assistant.tsx`
|
|
535
|
+
- `apps/webui/components/chatbox/turn-tool.tsx`
|
|
536
|
+
- `apps/webui/components/chatbox/markdown-view.tsx` (verify no `font-display` reference)
|
|
537
|
+
- `apps/webui/components/settings/device-form.tsx` / `device-list.tsx` — sweep `font-display` and ensure pill/circular send pattern reused for primary actions if any
|
|
538
|
+
- `apps/webui/src/package-boundaries.test.ts`
|
|
539
|
+
- New test files per §Tests
|
|
540
|
+
- `docs/design.md` — already created, this spec references it as the source of truth
|
|
541
|
+
- `docs/ROADMAP.md` — append M5.7 entry + S0044 row, mark Done after ship
|
|
542
|
+
- `apps/webui/README.md` — note the chatbox-style rebuild, drop the Codex/serif description
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Risks And Boundaries
|
|
547
|
+
|
|
548
|
+
- **Sunk cost from S0040–S0042**: M5.5 polish work is now overwritten. The token framework (CSS vars + Tailwind theme.extend) survives; only values flip. Markdown / Shiki / autoscroll logic is untouched. Cost is low.
|
|
549
|
+
- **Two flips of `:root` in one week**: state to the team in PR description and ROADMAP. Future visual changes go through `docs/design.md` first.
|
|
550
|
+
- **Pure white + 1px borders**: WCAG AA contrast for `--color-text-faint #9CA3AF` on `--color-bg #FFFFFF` is borderline. Reserved strictly for placeholder / disabled where AA does not apply; do **not** use it for primary text. Reviewer must walk every screen.
|
|
551
|
+
- **Auto-resize textarea**: can fight with form submit on Enter if the height calc runs after submit. Test: type a multi-line prompt, press Enter; expect submit + clear + height back to one line.
|
|
552
|
+
- **Collapse persistence**: storage key shared across all devices in one browser. With many devices the JSON grows; capped naturally by user behavior. No GC strategy this spec.
|
|
553
|
+
- **Boundary regex**: the `font-display` className ban must allow comments or strings inside test files; scope the regex to non-test source.
|
|
554
|
+
- **Removing topbar**: the `disconnected` badge currently shown on topbar disappears. Connection state is still visible per-device in the sidebar tree (`DeviceStatus` dot). No regression.
|
|
555
|
+
- **Single-PR scope**: S0044 touches ~20 files. Keep one commit `S0044: feat: rebuild webui to chatbox style` per repo convention. Avoid mixing unrelated cleanup.
|
|
556
|
+
- **Manual e2e is the real gate**: automated tests catch class names; the visual judgment ("is this Chatbox-like?") is human-only. Reviewer must compare against the user-supplied screenshot before merging.
|