Skip to content

Chat: useChatTranscript hook with a pure reducer for chat events #838

Open

Description

@smart-ssh-planner

Goal

How chat events turn into the visible transcript is one pure function that can be tested without a DOM, and the chat panel subscribes to events through one hook.

Context

apps/smart-ssh-community/frontend/src/components/ChatPanel.tsx has 3,058 lines. The component function ChatPanel spans :401–1737 (JSX from ~:1444) with 44 useState and 17 useEffect. One effect (:662–998) subscribes to about 26 chat events (onChatAiRequestStarted, onChatTextDelta, onChatActionProposed, onRiskAssessmentUpdated, … onChatDocumentGenerated, wrappers in src/events.ts) and reduces them with many setters per event (setRetrying, setAiWait, setItems, …). About 1,320 further lines hold 15 sub-components and helpers: RiskBadge (:1737), RiskBadges (:1758), RiskHintFootnote (:1816), errorCardText/ErrorCardExtras (:1829–1895), ChatItemView (:1896–2292, exported), RunningCommandIndicator (:2293), ConfirmActionForm (:2336), reasonList/CoverageList (:2430–2510), QuickRuleButton (:2511–2720, exported), BinaryFileChangeHint (:2721), ActionResultView (:2734), TakeIntoNoteButton (:2803), ScrollablePre (:2855), DocumentCard (:2863), AssistantMessageView (:2951), attachmentLine (:3047); exported helpers ChatItem type (:119) and splitInjectionReason (:323). Tests: ChatPanel.test.tsx and 13 ChatPanel*.test.tsx files (about 130 tests). Line numbers are from develop before the colour-token migration and may shift slightly.

The event effect (:662–998) updates items, retrying, aiWait, injectionChecks, context usage, budget-waiting cards (with timers), web-research notices, question cards and document cards with several setters per event (e.g. onChatTextDelta ~:676–684 copies the whole items array and appends the delta). The effect depends only on sessionId (previous issue in this series).

End state of the series: no file over 800 lines, no function component over 400 lines, at most 12 useState per component; sub-components and hooks under src/components/chat/.

Requirements

  • src/components/chat/chatTranscriptReducer.ts: a pure reducer chatTranscriptReducer(state, action) with one action type per chat event (plus local actions the panel dispatches today through the same setters, e.g. clearing on "New chat", loading history). State covers everything the effect writes today.
  • src/components/chat/useChatTranscript.ts: useChatTranscript(sessionId, options) subscribes to all chat events (filtered by sessionId), dispatches to the reducer and exposes state plus the dispatch functions the panel needs. The subscription effect depends only on sessionId; values such as the second-opinion setting are read through refs.
  • Timers (budget-waiting card removal) are owned by the hook and cleared on unmount and session change.
  • ChatPanel uses the hook; the moved useState calls disappear from it.
  • Item identity: an update to the last assistant item creates a new object only for that item; all other items keep their object identity (prepares later memoisation, but no memo is added now).

Acceptance Criteria

  • chatTranscriptReducer has unit tests (no React, no DOM) for every action type, including: deltas append to the last assistant item or start a new one; an action proposal ends the AI wait state; a risk-assessment update changes only the matching action item; retry/error/empty/refused/cancelled responses; context compaction; question asked; document generated.
  • A reducer test asserts that applying a text delta keeps the object identity of all items except the last.
  • The test from the previous issue (toggle second opinion while deltas arrive, no delta lost) still passes.
  • ChatPanel has at most 25 useState calls left.
  • The test count of ChatPanel*.test.tsx (plus the new reducer tests) is not lower than before; all pass.
  • npm run lint, npx vitest run, npm run build and npm run test:e2e pass.

Technical Notes

  • Pure refactoring: no change to behaviour, visible text, i18n keys, CSS classes, data-testids, ARIA attributes, keyboard handling, invoked Tauri commands or listened events. Behaviour changes, if you find a bug, go into a separate issue (mention it in the PR).

  • Colour classes in these files are being migrated to theme tokens in a separate issue that must be merged first; keep exactly the classes that are on develop when you start.

  • Exports that other files or tests import from the original file keep working: either update the importers or re-export from the original path. The extension API (src/extensions/*, src/api.ts, src/components/ui) does not change.

  • Tests: count it(/test( in the listed test files before and after; the number must not decrease. Tests may move next to an extracted unit, but their assertions stay the same. Tests drive the user-visible behaviour through FileBrowserPanel/ChatPanel/ServerForm as today.

  • New code comments in English; existing German comments may move unchanged.

  • No filter, risk, redaction or credential logic is touched (frontend only).

  • Gate: npm run lint, npx vitest run, npm run build in apps/smart-ssh-community/frontend, plus npm run test:e2e if a browser test covers the component.

  • Do not add React.memo, useMemo around item rendering, or list virtualisation in this series. DocumentCard reads the document-action registry with listDocumentActions() during render (:2913); actions registered later only appear because the chat re-renders. Memoising the item views is only safe once that registry is reactive (subscribe + useSyncExternalStore, like the composer registry at :411–412), which is a separate change.

  • One shared event hub (one Tauri listener per event name for all open tabs) would reduce listener count with many tabs, but is out of scope here.

Spec impact: none

Related Work

Activity

  1. added this to the 0.8.0 milestone on Oct 26, 2026
  2. 21 remaining items

  3. marked this as blocking 15 issues on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    frontendFrontend (React/TypeScript)

    Type

    Fields

    Priority

    P2 - Normal

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions