- new deploy/k8s/keycloak/keycloak-bootstrap-job.yaml: one-shot Job that waits for Keycloak, authenticates as admin, creates hwlab realm + hwlab-cloud-web OIDC client via REST API
- secret hwlab-cloud-web-client holds the OIDC client_secret used by cloud-api
- kustomization.yaml picks up the new Job and Secret
- access-control.test.ts: cloud api AgentRun trace records real actor.userId from OIDC-upserted user (issue 788 spec)
- access-control.test.ts: cloud api /v1/users/me returns actor with authMethod=api-key for HWLAB_API_KEY Bearer token
- 31 pass / 4 pre-existing fail
- submitAgentRunChatTurn resolves owner user API key via accessController.store.findActiveDefaultApiKeyForUser
- buildAgentRunRunnerJobInput accepts ownerApiKey; when present, removes HWLAB_DEVICE_POD_API_KEY and adds HWLAB_API_KEY as sensitive
- buildAgentRunTransientEnv no longer emits HWLAB_DEVICE_POD_API_KEY
- codex-stdio child env now reads HWLAB_API_KEY (not HWLAB_DEVICE_POD_API_KEY)
- server-code-agent-http codeAgentAuthEnv looks up user default API key; URL env vars still set unconditionally
- tests: code-agent-session-registry Codex child env carries only device-pod API key now checks HWLAB_API_KEY
- tests: server-agent-chat delegates v0.2 turns removes HWLAB_DEVICE_POD_API_KEY assertion
- 58 pass / 4 pre-existing fail
- authHeaders now reads HWLAB_API_KEY (env or --apiKey/--bearerToken) first; only when hwl_live_ prefix is present
- when both are set, HWLAB_API_KEY wins and x-hwlab-device-pod-api-key is suppressed
- new test asserts the precedence using a fake API key against a local http server
- help auth hint updated to mention the preferred credential
- requestJson now prefers HWLAB_API_KEY (env or --apiKey / --bearerToken) and sends Authorization: Bearer hwl_live_...
- new client auth whoami command returns actor + authMethod via /v1/users/me
- client auth status reports apiKey.source and apiKey.prefix
- authVisibility + authDiagnosis surface apiKeySource/Prefix; invalid key returns api_key_invalid diagnosis
- legacy cookie / auto-login paths still work when no HWLAB_API_KEY is set
Adds the user API key foundation called out in #788:
- New api_keys table with idx_api_keys_user and idx_api_keys_prefix
- users extended with auth_provider/keycloak_issuer/keycloak_sub/email/last_login_at
- Authorization: Bearer hwl_live_... resolves to the same AuthPrincipal
- /v1/api-keys, /v1/api-keys/default, /v1/api-keys/{id}/regenerate, DELETE /v1/api-keys/{id}
- First-admin and /auth/login bootstrap a Default API key
- Short-test mode keeps display_secret; production化 will switch to hash-only later
- Legacy HWLAB_DEVICE_POD_API_KEY and Bearer <session-token> paths preserved
- 4 new access-control.test.ts cases
Refs pikasTech/HWLAB#788
PR #807 collapsed the top #code-agent-summary and per-message debug panels
into a 调试信息 dialog, but the conversation list still showed two hard-coded
System cards (界面模式 / Code Agent 状态) that fed availability into the
agent run path. Both appeared as full <article class="message-card
message-system"> bubbles before any user message on every session load,
contradicting the issue goal of a clean agent / user conversation feed.
- remove the ConversationPanel intro useMemo + [...intro, ...messages]
spread; the conversation list now only contains real agent / user
messages.
- move the static 界面模式 content to a small muted hint line under the
panel title (id=workspace-hint, .conversation-panel-hint).
- the live Code Agent availability snapshot is already covered by the
existing #code-agent-summary + 调试信息 dialog, so it is dropped.
- add a conversation-list empty-state placeholder so the panel keeps
height for layout smoke (messages.length === 0).
- delete the now-orphan MessageRuntimePath / MessageSessionContinuity /
MessagePendingContext components (no remaining callers per rg).
- extend tools/capture-issue-803-noise.mjs to report each .message-card
role plus systemMessageCount / roleCounts so layout-level evidence
can assert "no message-system in the conversation list".
web:check 12 pass / 0 fail; web:layout:build pass (desktop, narrow,
mobile). Issue: pikasTech/HWLAB#803
The Agent workspace (top #code-agent-summary and each agent message card)
exposed too many "未观测" / readiness / providerTrace / runtime-path rows
inline; per-message panels (MessageRuntimePath, MessageSessionContinuity,
MessagePendingContext) ate most of the card width and made the agent trace
events list (the only thing the user really wants to see) read at ~83% of
the card.
This change:
- introduces a reusable <DebugDialog> component (web/hwlab-cloud-web/src/components/shared/DebugDialog.tsx)
that surfaces the full availability / readiness / runnerTrace / raw JSON
payload only when the user clicks a 调试信息 button.
- shrinks codeAgentSummaryRows() in state/code-agent-status.ts to only
return rows with meaningful values; "未观测" / "字段缺失:证据不足" /
"DEGRADED:运行路径字段不完整" / "无"-only readiness blockers are
filtered out of the top summary and remain reachable via the dialog.
- rewrites MessageCard to drop the inline MessageRuntimePath /
MessageSessionContinuity / MessagePendingContext panels and to show a
single compact footer (timestamp + 调试信息 button). The pending running
hint is reduced to a one-line tag instead of a 7-row grid.
- makes .message-trace and .message-trace-events fill the message card so
the trace events list is the dominant element per the issue.
- adds tools/capture-issue-803-noise.mjs so CLI can log into the live or
local Cloud Web, capture the rendered #code-agent-summary / per-message
meta / trace widths into a JSON document, and diff before/after the
refactor.
web:check, web:layout, dev-cloud-workbench-smoke --static, and
dev-cloud-workbench-layout-smoke all pass on the local build.
Issue: pikasTech/HWLAB#803
Co-authored-by: HWLAB Agent <hwlab-agent@pikastech.local>
Follow-up to PR #798 (#795 final). PR #798 closed the total-timeout /
hard-cap issue, but left two residual code-path inconsistencies that
#802 captures:
- Submit path and refresh path used different trace-polling code:
submitMessage called `pollRunnerTrace` + `waitForAgentResult` (a
dual function pair from pre-#798), while hydrate() never subscribed
to the running trace at all. After a page refresh, the user saw
the cloud-api "running" state but no live trace events.
- The submit fail branch only dispatched a local "Code Agent 超时"
message; it never called `persistConversation`, so the failure was
invisible after F5 — the cloud-api conversation would show
"running" forever from the user's perspective.
This PR collapses both into a single `subscribeToTrace` entry point
and wires the refresh path into the same active subscription:
- `state/runner-trace.ts` — replace `waitForAgentResult` +
`pollRunnerTrace` with a single `subscribeToTrace(config)` that
handles both the result endpoint and the trace endpoint in one
`for(;;)` loop. Calls `onActivity` on EVERY successful poll (not
just on new events) so the per-poll inactivity window in
`fetchJson` does not fire on a healthy polling loop. Terminal
status -> `onComplete`; 5xx -> keep polling, no activity; 4xx ->
`onInfrastructureError`; `signal.aborted` -> silent return. The
hard-cap / 4x / `TRACE_HARD_CAP_ATTEMPTS` machinery is gone (was
already removed by #798).
- `state/trace-reattach.ts` — new `useTraceReattach` hook that
subscribes to the running trace on hydrate (same `subscribeToTrace`
call as submitMessage). Reuses an existing agent message slot for
the traceId if one exists; otherwise creates a placeholder. Persists
on both onComplete and onInfrastructureError. Path unification
achieved: one entry, two callers.
- `state/workbench.ts`:
- `submitMessage` calls `subscribeToTrace` directly with a
single-line `onActivity: () => updateActivity()` plus
`onComplete` / `onInfrastructureError` callbacks. The
`onInfrastructureError` branch now ALSO calls
`persistConversation`, persisting the fail state to cloud-api.
- The re-attach useEffect is replaced with a one-liner
`useTraceReattach({...})` call (workbench.ts stays under the
400-line guard).
- `scripts/fetchJson-inactivity.test.ts` — third case tightened:
200ms-cadence activity for 10s with `timeoutMs: 3000`; the
request must live the full 10s and abort only after activity
stops. Asserts `elapsed >= 10_000`.
- `docs/reference/spec-v02-hwlab-cloud-web.md` — the "Code Agent
Timeout Model" section is renamed "Code Agent Timeout Model 与
Path Unification" and now also pins: (a) `subscribeToTrace` is
the only entry; `waitForAgentResult` / `pollRunnerTrace` /
`TRACE_HARD_CAP_ATTEMPTS` MUST NOT exist; (b) `useTraceReattach`
MUST be wired in; (c) `persistConversation` is required in both
onComplete and onInfrastructureError; (d) `state.workspace.activeTraceId`
changes MUST re-attach. Includes a distilled history of
#775 / #777 / #791 / #795 / #797 / #798 / #802.
Verification
- `bun run scripts/tsc-check.ts` → strict React TSX, 0 explicit any
- `bun run check` → 5 pass / 0 fail (3 inactivity + 2 dist-contract)
- `bun run build` → 9 dist files verified fresh; `app.js`
238460 → 240990 B
- `state/trace-reattach.ts` is now wired in; `state/runner-trace.ts`
exports only `subscribeToTrace` (verified by tsc + grep)
- `workbench.ts` line count 400 (under the 400-line guard)
- New 200ms-cadence 10s test passes; old 100ms-cadence 2s test
and total-timeout 200ms test still pass.
Refs: HWLAB #802, #795, #777, #791, #798
Co-authored-by: Codex Agent <codex@hwlab.local>
The CLI was emitting workspace.build/debug.download with action=evidence
for "<pod>:workspace build evidence <jobId>" and "<pod>:debug-probe
download evidence <jobId>". Cloud-api accepted the sub-action form, but
the device-pod executor mapped it to device-host-cli argv as
"workspace build evidence" / "debug-probe download evidence" -- neither
of which exists in the host CLI dispatcher; the host returned
"unsupported command: --profile-json-b64 ..." and the evidence job was
marked failed with blocker.code gateway_dispatch_failed.
Per docs/reference/g14.md (v0.2 device-pod cloud-api architecture),
"The evidence sub-action on workspace.evidence / debug.evidence is a
first-class intent, not a workspace.build sub-action." The executor's
deviceHostArgs already produces the correct argv for the first-class
intent, so the fix lives in the CLI selector parser: when
operation === "build" and rest[1] === "evidence", emit
intent=workspace.evidence with kind=build and the explicit jobId; the
same shape for operation === "download" -> debug.evidence with
kind=download. Non-evidence sub-actions (start, status, wait, output,
cancel) are unchanged.
Live verification on G14 against http://74.48.78.17:19667 (D601-F103-V2,
profileHash sha256:0352cd14...):
- before: device-host-cli argv = "workspace build evidence"
-> gateway_dispatch_failed
- after: device-host-cli argv = "workspace evidence build <jobId> ..."
accepted by cloud-api with status=running, blocker=null
- the executor argv proves the fix; the D601 Windows host
F:\Work\ConStart\tools\device-host-cli.mjs is still a stale
D601 ops-side copy and is tracked separately in #779 (out of scope
for #801).
Adds:
- device-pod-cli.test.ts: evidence sub-action -> first-class intent
- device-pod-cli.test.ts: build start / download start regression
(non-evidence sub-actions keep workspace.build / debug.download)
Out of scope: COM9 -> COM3 UART port drift on the Windows device pod
host (separate issue from #801; no UART/profile port code touched).
Follow-up to PR #797: drop every wall-clock or attempt-count ceiling from
`state/runner-trace.ts` so the Web UI's Code Agent inactivity-timeout
contract is total-cap-free. The previous PR kept a
`max(totalTimeoutMs * 4, totalTimeoutMs + 60s)` outer-loop safety net
plus a `TRACE_HARD_CAP_ATTEMPTS = 120` poll-count cap, which would
re-introduce the same regression class that #795 was filed against: a
long-running operation that keeps emitting trace events could still be
killed by the outer ceiling even with an active `activityRef`. The
"Code Agent Timeout Model" section of
`docs/reference/spec-v02-hwlab-cloud-web.md` now pins the contract:
- The sole abort signal is per-poll `fetchJson` inactivity-timeout,
driven by `activityRef`.
- The outer `waitForAgentResult` / `pollRunnerTrace` loops are
`for (;;)`. They only exit when the upstream returns a terminal
status, the request fails with a non-5xx, or the inactivity window
fires inside `fetchJson`.
- The per-poll `getAgentChatResult` call passes the full
`totalTimeoutMs`; the inactivity window is **not** shrunk by
wall-clock elapsed time.
- `TRACE_HARD_CAP_ATTEMPTS` and the 4x / +60s outer cap are deleted.
- Runaway protection is the caller's responsibility: Web users have
cancel / steer / close-tab; CLI has `--timeout-ms`. The browser does
not introduce an implicit cap.
Changes
- `web/hwlab-cloud-web/src/state/runner-trace.ts`:
- Drop `const hardCapMs = Math.max(totalTimeoutMs * 4, totalTimeoutMs + 60_000)`
and the `if (Date.now() - startedAt >= hardCapMs) break;` checks in
both `waitForAgentResult` and `pollRunnerTrace`.
- Replace `while (Date.now() - startedAt < hardCapMs)` with
`for (;;)`. Drop the now-unused `startedAt` locals and the
`let attempt = 0; attempt > TRACE_HARD_CAP_ATTEMPTS` guard.
- Pass `totalTimeoutMs` directly to `api.getAgentChatResult` instead
of `totalTimeoutMs - (Date.now() - startedAt)` so the per-poll
inactivity window is stable.
- Delete `const TRACE_HARD_CAP_ATTEMPTS = 120;` (no longer referenced).
- `web/hwlab-cloud-web/scripts/fetchJson-inactivity.test.ts`:
- Replace the "1.5s cadence 6.5s" case with a stricter "200ms cadence
10s" case: `timeoutMs=3000` inactivity window, continuous 200ms
activity for 10s, then stop. Assert `elapsed >= 10_000` and that
the abort is the inactivity-timeout error. This pins the
`elapsed >= 10000` invariant in the spec.
- `docs/reference/spec-v02-hwlab-cloud-web.md`:
- Strengthen the existing bullet in "在系统中的职责划分" to
explicitly forbid total-timeout, hard cap, `codeAgentTimeoutMs * N`,
poll-count cap, and wall-clock-shrunk per-poll windows.
- Add a dedicated "Code Agent Timeout Model" section after "## 内部架构"
that distils the contract, lists the three invariants, and records
the history of #775 / #777 / #791 / #795 leading to the final state.
Verification
- `bun run scripts/tsc-check.ts` → strict React TSX, 0 explicit any
- `bun run check` → 5 pass / 0 fail (3 inactivity + 2 dist-contract)
- `bun run build` → 9 dist files verified fresh; `app.js` 241802 → 238460 B
(delta reflects the deleted hardCap / TRACE_HARD_CAP_ATTEMPTS code)
- New 200ms-cadence 10s inactivity test runs for ~13s and asserts the
request lives the full window — proving the outer cap is gone.
Refs: HWLAB #795 (final), #777, #791, #775
Co-authored-by: Codex <codex@local>
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>
P0 freezes the concept boundary between HWLAB (business session/workspace) and
AgentRun (tenant/policy/execution):
- HWLAB adapter now always uses DEFAULT_HWLAB_AGENTRUN_PROJECT_ID /
DEFAULT_HWLAB_AGENTRUN_PROVIDER_ID / DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE on
AgentRun runPayload, regardless of caller options. Business projectId belongs
in workspaceRef only, not in AgentRun projectId.
- commandPayload.projectId is now always set to the HWLAB-fixed
DEFAULT_HWLAB_AGENTRUN_PROJECT_ID, never undefined and never derived from
caller options.
- client agent send --session-id <SES> --conversation-id <CONV> without
--provider-profile now looks up the explicit session record to inherit
providerProfile / threadId / conversationId. Mismatched --conversation-id
against the session record aborts with session_conversation_mismatch
before dispatching to AgentRun (prevents stale session state from being
used as a new conversation).
Contract tests added (all 4 passing):
- internal/agent/agentrun-dispatch.test.mjs: HWLAB AgentRun assembly fixes
tenantId/projectId/providerId/backendProfile regardless of options; command
payload does not leak business projectId into AgentRun command projectId.
- tools/hwlab-cli/client.test.ts: send --session-id + --conversation-id
inherits explicit session providerProfile; fails with
session_conversation_mismatch when the session points at a different
conversation.
P2 (CLI/trace result segregation) and P3 (state machine convergence) remain
for follow-up. Tracked via #792 comment chain.
Tracked-by: pikasTech/HWLAB#792
Live CLI verification at 74.48.78.17:19667 showed that
workspace.evidence dispatched correctly through cloud-api and
the executor (executor returned text with the actual error),
but the device-host-cli threw unsupported command because
the new evidence subcommand was nested under command=build
instead of being its own top-level command.
The args shape for workspace.evidence from deviceHostArgs is
[workspace, evidence, kind, ...options], so the main() match
in device-host-cli should hit
at the workspace group level, not
under . Same fix for debug-probe.
After the move, readJobEvidence is dispatched with the right
kindPrefix (keil-build for workspace.evidence kind=build,
keil-download for debug-probe.evidence kind=download) and
the optional jobId from rest[1]. Tail defaults to 200 lines,
--full for the entire log.
Tracked-by: pikasTech/HWLAB#773
Tracks pikasTech/HWLAB#773. PR #765 fixed selector confusion but did
not touch cloud-api evidence propagation. This change closes the
real root cause and adds the read-only evidence selectors that #760
follow-up called for.
cloud-api (internal/cloud/access-control.ts):
- DEVICE_JOB_INTENTS adds workspace.evidence and debug.evidence.
- DEVICE_JOB_READ_ONLY_SUB_ACTIONS = { status, output, wait, cancel,
evidence } and DEVICE_JOB_ACTIONABLE_INTENTS = { workspace.build,
debug.download } make the mutating-intent / sub-action matrix
explicit.
- _deviceJobRequiresReason(intent, args, reason) returns false when
the caller already provided reason OR when the actionable mutating
intent is paired with a read-only sub-action. debug.reset and other
non-actionable mutating intents still always require reason.
- executorOutputPayload now also surfaces output.summary,
nestedOutput.summary, evidence.text, evidence.logTail and
evidence.summary as body.output.text, so a dispatcher that
includes the host logTail / buildSummary in its result becomes
visible to the Code Agent without a separate bootsharp dance.
device-pod executor (cmd/hwlab-device-pod/main.ts):
- gatewayDispatchText also walks result.evidence.{text,logTail,
summary}, dispatch.message (only when dispatchStatus=completed),
dispatch.summary, result.summary and dispatch.buildSummary before
falling back to JSON.stringify.
- deviceHostArgs maps workspace.evidence / debug.evidence to the
host device-host-cli evidence subcommands.
device-host-cli (skills/device-pod-cli/assets/device-host-cli.mjs):
- new readJobEvidence(kindPrefix, requestedId, { tail, full,
target }) reads the most recent (or specified) keil-build /
keil-download job log file and returns it as keil-build.evidence /
keil-download.evidence. tail defaults to 200, --full for entire
log. Missing job returns ok=false but does not throw.
- workspace build evidence and debug-probe download evidence now
wired in main() and help text.
device-pod-cli (tools/src/device-pod-cli-lib.ts):
- selectorCheatSheet adds the evidence row and clarifies that
build/download status/output/wait/cancel/evidence are read-only
and do NOT need --reason. build/download start still require
--reason. usage examples now include the two evidence selectors.
- failed-fast selectors are unchanged; existing 26 tests still pass.
SKILL.md (skills/device-pod-cli/SKILL.md):
- Selector Cheat Sheet adds workspace.evidence and
debug.evidence rows.
- #773 follow-up note: --reason is now required only for
mutating sub-actions, not for the read-only ones.
cloud-api tests (internal/cloud/access-control.test.ts):
- workspace.evidence and debug.evidence must be in DEVICE_JOB_INTENTS.
- _deviceJobRequiresReason signature and the read-only / actionable
branch table.
- executorOutputPayload must look at evidence and summary fields
in addition to text. 3 new tests; pre-existing 4 workbench
failures are unrelated to this change and reproduce on the
unchanged v0.2 base.
Verification (host-side, G14 /root/hwlab-v02, source=0cf9a8c6):
- bun test tools/device-pod-cli.test.ts → 26/26 pass.
- bun test internal/cloud/access-control.test.ts → 23 pass,
4 pre-existing workbench failures (unchanged on v0.2 base).
- bun test cmd/hwlab-device-pod/main.test.ts → 7/7 pass.
- node --check skills/device-pod-cli/assets/device-host-cli.mjs
→ syntax OK.
Hot probe: live v0.2 cloud-api at 74.48.78.17:19667 confirmed
job_devicepod_804c5db4... (workspace.build) returned
text="", bytes=0, output={} before this change. After the
executor text-extraction update and the new evidence selector,
a follow-up workspace.build start + build evidence will surface
the host logTail / buildSummary in body.output.text without
requiring bootsharp + host file fallback.
Tracked-by: pikasTech/HWLAB#773
Fixes HWLAB #775 round-1 regression vs pre-React `app.ts` /
`app-conversation.ts` / `app-device-pod.ts` / `app-trace.ts` /
`code-agent-status.ts`. This PR lays the foundation for rounds 2-7
by restoring three of the three explicitly named regressions:
1. **Inactivity timeout** (the "应该是无活动超时,而非全超时" line)
- `services/api/client.ts` `fetchJson` / `fetchText` now accept
`activityRef`; an internal `schedule()` re-evaluates
`remainingMs = timeoutMs - (now - lastActivityAt)` every 1s, so
long-running fetches stay alive while upstream keeps emitting
trace events. The AbortError now distinguishes "总超时" vs
"无活动超时(idle Ns;waitingFor=…;lastEventLabel=…)".
- Mirrors pre-React `app-device-pod.ts:1219-1293` `fetchJson` contract.
2. **Code Agent status 23 rows** (the "裸 JSON 出现" line)
- New `state/code-agent-status.ts` (`classifyCodeAgentStatusSummary`,
`codeAgentRuntimePathFromMessage`, `codeAgentSummaryRows`) lifts
the 23-row pre-React `codeAgentSummaryRows` into React; rows cover
codeAgent.status, provider/mode/backend, capabilityLevel, session,
sessionId/status, 会话提示, workspace, sandbox, runnerKind,
protocol, implementationType, providerTrace.{command,terminalStatus,
failureKind}, 运行路径语义, toolCalls, skills, conversation facts,
runnerTrace, readiness blockers, last traceId, 当前部署 revision.
- ConversationPanel's `<CodeAgentSummary>` now renders the 23 rows
through `codeAgentSummaryRows(summary)`. Blockers render through a
collapsible "展开 readiness 原始 JSON" disclosure so unmodeled
blockers stay inspectable without dumping bare JSON inline.
3. **Rich trace panel** (the "Trace 过程事件实时滚动,事件渲染等全丢了" line)
- New `components/conversation/MessageTracePanel.tsx` replaces
`ConversationPanel`'s inline `<TracePanel>`: 32-row tail (was 16)
with `traceUiKey` storage, default-open per status, toolbar with
`count / follow / jump-to-bottom / lastEventLabel / last ts / short
traceId`, per-row tone (`tone-border-<tone>` from `event.status`),
follow-on-scroll, follow-on-`pre` body with markdown harden hook,
"等待后端事件" placeholder when events is empty, and
`markTraceScrollIntent`-equivalent scroll handler.
- Pre-React `app-conv.ts:1337-1500` semantics are now rendered through
the same `traceUiKey` storage and a follow/pause toggle.
4. **Beijing time formatting** (the "formatBeijingTime" regression)
- `utils.ts` adds `formatBeijingTime(value, {withSeconds, includeYear})`
using a shared `Intl.DateTimeFormat` Asia/Shanghai formatter; the
pre-existing `formatTimestamp` now delegates to it.
- `MessageCard` and `MessageTracePanel` now render `formatBeijingTime`
in `withSeconds: true` mode so users see `2026-06-03 19:56:20` not
raw ISO `2026-06-03T11:56:20.731Z`.
5. **`api.gateDiagnostics` and `api.cancelAgentMessage` clients** are now
declared alongside the existing `sendAgentMessage` / `getAgentChatResult`
so round 6 can wire `GateView` and round 3 can wire 取消当前请求
without another client surface churn.
## Verification
- `cd web/hwlab-cloud-web && bun run check` passes (Vite build + 2/2 dist
freshness test pass + 12 fresh dist files)
- `bun run scripts/tsc-check.ts` passes (strict React TSX, 0 explicit any)
- `bun test` 2 pass / 0 fail
- `bun run build` 9 dist files verified fresh; `app.js` grew 198671 →
213154 bytes; `index.css` grew 13483 → 15324 bytes
- new `app.js` contains: `activityRef`, `inactivityDetail`,
`MessageTracePanel`, `codeAgentSummaryRows`,
`classifyCodeAgentStatusSummary`, `formatBeijingTime`
- new `index.css` contains: `.message-trace-toolbar`,
`.message-trace-events`, `.message-trace-list`, `.message-trace-row`,
`.message-trace-body`, `.message-trace-empty`, `.message-trace-last`,
`.message-trace-time`, `.message-trace-tone`, `.message-trace-label`,
`.code-agent-summary-rows`, `.code-agent-summary-row`,
`.code-agent-summary-key`, `.code-agent-summary-value`,
`.code-agent-summary-raw`
- `web/hwlab-cloud-web/src/state/code-agent-status.ts` 398 lines, the
other new/edited files are also within the 400-line frontend guard.
Refs: pikasTech/HWLAB#775 (round 1 of 7)
Co-authored-by: HWLAB <ci@hwlab.local>