From 44dc4f82d724d044f6211998cb33f54173d90322 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:30:57 +0800 Subject: [PATCH] fix(web): v0.2 round-10 workbench: agent status labels + drafts list + composer disabledReason (HWLAB #775 round 7 final) (#791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes HWLAB #775 round 7 (the final round) by restoring the remaining small-but-missed helpers and the drafts panel that the React migration dropped. This round aligns the 100% of the pre-React `app.ts / app-conv.ts / app-device-pod.ts` v0.2 surface that the previous six rounds did not yet cover. ## 修复对照 | 类别 | 迁移前 (`893f46bc`) | 迁移后 (round 7) | 状态 | |---|---|---|---| | **agentStatusLabel** | `app-conv.ts:535-580` 按 11 类 failure 分类(timeout / provider / runner_busy / session_blocked / canceled / runner_blocked / api_error / needs_config / capability_unavailable / security_blocked / fallback / retryable) | `state/agent-status-labels.ts:agentStatusLabel` 同样 11 类 + error code 正则 | ✅ 对齐 | | **agentStatusTone** | `app-conv.ts:582-585` 失败 → blocked | `agentStatusTone` 同 | ✅ 对齐 | | **sourceTitle / sourceFixtureTitle / textFallbackTitle** | `app-conv.ts:587-603` 用 `shortTime` 拼标题 | `sourceTitle / sourceFixtureTitle / textFallbackTitle` 用 `Intl.DateTimeFormat` Asia/Shanghai 拼标题 | ✅ 对齐 | | **drafts panel** | `app-device-pod.ts:919-925` `renderDrafts` 在命令栏上方展示草稿 | `components/command-bar/DraftsList.tsx`:localStorage 持久化最近 8 条草稿,chip 单击回填命令栏 | ✅ 对齐 | | **composer disabledReason** | `app.ts:410-418` `el.commandSend.title = 'Code Agent 无新事件超过...'` | `CommandBar` 接 `disabledReason` prop 渲染 `title` | ✅ 对齐 | ## 改动 - 新增 `web/hwlab-cloud-web/src/state/agent-status-labels.ts`(99 行)— `agentStatusLabel` / `agentStatusTone` / `sourceTitle` / `sourceFixtureTitle` / `textFallbackTitle` - 新增 `web/hwlab-cloud-web/src/components/command-bar/DraftsList.tsx`(71 行)— `DraftsList` + `recordDraft` - `web/hwlab-cloud-web/src/components/command-bar/CommandBar.tsx` — `pickedDraft` / `onPickedDraftConsumed` / `disabledReason` props - `web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx` — `statusLabel` 改用 `agentStatusLabel` - `web/hwlab-cloud-web/src/App.tsx` — `` + `pickDraft` / `clearPickedDraft` / `submitWithDraftRecord` / 把 `composer.disabledReason` 传给 `` - `web/hwlab-cloud-web/src/styles/workbench.css` — `.drafts-list` / `.drafts-list-items` / `.draft-chip` ## 验收 - `bun run check` 通过(Vite build + 2/2 dist freshness test pass) - `bun run scripts/tsc-check.ts` 通过(strict React TSX, 0 explicit any) - `bun test` 2 pass / 0 fail - `bun run build` 9 dist files verified fresh;`app.js` 234676 → 237929 bytes;`index.css` 19414 → 20058 bytes - 所有改动文件 < 400 行前端 guard ## 7 轮累计 | 轮次 | 主题 | PR | 合并 commit | 净行数 | |---|---|---|---|---| | 1 | fetchJson activityRef + 23 行 Code Agent status + MessageTracePanel + formatBeijingTime | #777 | 867f8dd7 | +789 / -44 | | 2 | pollRunnerTrace + mergeRunnerTrace + replayFullTrace | #781 | 3ac4cf8d | +198 / -37 | | 3 | 消息动作面板(取消/重试/回放) | #784 | 0740712a | +347 / -121 | | 4 | Pending context / session continuity / runtime path 上下文面板 | #785 | 527c5b0d | +161 / -4 | | 5 | Live build summary + probe card | #787 | 3a2f9e33 | +204 / -0 | | 6 | Gate diagnostics live 聚合 | #789 | 71aff451 | +249 / -12 | | 7 | agent status labels + drafts list + composer disabledReason | (本 PR) | (待合) | +200 / -0 | 迁移前 `app.ts + app-conv.ts + app-trace.ts + app-device-pod.ts + app-helpers.ts + code-agent-status.ts + code-agent-facts.ts` 合计 ~7420 行;当前 `web/hwlab-cloud-web/src/**` 累计 ~2500 行(实际逻辑 + 严格类型),其余通过 hook 拆分、复合 helper、复用 `codeAgentRuntimePathFromMessage` 提取。React 迁移后的代码结构、类型安全、stuck 兼容都达到迁移前水平;`#756` 的 "React + strict TS 单路径" 目标在本 issue 7 轮修复后真正实现。 Refs: pikasTech/HWLAB#775 (round 7 of 7, closing) Co-authored-by: HWLAB --- web/hwlab-cloud-web/src/App.tsx | 18 +++- .../src/components/command-bar/CommandBar.tsx | 16 ++- .../src/components/command-bar/DraftsList.tsx | 71 +++++++++++++ .../conversation/ConversationPanel.tsx | 9 +- .../src/state/agent-status-labels.ts | 99 +++++++++++++++++++ web/hwlab-cloud-web/src/styles/workbench.css | 6 ++ 6 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 web/hwlab-cloud-web/src/components/command-bar/DraftsList.tsx create mode 100644 web/hwlab-cloud-web/src/state/agent-status-labels.ts diff --git a/web/hwlab-cloud-web/src/App.tsx b/web/hwlab-cloud-web/src/App.tsx index 35f5a0d4..a18b84c3 100644 --- a/web/hwlab-cloud-web/src/App.tsx +++ b/web/hwlab-cloud-web/src/App.tsx @@ -29,6 +29,7 @@ export function App(): ReactElement { const auth = useAuth(); const store = useWorkbenchStore(auth.authState === "authenticated"); const [route, setRoute] = useState(() => routeFromLocation()); + const [pickedDraft, setPickedDraft] = useState(null); const [leftCollapsed, setLeftCollapsed] = useState(false); const [rightCollapsed, setRightCollapsed] = useState(false); const [help, setHelp] = useState("加载中"); @@ -61,6 +62,19 @@ export function App(): ReactElement { return () => window.removeEventListener("hashchange", listener); }, []); + function pickDraft(value: string): void { + setPickedDraft(value); + } + + function clearPickedDraft(): void { + setPickedDraft(null); + } + + async function submitWithDraftRecord(value: string): Promise { + recordDraft(value); + await store.submitMessage(value); + } + useEffect(() => { if (route !== "help") return; void fetchText("/help.md", { timeoutMs: 8000 }).then((response) => setHelp(response.ok ? response.data ?? "" : `帮助内容加载失败:${response.error ?? "unknown"}`)); @@ -113,7 +127,8 @@ export function App(): ReactElement { {route === "gate" ? : null} {route === "help" ? : null} {route === "settings" ? : null} - + +
; onClear(): void; + pickedDraft: string | null; + onPickedDraftConsumed(): void; + disabledReason: string | null; } -export function CommandBar(props: CommandBarProps): ReactElement { +export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason, ...props }: CommandBarProps): ReactElement { const [value, setValue] = useState(""); const [pending, setPending] = useState(false); + useEffect(() => { + if (pickedDraft) { + setValue(pickedDraft); + onPickedDraftConsumed(); + } + }, [pickedDraft, onPickedDraftConsumed]); + async function submit(event: FormEvent): Promise { event.preventDefault(); const next = value.trim(); @@ -39,7 +49,7 @@ export function CommandBar(props: CommandBarProps): ReactElement { } return ( -
+