refactor(frontend): type the SSE event layer with a discriminated union - #157
refactor(frontend): type the SSE event layer with a discriminated union#157lucastononro wants to merge 5 commits into
Conversation
… union ChatItem.meta, generatedFiles/restoredFiles, the SSE handler's event.data, and MetricsTab's ChartTooltip were all any-typed, defeating the type system exactly where malformed backend payloads are most likely. - lib/types.ts: define SSEEvent as a discriminated union over every event.type the backend publishes, replacing the old unused/inaccurate per-event Data interfaces with ones matching what the handler actually reads (and defends against). - page.tsx connectSSE: narrow on event.type per case (each case gets its own block scope) instead of one blanket `event.data as any`; type ChatItem.meta as a flat, all-optional ChatItemMeta (a full discriminated union would require threading narrowed prop types through ~8 render components that read meta without first narrowing on item.type — out of scope for this change); type generatedFiles/restoredFiles as GeneratedFile[]; add metaStr/metaNum runtime-checked readers for Message.metadata on session-reload restore. - MetricsTab.tsx: give ChartTooltip a real props interface instead of any. Runtime behavior is unchanged — this is purely a typing refactor. Closes #101 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Greptile review follow-up (c8a1bba)Fixed — 1/1 findings:
Dismissed: none. Tests: the frontend has no test runner (no vitest/jest), and this PR is a compile-time typing change, so instead of bolting on a runner I added
Verified the assertions bite: re-mapping Manual smoke test (runtime path): start a session that emits charts (any run logging metrics with a chart config), open the Metrics tab, and confirm pinned panels appear when the Validation: |
Conflicts/semantic reconciliation: - MetricsTab.tsx: #157 typed ChartTooltip props (dropped `any`); staging's #161 compare page imports it — kept the typed props AND the export. - SSE union extended with events other staging PRs added/consume: `budget_exceeded` (#165, new BudgetExceededSSEData) and the `notebook.kernel.state`/`notebook.structure.changed`/`notebook.cell.*` events dispatched by useNotebookSSE (#148's typed bus requires them). StructureChangedEvent moved to lib/notebook/types.ts (re-exported from useNotebookSSE) so lib/types.ts can reference it. - page.tsx budget_exceeded case adapted to #157's per-case `const data = event.data` block pattern. - useNotebookSSE.test.tsx: cell.completed payload gained exec_count: null to satisfy the now-typed CellCompletedEvent.
|
Merged into integration branch |
Summary
ChatItem.metawasany,generatedFiles/restoredFileswereany[], the SSE handler castevent.data as anythen read dozens of untyped fields, andMetricsTab'sChartTooltipprops wereany— all in exactly the place most likely to see a malformed/unvalidated backend payload.Changes
src/lib/types.ts— definedSSEEventas a discriminated union covering everyevent.typethe backend publishes on/api/sessions/{id}/stream(state_change,agent_token/agent_message,tool_start/tool_end,code_output,agent_error,usage_event,report_ready,files_ready,file_created,agent_aborted,metrics_batch,metric,chart_config,log_event,canvas_html,subagent_start/subagent_end,clarification_request/clarification_resolved,agent_tool_call,clarification_exchange,notebook.created, the lineage events, and the task events). This replaces a set of unused, inaccurate*Datainterfaces that predated this refactor (never imported anywhere, and didn't match the fields the handler actually reads).src/app/page.tsxconnectSSE— narrows onevent.typepercase(each case now has its own{ }block so it can bind a correctly-typed localdata), instead of one blanketconst data = event.data as anyshared across the whole switch. No behavior change — every field read is identical, just typed instead of implicitany.ChatItem.metais nowChatItemMeta— a flat, all-optional interface with real field names instead ofany. (A precise per-ChatItem.typediscriminated union would be more rigorous, butmetaflows through ~8 render components —CollapsibleToolCard,SubAgentCard,ClarificationCard,AgentToolCard,ToolGroupCard— that read their own subset without first narrowing onitem.type; threading a strict per-variant type through all of them is a much larger, riskier change than this issue calls for. Noted as deferred below.)generatedFiles/restoredFilesare nowGeneratedFile[](reusing the existingGeneratedFiletype) instead ofany[].metaStr/metaNumruntime-checked readers forMessage.metadata(Record<string, unknown>, unvalidated persisted history) used when reconstructingChatItem.metaon session-reload restore, replacing blindascasts with an actual runtime guard.src/components/MetricsTab.tsx—ChartTooltipnow has a realChartTooltipPropsinterface instead ofany.Deferred
ChatItem(narrowingmeta's shape peritem.type) was scoped out — it would require changing the prop types of ~8 render components to accept only the narrowed member of the union, which is a much larger surface than this issue's ask and carries real regression risk for comparatively little benefit over the flatChatItemMetashape landed here.Test plan
Existing (must still work) — this is a pure typing refactor, no runtime logic changed, but the SSE handler is the most-exercised code path in the app:
metrics_batch, richlog_eventpanels (image grid/table/confusion matrix),chart_config.file_created/files_readyauto-opening the canvas, file tree updates.notebook.created+ theuseNotebookSSEshared-stream path from PR Stop opening two EventSource connections to the same stream #100).Message.metadata.experiment_created,dataset_registered, etc.) still refresh the lineage tab.task_created/task_updated/task_deleted).New
npx tsc --noEmitandnpm run lintboth pass clean.Closes #101
🤖 Generated with Claude Code
Greptile Summary
This PR replaces the untyped SSE event layer (
event.data as any+type: string) with a discriminated union covering every event type the backend publishes, and strengthens adjacentanyusages inpage.tsxandMetricsTab.tsx. The changes are a pure compile-time refactor — no runtime logic is altered.SSEEventdiscriminated union (types.ts): eachevent.typeliteral now maps to a dedicated payload interface; theconnectSSEswitch narrows per-case with a locally-scopedconst data = event.datainstead of a blanket cast.ChatItemMeta+metaStr/metaNum(page.tsx):meta: anyreplaced with a flat all-optional interface; session-restore path uses lightweight runtime guards instead of blindascasts on unvalidated persisted metadata.ChartTooltiptyped props (MetricsTab.tsx):anyprops replaced withChartTooltipProps;entry.valuenow guarded before callingsmartFormat.Confidence Score: 5/5
Pure compile-time refactor with no runtime logic changes; all existing SSE handler behavior is preserved exactly.
Every field read in the SSE handler was already present before; the PR only adds types around them. The compile-time test file verifies discriminated-union narrowing, and the PR description confirms tsc --noEmit passes clean. No new execution paths are introduced.
Files Needing Attention: frontend/src/lib/types.ts — two small consistency gaps: MetricsBatchData.items typed as required while the handler still defends with || [], and NotebookCreatedData replicates the existing NotebookCreatedEvent shape instead of reusing it.
Important Files Changed
Reviews (3): Last reviewed commit: "merge: staging-v0.0.5 into fix/101-typed..." | Re-trigger Greptile