Skip to content

refactor(frontend): split the 4345-line page.tsx monolith - #166

Open
lucastononro wants to merge 2 commits into
fix/102-error-boundaryfrom
fix/97-split-page-tsx
Open

refactor(frontend): split the 4345-line page.tsx monolith#166
lucastononro wants to merge 2 commits into
fix/102-error-boundaryfrom
fix/97-split-page-tsx

Conversation

@lucastononro

@lucastononro lucastononro commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Breaks up frontend/src/app/page.tsx (4345 lines) into a useSessionStream hook plus focused view components — a pure structural move with no behavior change. page.tsx drops to 753 lines and keeps only orchestration: app context, canvas open/collapse, send/attach/S3 handlers, pending-message drains, the header, layout panels, and modals.

Stacked on #102 (fix/102-error-boundary) — preserves that branch's SSEStreamContext publish fan-out, memoized ChatItemView, and error boundaries exactly as they were.

Changes

page.tsx: 4345 → 753 lines. Everything below was moved verbatim (comments included); only imports/exports and prop plumbing changed.

New file Lines What moved there (from page.tsx)
src/lib/useSessionStream.ts 1306 The useSessionStream(experimentId, sessionId, {openCanvas, collapseCanvas, onReset}) hook: the single per-session EventSource (connectSSE), the ~650-line onmessage reducer (L417–1056), addItem, resetSessionState, and the session load/hydration effect (L1101–1525). Owns and returns typed slices: chatItems, tasks, sessionState, loading, sseConnected, canvas/report content, fileTree, generatedFiles, htmlArtifacts, metricPoints/chartConfig/logEvents, usageTotals/recentUsage, activeAgents, plus streamingItemIdRef/pinnedToBottomRef.
src/lib/chatItems.ts 92 ChatItem / ChatItemMeta interfaces + metaStr/metaNum runtime readers (L117–206).
src/components/chat/ChatPane.tsx 280 The chat panel: scrollable stream + pinned-to-bottom auto-scroll tracking, inline tasks card, typing indicator, and the in-session input bar with attach menu (L2164–2332, plus the scroll effect from L373–397).
src/components/chat/WelcomeScreen.tsx 236 The welcome screen: logo, centered input + attach menu, SUGGESTIONS chips + handleSuggestion (L212–229, L2011–2153).
src/components/chat/ChatItemView.tsx 205 Memoized ChatItemView + CHAT_MARKDOWN_PLUGINS + renderGroupedChatItems/isToolItem (L4018–4345).
src/components/chat/ToolGroupCard.tsx 67 ToolGroupCard (L3506–3565).
src/components/chat/CollapsibleToolCard.tsx 158 CollapsibleToolCard + FUN_VERBS/PAST_VERBS/useFunVerb (L3454–3660).
src/components/chat/SubAgentCard.tsx 110 SubAgentCard (L3727–3822).
src/components/chat/ClarificationCard.tsx 123 ClarificationCard (L3824–3932).
src/components/chat/AgentToolCard.tsx 89 AgentToolCard + AGENT_TOOL_META + _agentLabel (L3934–4016).
src/components/chat/UserMessageFilePills.tsx 50 UserMessageFilePills + FILE_PILL_PREVIEW (L4060–4098).
src/components/chat/AttachedFilesPreview.tsx 109 AttachedFilesPreview + humanBytes + pill/hint constants (L4100–4203).
src/components/chat/agentMeta.ts 66 Shared AGENT_META / AGENT_COLORS (L3666–3725).
src/components/workspace/WorkspaceCanvas.tsx 653 The workspace panel (formerly inline WorkspaceSidebar, renamed to match the issue): tree sidebar, tab strip, MRU tab mounting, lineage tab, window-event listeners, default-tab picker (L2824–3452).
src/components/workspace/MetricsPanel.tsx 32 The metrics tab body (wrapper around the existing MetricsTab).
src/components/workspace/FileViewer.tsx 180 FileViewer + the SyntaxHighlighter language registration (L99–115, L2617–2776).
src/components/workspace/HtmlPanel.tsx 105 HtmlPanel + humanArtifactBytes (L2519–2615).
src/components/workspace/ReportMarkdown.tsx 54 ReportMarkdown (L2778–2822).
src/components/workspace/FileTreeRow.tsx 82 FileTreeRow (L2442–2517).
src/components/workspace/fileIcons.ts 40 getFileIconInfo + DIR_LABELS/DIR_COLORS (L2413–2440).

