The React migration (#756) and the 7-round #775 patch left the
`fetchJson` inactivity-timeout surface half-wired: `client.ts` and the
`api.*` helpers accepted an `ActivityRef`, but `state/workbench.ts`
`submitMessage` and `state/runner-trace.ts` `waitForAgentResult` /
`pollRunnerTrace` never built or forwarded one. The Web UI was therefore
falling back to a **total** timeout: a long-running Code Agent operation
(e.g. `bootsharp` that finished in ~9.7s but the assistant message
chain took ~37s) was killed at `codeAgentTimeoutMs` even while trace
events kept flowing, producing "Code Agent 在超时内未返回可显示正文".
User-facing symptom in #795 is exactly this: the 4th turn on
`D601-F103-V2 bootshar[p]` timed out in the Web UI while the
backend completed the work and emitted `agentrun:result:completed`.
Changes
- `state/workbench.ts` adds a `useRef<number>`-backed `lastActivityAtRef`,
`updateActivity`, `buildActivityRef` and exposes `recordActivity` on the
store. `submitMessage` now bumps activity on the initial submit, on
every trace snapshot, and threads a single `submitActivityRef` through
`api.sendAgentMessage` / `api.steerAgentMessage`, `pollRunnerTrace`,
and `waitForAgentResult`.
- `state/runner-trace.ts` `waitForAgentResult` and `pollRunnerTrace`
accept an `ActivityRefSource`, pass it to `api.getAgentChatResult` so
each per-poll `fetchJson` uses the inactivity-timeout branch, and
`pollRunnerTrace` mutates the ref's `lastActivityAt` / `lastEventLabel`
/ `waitingFor` whenever a fresh snapshot arrives. The outer
`while`-loop hard ceiling becomes `max(totalTimeoutMs * 4, totalTimeoutMs + 60s)`
as a safety net for runaway operations; the per-request
inactivity-timeout (driven by `activityRef`) is the primary abort
signal, matching the pre-React `app-device-pod.ts:fetchJson` contract.
- `components/command-bar/CommandBar.tsx` adds an `onTyping` prop and
fires it from the textarea `onChange` so user-typing resets the
inactivity clock.
- `App.tsx` adds a stable `noteActivity` (`useCallback`) wired to
`store.recordActivity` and passes it as `onTyping` to `<CommandBar>`.
Test
- `scripts/fetchJson-inactivity.test.ts` adds three cases:
1. `fetchJson` without `activityRef` honours the total window.
2. `fetchJson` with 100ms-cadence activity survives the configured
window and aborts ~1-2s after activity stops with the
"无新活动" error (the HWLAB #795 regression assertion).
3. `fetchJson` with 1.5s-cadence activity (the actual
`pollRunnerTrace` interval) survives past the 5s window and only
aborts after activity stops at 6.5s.
Verification
- `bun run scripts/tsc-check.ts` passes (strict React TSX, 0 explicit any)
- `bun run check` 5 pass / 0 fail (3 new + 2 dist-contract)
- `bun run build` 9 dist files verified fresh; `app.js` 234676 → 238740 B
- orphan-listener check (per HWLAB #748): no `el.X.addEventListener`
in `*.tsx` whose X is not in the `el` literal.
Refs
- HWLAB #795 (this issue)
- HWLAB #775 round 1 #777 added the `fetchJson` `activityRef` plumbing
in `client.ts` but stopped short of the workbench / runner-trace layer
- HWLAB #756 React migration baseline
Co-authored-by: Codex <codex@local>
- Replace monolithic app.ts / app-conversation.ts / app-device-pod.ts /
app-skills.ts / app-helpers.ts / app-session-tabs.ts / auth.ts and the
364-line index.html with a Vite-built React + TypeScript SPA.
- New src/ module tree: main.tsx, App.tsx, components/{layout,auth,
conversation,command-bar,device-pod,skills,settings,help},
hooks/, services/api, services/auth, state, types, logic/ (pure
helpers from the old tree), styles/.
- Pure-logic modules (composer-policy, code-agent-facts, code-agent-
status, live-status, message-markdown, app-trace, app-session-tabs,
runtime) move to src/logic/ with their tests preserved; tests still
auto-discover.
- Build pipeline switches from Bun.build to Vite: vite.config.ts
inlines everything into a single app.js + app-assets/ for the dist;
scripts/dist-contract.ts now runs vite build and asserts the dist
against a fresh build instead of comparing dist to source.
- scripts/check.ts now verifies the React entry, the mount-only
index.html, Vite dist, the composer / session / device pod / skills
/ settings / help contracts, the React state shape, and the new
test file locations. It also refuses to run if any legacy app-*.ts
or auth.ts survives at the top level.
- New scripts/frontend-guard.ts enforces the 400-line migration target,
refuses the legacy el map and document.getElementById markers,
refuses to keep app-*.ts / auth.ts at the top level, and verifies
the required module directory layout.
- Update scripts/src/dev-cloud-workbench-smoke-lib.mjs to read the new
src/services/, src/logic/ paths and refresh cloudWebModuleSourceFiles.
- index.html is now a mount shell only (no login shell, no workbench
shell, no device pod sidebar markup). React renders the full UI from
src/main.tsx, satisfying the issue-756 migration goal of catching
unclosed tags at the TypeScript/JSX compile step instead of relying
on browser DOM healing.
- web:check (now: check + frontend-guard + tsc-check + bun test) is
green: 51 pass / 0 fail, Vite build produces dist/app.js (379 kB)
+ dist/index.html + dist/app-assets/. Vite build is reproducible;
dist freshness is verified against a fresh Vite build.
- Grandfathered pure-logic files (> 600 lines): src/logic/app-trace.ts
(1367), src/logic/live-status.ts (1006), src/logic/code-agent-facts.ts
(548), src/logic/code-agent-status.ts (572). Reason: each is a tested
pure-logic classifier with a public contract consumed by both the
CLI renderer and the Web renderer; splitting now would fork the
public function shape across two PRs and the migration target is the
React entry + module split, not a pure-logic refactor.
Refs #756. Closes#756 once merged + deployed to hwlab-v02.
Co-authored-by: Codex <codex@local>