refactor(frontend): split the 4345-line page.tsx monolith - #166
refactor(frontend): split the 4345-line page.tsx monolith#166lucastononro wants to merge 2 commits into
Conversation
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
| 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 |
There was a problem hiding this comment.
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!
| <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" | ||
| > |
There was a problem hiding this comment.
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.
| <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!
- 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
Greptile review findings — resolution (commit b4aaeaf)Fixed (3):
Dismissed (1):
Verification (no runtime test harness in this repo — refactor verified per the PR test plan):
🤖 Generated with Claude Code |
Summary
Breaks up
frontend/src/app/page.tsx(4345 lines) into auseSessionStreamhook plus focused view components — a pure structural move with no behavior change.page.tsxdrops 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'sSSEStreamContextpublish fan-out, memoizedChatItemView, 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.src/lib/useSessionStream.tsuseSessionStream(experimentId, sessionId, {openCanvas, collapseCanvas, onReset})hook: the single per-sessionEventSource(connectSSE), the ~650-lineonmessagereducer (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, plusstreamingItemIdRef/pinnedToBottomRef.src/lib/chatItems.tsChatItem/ChatItemMetainterfaces +metaStr/metaNumruntime readers (L117–206).src/components/chat/ChatPane.tsxsrc/components/chat/WelcomeScreen.tsxSUGGESTIONSchips +handleSuggestion(L212–229, L2011–2153).src/components/chat/ChatItemView.tsxChatItemView+CHAT_MARKDOWN_PLUGINS+renderGroupedChatItems/isToolItem(L4018–4345).src/components/chat/ToolGroupCard.tsxToolGroupCard(L3506–3565).src/components/chat/CollapsibleToolCard.tsxCollapsibleToolCard+FUN_VERBS/PAST_VERBS/useFunVerb(L3454–3660).src/components/chat/SubAgentCard.tsxSubAgentCard(L3727–3822).src/components/chat/ClarificationCard.tsxClarificationCard(L3824–3932).src/components/chat/AgentToolCard.tsxAgentToolCard+AGENT_TOOL_META+_agentLabel(L3934–4016).src/components/chat/UserMessageFilePills.tsxUserMessageFilePills+FILE_PILL_PREVIEW(L4060–4098).src/components/chat/AttachedFilesPreview.tsxAttachedFilesPreview+humanBytes+ pill/hint constants (L4100–4203).src/components/chat/agentMeta.tsAGENT_META/AGENT_COLORS(L3666–3725).src/components/workspace/WorkspaceCanvas.tsxWorkspaceSidebar, 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.tsxMetricsTab).src/components/workspace/FileViewer.tsxFileViewer+ theSyntaxHighlighterlanguage registration (L99–115, L2617–2776).src/components/workspace/HtmlPanel.tsxHtmlPanel+humanArtifactBytes(L2519–2615).src/components/workspace/ReportMarkdown.tsxReportMarkdown(L2778–2822).src/components/workspace/FileTreeRow.tsxFileTreeRow(L2442–2517).src/components/workspace/fileIcons.tsgetFileIconInfo+DIR_LABELS/DIR_COLORS(L2413–2440).Plumbing notes (only deltas from a verbatim move):
openCanvas/collapseCanvascallbacks (panel ref stays in the page) and anonResetcallback soresetSessionStatecan still clear the page-owned input draft.attachMenuRef, file/folder inputs) stay in the page and are passed to bothWelcomeScreenandChatPane(only one mounts at a time), so the outside-click close handler is unchanged.TaskEventData,stripSessionPrefix) and the unusedsidebarOpen/setSidebarOpendestructure.ChatPane/WelcomeScreentakeisRunning/activeProjectId/experiments/activeSessionIdas props from the page (which reads them fromuseApp()), per the frontend/AGENTS.md "props over context" convention.Test plan
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).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.tsxmonolith into 20 focused files — auseSessionStreamhook, 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 theErrorBoundary key={activeSessionId}correctly resets workspace state on session switches.useSessionStream.tsowns the singleEventSource, the fullonmessagereducer, and all session-scoped state slices;resetSessionStatecorrectly clears every slice (includingtasks) on experiment switch.ChatPane/WelcomeScreennow receiveactiveSessionId,activeProjectId,experiments, andisRunningas props, removing theAppContextcoupling flagged in the prior round.WorkspaceCanvasaccepts ageneratedFilesprop that is destructured but never read; the component derives file navigation fromfileTreealone, 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
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%%{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| AReviews (2): Last reviewed commit: "fix(review): address Greptile findings o..." | Re-trigger Greptile