Plumbing notes (only deltas from a verbatim move):

  • The hook takes stable openCanvas/collapseCanvas callbacks (panel ref stays in the page) and an onReset callback so resetSessionState can still clear the page-owned input draft.
  • The attach-menu refs (attachMenuRef, file/folder inputs) stay in the page and are passed to both WelcomeScreen and ChatPane (only one mounts at a time), so the outside-click close handler is unchanged.
  • Dropped two imports that were already unused (TaskEventData, stripSessionPrefix) and the unused sidebarOpen/setSidebarOpen destructure.
  • ChatPane/WelcomeScreen take isRunning/activeProjectId/experiments/activeSessionId as props from the page (which reads them from useApp()), per the frontend/AGENTS.md "props over context" convention.

Test plan

  • Existing: every UI flow behaves identically — this is a pure move (verified by a line-level diff of old vs. new corpus: every substantive original line survives verbatim modulo renames/prop plumbing). Reviewer verifies via npm run build + manual smoke (welcome screen send/attach/S3, in-session chat streaming + stop, tool/sub-agent/clarification cards, workspace tabs incl. report/metrics/lineage/HTML artifacts, session switch reset, reload hydration).
  • New: none.
  • npx tsc --noEmit ✅ · npm run lint ✅ · npm run build

Closes #97

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4

Greptile Summary

