git:20260904.c6805ff to git:20260912.8eb9360

1 added, 1 removed. Audit A to A.

# DOX — packages/client/src/lib/chat
Files in this directory. One row per source file. See change: fold-oversized-agents-directories.
| File | Purpose |
|------|---------|
| `attachment-original-url.ts` | Builds the full-resolution originals URL for a transcript attachment. Exports `attachmentOriginalUrl(sessionId, attachmentId)` -> `/api/sessions/<sid>/attachments/<id>` or `null`. MIRRORS the server's id contract (`^[0-9a-f]{64}$`) and returns `null` for a malformed id, so the zoom path never issues a request the server would only 400. `sessionId` is percent-encoded so it cannot escape its path segment. Consumed by `ChatView`'s ImageAttachments -> ImageLightbox `src`, with the inline fitted derivative passed as `fallbackSrc` (F6: a failing original degrades ONLY the zoom). See change: fit-attachments-for-display. |
| `chat-selection-copy.ts` | Pure `buildSelectionClipboardText(range, container)`: rebuilds clipboard text for a transcript `copy`. → see `chat-selection-copy.ts.AGENTS.md` |
| `chat-virtual-rows.ts` | Pure helpers for the windowed (TanStack Virtual) transcript. → see `chat-virtual-rows.ts.AGENTS.md` `baseRowSize` gains an explicit `custom` arm (160) with the text reserve clamped to the card's 240px body cap. See change: render-inline-reasoning-and-custom-entries. Both custom arms stamp the server-sent `groupId` onto the row (`ChatMessage.groupId`); absent → the gate treats the row as `other`. See change: add-custom-event-group-filters. |
| `coalesce-live-events.ts` | `foldLiveEvents(queued)` — pure fold that coalesces queued live WS events (`QueuedLiveEvent`) before dispatch. |
| `collapse-retried-errors.ts` | ChatView duplicate-collapse helpers. `findRetriedErrorIds` flags failed `toolResult` superseded by successful… → see `collapse-retried-errors.ts.AGENTS.md` |
| `command-filter.ts` | Exports `filterCommands(commands, filter)` — case-insensitive substring match on `CommandInfo.name` or `description`. Returns input unchanged when filter empty. |
| `event-reducer.ts` | `ChatMessage` gains `view?: ViewTarget` field. View rows produced by server-side `ViewMessageStore` are… → see `event-reducer.ts.AGENTS.md` Adds `addNotify(state, notifyId, message, level?)` — appends ONLY an `interactiveUi` row (`ui-<notifyId>`, content `notify`) to `messages`, never an `interactiveRequests` entry; dedups by `notifyId` (not message text) so `replayNotifyLog` is idempotent on a warm reconnect. See change: split-notify-from-prompt-request. `PendingPrompt.status` gains `"failed"`; new `applyPromptTimeout(state,message)` (sending→failed + `lastError`) and `carryPendingPrompt(prompt)` (settled carries, `sending` dropped); `applyPromptReceived` guard widened to any settled status. See change: fix-optimistic-prompt-stuck-sending. `addInteractiveRequest` dedup narrowed to `requestId` equality only (content fallback method+title deleted) so two concurrent same-title prompts both surface. See change: surface-concurrent-ask-user-prompts. `ChatMessage` gains `role:"custom"` + `customType` (NOT a turn boundary — D9 recorded at TURN_BOUNDARY_ROLES); message_end role=custom arm (exact `display===false` + flow-event refusal) and `custom_entry` arm build the same row via `extractCustomEntryBody` (extraction first, 64k char ceiling, then truncateOutputForDisplay — at row creation, live==replay). See change: render-inline-reasoning-and-custom-entries. |
| `followup-entries.ts` | Tolerant read of `pendingQueues.followUp` at the client boundary. Exports `normalizeFollowUpEntry`, `normalizeFollowUpEntries`, coercing legacy `string` and current `{text, imageCount}` into one shape. Applied once in `App.tsx` before `<QueuePanel>`, so a browser tab left open across an extension reload renders readable text instead of `[object Object]`. Delete the string branch one release after the change ships. See change: fix-bridge-followup-image-drop (D2b). |
| `group-tool-bursts.ts` | Temporal burst grouping — OUTER pass over `groupConsecutiveToolCalls`. → see `group-tool-bursts.ts.AGENTS.md` |
| `group-tool-calls.ts` | Collapses repetitive retry loops in chat view. Exports `ToolCallGroup`, `ChatItem`,… → see `group-tool-calls.ts.AGENTS.md` |
| `history-gap.ts` | Client bookkeeping for a windowed replay's gap. Exports `HistoryGapState`, `HISTORY_GAP_ROW_ID` (`__history_gap__`), `createHistoryGapState`, `createHistoryGapRow`, `nextBackfillRange`, `isHeadFree`, `historyGapTerminus`, `shouldAutoLoadHistory`, `SETTLE_MS` (120), `TriggerInputs`, and the pure `captureScrollAnchor`/`restoreScrollAnchor` splice arithmetic (extracted because jsdom reports `scrollHeight` as 0). `nextBackfillRange` is TAIL-anchored (the server retreats `tailMinSeq`; "load earlier" must deliver what immediately precedes what you are reading) and requests the FULL remaining gap — `{fromSeq: floor, toSeq: tailMinSeq - 1}`, no client-side seq window: the server's cap is an EVENT COUNT, so the newest N events are selected from anywhere in the range (a seq window caps how FAR a request reaches, not how much it delivers — the holey-store bug); in a HEAD-FREE window its lower bound is the store FLOOR `oldestGapSeq`, not `headMaxSeq + 1`, so the walk never spends a round trip below the floor nor lands on the empty-response branch that would mislabel *reached the floor* as *nothing servable*. `windowShape` is READ from the server's announcement, never inferred from `headMaxSeq === 0` (the sentinel overloads a bound with a mode and fails silently). `holey` is computed at ANNOUNCE time (`gapCount < tailMinSeq − headMaxSeq − 1`, two-sided windows only) and `twoSidedTerminus` is the dedicated flag for the holey exhaust — `atFloor` remains the head-free SUCCESS terminus; `historyGapTerminus` returns `not-retained` for a holey two-sided gap (removing the row would render head and tail as adjacent, erasing the elision disclosure), `session-start` vs `not-retained` for a head-free one on `oldestGapSeq <= 1`, and null otherwise — answering "is anything below", never "why is it gone". The mid-walk `unservable` state is RETIRED. `shouldAutoLoadHistory` is PURE and keyed on INTENT, not position: `scrollTop` clamps at 0 so a delta rule stalls, and a splice smaller than the proximity band yields no new rising edge. `armed` gates backfill until the initial replay terminates. See change: lazy-load-session-history. See change: fix-lazy-history-backfill-ux. See change: add-tail-only-replay-window. See change: fix-history-backfill-holey-store (D2, D6). |
| `linkify-tool-output.ts` | Pure tokeniser. Exports `tokenize(text): Token[]`, `MAX_LINKS=5000`. → see `linkify-tool-output.ts.AGENTS.md` |
| `message-queue.ts` | Offline outgoing message queue. Exports `MessageQueue` class — `setSendFunction`, `enqueue` (caps at 10,… → see `message-queue.ts.AGENTS.md` |
| `pending-free-floating.ts` | Pure `derivePendingFreeFloating(messages, interactiveRequests)` — the set of pending, free-floating (no `toolCallId`), non-widget-bar asks that render in `MultiAskPanel`. Maps requestId→toolCallId off `interactiveUi` message rows; notifies never enter `interactiveRequests` so are excluded by construction. See change: surface-concurrent-ask-user-prompts. |
| `prompt-answer-encoder.ts` | Pure helper encoding interactive renderer `result` → `answer` string for PromptBus `prompt_response`. → see `prompt-answer-encoder.ts.AGENTS.md` |
| `prompt-component-registry.ts` | Compatibility shim re-exporting prompt component registry from… → see `prompt-component-registry.ts.AGENTS.md` |
- | `refresh-chat.ts` | Chat-refresh coordinator. Exports `refreshChat(sessionId, deps)` + `RefreshChatDeps` (`dropPersisted`, `resetSessionState`, `resetCursor`, `markSubscribed`, `subscribe`, `beginLoadingHistory`, `beginReplayInFlight`). Extracted from `App.tsx`, where the block was duplicated behind the header and mobile `onRefresh` props, so the ORDERING guarantee is unit-testable without mounting the app. Durable invalidation runs BEFORE the in-memory reset: a surviving entry is AUTHORITATIVE (`rehydrateSession` returns its `maxSeq` as the subscribe cursor), so reset-first would leave an interrupted refresh with a stale base plus a fresh tail; delete-first leaves BOTH layers untouched. A failed `dropPersisted` NEVER aborts the refresh (private browsing / disabled storage — refresh is the user's escape hatch precisely when the view looks wrong); residual stale entry accepted, design D6. `beginReplayInFlight` armed here, not at the call sites, so header and mobile cannot drift. See change: purge-replay-cache-on-reset-paths. |
+ | `refresh-chat.ts` | Chat-refresh coordinator. Exports `refreshChat(sessionId, deps)` + `RefreshChatDeps` (`dropPersisted`, `resetSessionState`, `resetCursor`, `markSubscribed`, `subscribe`, `beginLoadingHistory`, `beginReplayInFlight`). Extracted from `App.tsx`, where the block was duplicated behind the header and mobile `onRefresh` props, so the ORDERING guarantee is unit-testable without mounting the app. Durable invalidation runs BEFORE the in-memory reset: a surviving entry is AUTHORITATIVE (`rehydrateSession` returns its `maxSeq` as the subscribe cursor), so reset-first would leave an interrupted refresh with a stale base plus a fresh tail; delete-first leaves BOTH layers untouched. A failed `dropPersisted` NEVER aborts the refresh (private browsing / disabled storage — refresh is the user's escape hatch precisely when the view looks wrong); residual stale entry accepted, design D6. `beginReplayInFlight` armed here, not at the call sites, so header and mobile cannot drift. Dep `requestPromptResync(sessionId)` (D9 of fix-pending-prompt-lost-on-replay) fired UNCONDITIONALLY after reset+subscribe, failure-isolated: a throwing/rejecting send never aborts the refresh nor surfaces an unhandled rejection (X1). See change: purge-replay-cache-on-reset-paths. |
| `selection-anchor.ts` | Pure, stateless anchor arithmetic for the selection-anchoring compensator. Exports `ANCHOR_EPSILON`, `AnchorCorrectionInput`, `computeAnchorCorrection(...)`. → see `selection-anchor.ts.AGENTS.md` |
| `tool-summary.ts` | One-line tool-call summaries (`$ <cmd>`, `Read <path>`, `Grep …`, `git …`, `kb_search …`, `ctx_* …`). → see `tool-summary.ts.AGENTS.md` |