This PR breaks the 4345-line page.tsx monolith into 20 focused files — a useSessionStream hook, chat-panel components, and workspace components — with no intentional behavior change. The split is clean and well-executed: callbacks are kept stable, session-scoped values flow as props rather than being re-read from context (addressing the prior review's coupling concern), and the ErrorBoundary key={activeSessionId} correctly resets workspace state on session switches.

  • useSessionStream.ts owns the single EventSource, the full onmessage reducer, and all session-scoped state slices; resetSessionState correctly clears every slice (including tasks) on experiment switch.
  • ChatPane / WelcomeScreen now receive activeSessionId, activeProjectId, experiments, and isRunning as props, removing the AppContext coupling flagged in the prior round.
  • WorkspaceCanvas accepts a generatedFiles prop that is destructured but never read; the component derives file navigation from fileTree alone, making the prop dead weight.

Confidence Score: 5/5

Safe to merge — this is a well-scoped structural extraction with no intentional behavior changes and all prior review concerns addressed.

Every substantive piece of logic is a verbatim move: the onmessage reducer, session hydration, resetSessionState (including task clearing), auto-scroll tracking, and the session-keyed ErrorBoundary around WorkspaceCanvas are all preserved correctly. Stable callbacks flow through the hook interface as documented. The only finding is a dead generatedFiles prop in WorkspaceCanvas that was carried over from the original code without causing any incorrect behavior.

WorkspaceCanvas.tsx — the unused generatedFiles prop and corresponding import of GeneratedFile can be removed; the call site in page.tsx would drop that one prop too.

Important Files Changed

Filename Overview
frontend/src/lib/useSessionStream.ts Core hook extracted from page.tsx: owns the single EventSource, ~650-line onmessage reducer, all chat/task/canvas/metric/usage state slices, and session-load hydration. Logic is a verbatim move; the hook interface is well-typed and stable callbacks are correctly threaded through.
frontend/src/app/page.tsx Reduced from 4345 to 753 lines; retains orchestration only (canvas open/collapse, pending-message drains, attach/S3 handlers, header, modals). Props are wired cleanly to child components; context usage is appropriately scoped to page-level state.
frontend/src/components/workspace/WorkspaceCanvas.tsx Formerly WorkspaceSidebar in page.tsx; handles tab management, file tree, lineage, metrics, and HTML artifact tabs. The generatedFiles prop is declared and received but never consumed anywhere in the component body.
frontend/src/components/chat/ChatPane.tsx Chat panel extracted with all session-scoped values as props (not context), resolving the coupling concern from the prior review. Auto-scroll ref tracking and pinned-to-bottom logic are preserved correctly.
frontend/src/components/chat/WelcomeScreen.tsx Welcome screen extracted with activeProjectId and experiments as props; previous context-coupling concern is addressed. Suggestion chips and attach menu work identically to original.
frontend/src/components/chat/ChatItemView.tsx Memoized per-item renderer extracted cleanly; stable CHAT_MARKDOWN_PLUGINS array and renderGroupedChatItems helper preserved correctly.
frontend/src/lib/chatItems.ts Clean extraction of ChatItem/ChatItemMeta interfaces and metaStr/metaNum runtime readers; no behavioral changes.
frontend/src/components/workspace/FileViewer.tsx FileViewer plus SyntaxHighlighter language registration extracted without changes; fetches file content by path via api correctly.
frontend/src/components/chat/CollapsibleToolCard.tsx Verbatim extraction of CollapsibleToolCard, FUN_VERBS/PAST_VERBS, and useFunVerb with no functional changes.
frontend/src/components/workspace/HtmlPanel.tsx HtmlPanel and humanArtifactBytes extracted; renders sandboxed iframe for agent HTML artifacts correctly.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[page.tsx\n753 lines] --> B[useSessionStream\n1306 lines]
    B -->|chatItems, tasks, canvas,\nfileTree, metrics, usage,\nactiveAgents| A

    A --> C[WelcomeScreen\n236 lines]
    A --> D[ChatPane\n280 lines]
    A --> E[WorkspaceCanvas\n653 lines]

    D --> F[ChatItemView\n205 lines]
    F --> G[ToolGroupCard]
    F --> H[CollapsibleToolCard]
    F --> I[SubAgentCard]
    F --> J[ClarificationCard]
    F --> K[AgentToolCard]
    F --> L[UserMessageFilePills]

    D --> M[AttachedFilesPreview]
    C --> M

    E --> N[FileTreeRow]
    E --> O[FileViewer]
    E --> P[HtmlPanel]
    E --> Q[MetricsPanel]
    E --> R[ReportMarkdown]

    B -->|SSEEvent fan-out| S[SSEStreamContext]
    B -.->|openCanvas\ncollapseCanvas\nonReset| A
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[page.tsx\n753 lines] --> B[useSessionStream\n1306 lines]
    B -->|chatItems, tasks, canvas,\nfileTree, metrics, usage,\nactiveAgents| A

    A --> C[WelcomeScreen\n236 lines]
    A --> D[ChatPane\n280 lines]
    A --> E[WorkspaceCanvas\n653 lines]

    D --> F[ChatItemView\n205 lines]
    F --> G[ToolGroupCard]
    F --> H[CollapsibleToolCard]
    F --> I[SubAgentCard]
    F --> J[ClarificationCard]
    F --> K[AgentToolCard]
    F --> L[UserMessageFilePills]

    D --> M[AttachedFilesPreview]
    C --> M

    E --> N[FileTreeRow]
    E --> O[FileViewer]
    E --> P[HtmlPanel]
    E --> Q[MetricsPanel]
    E --> R[ReportMarkdown]

    B -->|SSEEvent fan-out| S[SSEStreamContext]
    B -.->|openCanvas\ncollapseCanvas\nonReset| A
Loading

Reviews (2): Last reviewed commit: "fix(review): address Greptile findings o..." | Re-trigger Greptile

Pure structural move, no behavior change. page.tsx drops from 4345 to
753 lines:

- lib/useSessionStream.ts: new useSessionStream(experimentId, sessionId)
  hook owning the single per-session EventSource, the ~650-line
  onmessage reducer, the session reload/hydration effect, and every
  state slice those write (chat items, tasks, canvas/report, file tree,
  metrics, log events, HTML artifacts, usage totals, active agents),
  returning typed slices.
- lib/chatItems.ts: ChatItem/ChatItemMeta types + metaStr/metaNum.
- components/chat/: ChatPane (chat stream + in-session input bar),
  WelcomeScreen, ChatItemView (+ renderGroupedChatItems), ToolGroupCard,
  CollapsibleToolCard, SubAgentCard, ClarificationCard, AgentToolCard,
  UserMessageFilePills, AttachedFilesPreview, agentMeta (shared
  AGENT_META/AGENT_COLORS).
- components/workspace/: WorkspaceCanvas (formerly the inline
  WorkspaceSidebar), MetricsPanel, FileViewer, HtmlPanel,
  ReportMarkdown, FileTreeRow, fileIcons.

page.tsx keeps only orchestration: app context, canvas open/collapse,
send/attach/S3 handlers, pending-message drains, header, layout panels,
and modals. Preserves the SSEStreamContext publish fan-out,
ChatItemView memoization, and error boundaries from the base branch.

Closes #97

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Comment thread frontend/src/components/chat/ChatPane.tsx Outdated
Comment on lines +46 to +51
streamingItemIdRef: MutableRefObject<string | null>;
/** Whether the user is scrolled near the bottom of the chat pane (see
* ChatPane's scroll tracking). Owned here because `addItem` re-pins it
* when the user sends a message. */
pinnedToBottomRef: MutableRefObject<boolean>;
// Workspace state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Scroll-tracking ref living inside the SSE hook

pinnedToBottomRef is explicitly documented as tracking whether "the user is scrolled near the bottom of the chat pane" — a pure UI/scroll concern — yet it lives inside useSessionStream. The only reason for this is that addItem resets it when the user sends a message (pinnedToBottomRef.current = true). This creates an implicit bidirectional dependency: the hook owns a ref that exists for ChatPane's scroll behavior, and ChatPane receives it back through the SessionStream interface. A cleaner separation would be to have addItem accept an optional onSend side-effect callback, or to keep the ref entirely in ChatPane and pass the "re-pin" trigger as a callback.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment thread frontend/src/lib/useSessionStream.ts
Comment on lines +122 to +130
<button
onClick={() => setShowAttachMenu(!showAttachMenu)}
className={`p-1.5 rounded-xl transition-colors shrink-0 ${
showAttachMenu
? 'bg-white/[0.1] text-white'
: 'hover:bg-white/[0.08] text-gray-400 hover:text-gray-300'
}`}
title="Attach files or data"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing type="button" on the attach-menu toggle. While this button is outside a <form> element so it won't accidentally submit anything today, omitting the attribute is inconsistent with the equivalent button in ChatPane (which has type="button") and leaves the component fragile if a form wrapper is ever added.

Suggested change
<button
onClick={() => setShowAttachMenu(!showAttachMenu)}
className={`p-1.5 rounded-xl transition-colors shrink-0 ${
showAttachMenu
? 'bg-white/[0.1] text-white'
: 'hover:bg-white/[0.08] text-gray-400 hover:text-gray-300'
}`}
title="Attach files or data"
>
<button
type="button"
onClick={() => setShowAttachMenu(!showAttachMenu)}
className={`p-1.5 rounded-xl transition-colors shrink-0 ${
showAttachMenu
? 'bg-white/[0.1] text-white'
: 'hover:bg-white/[0.08] text-gray-400 hover:text-gray-300'
}`}
title="Attach files or data"
>

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

- ChatPane/WelcomeScreen: take activeSessionId/activeProjectId/
  experiments/isRunning as props instead of reading useApp(), per the
  frontend/AGENTS.md "props over context" pitfall — same values, same
  source (page.tsx already destructures them), no behavior change.
- WelcomeScreen: add type="button" to the attach-menu toggle, matching
  ChatPane's equivalent button.
- useSessionStream: document the referential-stability invariant on
  connectSSE's dependency array (all five deps audited stable:
  useCallback([]) / raw setState setter), so the session-load effect
  cannot be silently re-triggered by a context refactor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
@lucastononro

Copy link
Copy Markdown
Owner Author

Greptile review findings — resolution (commit b4aaeaf)

Fixed (3):

  1. ChatPane.tsx:81 / WelcomeScreen.tsx:85 — context coupling for session-specific data. Both components now take activeSessionId / activeProjectId / experiments / isRunning (ChatPane) and activeProjectId / experiments (WelcomeScreen) as props; useApp() removed from both. page.tsx already destructured all of these from the same context, so the values, source, and render cadence are identical — no behavior change, but the components are now reusable/testable without an AppContext wrapper, per the frontend/AGENTS.md pitfall. (The PR description's plumbing note has been updated to match.)

  2. useSessionStream.ts:1280 — connectSSE in the session-load effect deps. Addressed via the reviewer's suggested "useCallback audit" option: all five of connectSSE's deps were verified referentially stable — addItem (useCallback([])), openCanvas (page-level useCallback([]), per the options-contract doc), publish (SSEStreamContext useCallback([])), refreshExperiments (AppContext useCallback([])), setIsRunning (raw useState setter). The invariant is now documented in a comment on the dep array so a future context refactor can't break it silently. We deliberately did not move connectSSE into a ref: that exempts it from exhaustive-deps and trades the (now documented) stability invariant for a stale-closure hazard — the wrong trade for a behavior-preserving refactor. Also, one nuance: if the effect did re-trigger it would reload + re-hydrate the session from the server (a reconnect/flicker), not permanently wipe history.

  3. WelcomeScreen.tsx:130 — missing type="button" on the attach-menu toggle. Added, now consistent with ChatPane's toggle.

Dismissed (1):

  • useSessionStream.ts:51 — pinnedToBottomRef living in the SSE hook. Intentional, and neither suggested alternative works here: (a) the ref can't live in ChatPane because ChatPane unmounts whenever the welcome screen shows, and user sends can originate while it isn't mounted (the pending-message drain after experiment auto-create adds the user item from page.tsx); (b) the hook re-pins in two places, not one — addItem on user send and resetSessionState on session switch — so an onSend callback wouldn't cover it. Hoisting the ref to page.tsx and threading a re-pin callback into the hook would reproduce the same bidirectional coupling with more plumbing. The ownership rationale is documented at both ends (hook interface + ref declaration).

Verification (no runtime test harness in this repo — refactor verified per the PR test plan):

  • npx tsc --noEmit ✅ · npm run lint ✅ (0 warnings) · npm run build
  • Manual smoke tutorial: the PR-body Test plan already covers every surface these fixes touch — welcome screen send/attach/S3 (attach toggle, suggestion chips, mention autocomplete still receiving experiments/projectId), in-session chat streaming + stop/send toggle (isRunning), session switch reset, reload hydration.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant