feat(ui): boot context pill at session start#313
Conversation
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 7 issue(s) (5 warning).
frontend/src/pages/DesktopChatView.tsx
Functional feature with clean UI integration, but DesktopChatView is missing the new prop (boot context won't render on desktop), the contexgin dependency is undeclared, error handling is overly broad, and the existing test suites were not updated.
- 🟡 regressions: DesktopChatView does not extract or pass
bootContext(orsessionContext) to ChatArea. The mobile ChatView passes both props, but the desktop view omits them — boot context metadata will never render on desktop.[fixable]
server/chat.ts
Functional feature with clean UI integration, but DesktopChatView is missing the new prop (boot context won't render on desktop), the contexgin dependency is undeclared, error handling is overly broad, and the existing test suites were not updated.
- 🟡 unsafe_assumptions (L726):
contexginis dynamically imported but is not listed in any package.json dependencies. If the package is not installed, every session start will hit the.catch()path and emit a misleadinglocal-fallbackwith all-zero counts, silently masking a missing dependency rather than signaling a configuration problem.[fixable] - 🟡 unsafe_assumptions (L728): The
compiledobject's shape is assumed without validation —compiled.sources,compiled.bootTokens, andcompiled.trimmedare accessed directly. If thecontexginAPI changes or returns an unexpected shape, undefined values will be sent over the wire and silently coerced by the client parser'sascasts.[fixable] - 🔵 bugs (L738): The
.catch()handler swallows all errors (including runtime errors in the.then()callback likecompiled.sources.mapfailing on a non-array) and emits alocal-fallbackevent. Consider narrowing the catch to only handle the dynamic import failure, or at minimum logging the error so real bugs in the success path aren't silently eaten.[fixable]
packages/client/src/protocol-parser.ts
Functional feature with clean UI integration, but DesktopChatView is missing the new prop (boot context won't render on desktop), the contexgin dependency is undeclared, error handling is overly broad, and the existing test suites were not updated.
- 🟡 unsafe_assumptions (L197): All fields are coerced via
astype assertions (msg.source as 'contexgin' | 'local-fallback',msg.sourceCount as number, etc.) with no runtime validation. If any field is missing or has the wrong type, the assertion passes silently and the UI getsundefined/NaNvalues. Other message types in this file use the same pattern, but new code should ideally validate shape.[fixable]
packages/client/src/slices/messages.ts
Functional feature with clean UI integration, but DesktopChatView is missing the new prop (boot context won't render on desktop), the contexgin dependency is undeclared, error handling is overly broad, and the existing test suites were not updated.
- 🟡 missing_tests (L390): Existing test files
packages/client/__tests__/messages-slice.test.tsandpackages/client/__tests__/protocol-parser.test.tscover the messages reducer and protocol parser respectively, but neither has been updated with tests for the newSET_BOOT_CONTEXTaction or theboot_contextparser case.[fixable]
frontend/src/components/BootContextPill.tsx
Functional feature with clean UI integration, but DesktopChatView is missing the new prop (boot context won't render on desktop), the contexgin dependency is undeclared, error handling is overly broad, and the existing test suites were not updated.
- 🔵 style (L12): Hardcoded color strings
'#4ade80'and'#fbbf24'bypass the project's CSS variable system (the rest of the codebase usesvar(--*)tokens). The trimmed-count yellow in global.css (line 2362) also hardcodes#fbbf24. Consider using CSS variables for consistency and dark/light theme support.[fixable]
|
|
||
| // Fire-and-forget: capture prompt comparison for the experiments spoke | ||
| // Fire-and-forget: emit boot context metadata to client + capture prompt comparison | ||
| import('contexgin') |
There was a problem hiding this comment.
🟡 unsafe_assumptions: contexgin is dynamically imported but is not listed in any package.json dependencies. If the package is not installed, every session start will hit the .catch() path and emit a misleading local-fallback with all-zero counts, silently masking a missing dependency rather than signaling a configuration problem. [fixable]
| // Fire-and-forget: emit boot context metadata to client + capture prompt comparison | ||
| import('contexgin') | ||
| .then(({ compile }) => compile({ workspaceRoot: cwd, tokenBudget: 8000 })) | ||
| .then((compiled) => { |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The compiled object's shape is assumed without validation — compiled.sources, compiled.bootTokens, and compiled.trimmed are accessed directly. If the contexgin API changes or returns an unexpected shape, undefined values will be sent over the wire and silently coerced by the client parser's as casts. [fixable]
| sources: compiled.sources.map((s: { relativePath: string }) => s.relativePath), | ||
| }); | ||
| }) | ||
| .catch(() => { |
There was a problem hiding this comment.
🔵 bugs: The .catch() handler swallows all errors (including runtime errors in the .then() callback like compiled.sources.map failing on a non-array) and emits a local-fallback event. Consider narrowing the catch to only handle the dynamic import failure, or at minimum logging the error so real bugs in the success path aren't silently eaten. [fixable]
| }); | ||
| break; | ||
|
|
||
| case 'boot_context': |
There was a problem hiding this comment.
🟡 unsafe_assumptions: All fields are coerced via as type assertions (msg.source as 'contexgin' | 'local-fallback', msg.sourceCount as number, etc.) with no runtime validation. If any field is missing or has the wrong type, the assertion passes silently and the UI gets undefined/NaN values. Other message types in this file use the same pattern, but new code should ideally validate shape. [fixable]
| case 'SET_SESSION_CONTEXT': | ||
| return { ...state, sessionContext: action.context }; | ||
|
|
||
| case 'SET_BOOT_CONTEXT': |
There was a problem hiding this comment.
🟡 missing_tests: Existing test files packages/client/__tests__/messages-slice.test.ts and packages/client/__tests__/protocol-parser.test.ts cover the messages reducer and protocol parser respectively, but neither has been updated with tests for the new SET_BOOT_CONTEXT action or the boot_context parser case. [fixable]
| const [expanded, setExpanded] = useState(false); | ||
|
|
||
| const isContexgin = context.source === 'contexgin'; | ||
| const dotColor = isContexgin ? '#4ade80' : '#fbbf24'; |
There was a problem hiding this comment.
🔵 style: Hardcoded color strings '#4ade80' and '#fbbf24' bypass the project's CSS variable system (the rest of the codebase uses var(--*) tokens). The trimmed-count yellow in global.css (line 2362) also hardcodes #fbbf24. Consider using CSS variables for consistency and dark/light theme support. [fixable]
…tests - Wire bootContext + sessionContext to DesktopChatView (was missing) - Server: async/await with try/catch, validate compiled shape, log errors - Protocol parser: runtime validation instead of bare `as` casts - CSS: define --status-ok/--status-warn vars, use class modifiers - Add 6 tests: SET_BOOT_CONTEXT reducer (3) + boot_context parser (3) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (2 warning).
frontend/src/components/ChatArea.tsx
Clean feature addition with solid test coverage on the state/protocol layers. Main concern is a minor UI bug where the empty-state placeholder shows alongside the boot context pill, plus a defensive-coding gap around the sources array type assertion.
- 🟡 bugs (L158): Empty-state condition checks
!sessionContextbut not!bootContext. When boot context is present (the pill is rendered) but there is no session context, the "Send a message to start" placeholder still shows beneath the pill. Should include&& !bootContextto suppress the empty state when the pill is visible.[fixable]
packages/client/src/protocol-parser.ts
Clean feature addition with solid test coverage on the state/protocol layers. Main concern is a minor UI bug where the empty-state placeholder shows alongside the boot context pill, plus a defensive-coding gap around the sources array type assertion.
- 🟡 unsafe_assumptions (L200):
msg.sources as string[]is a type assertion without element-level validation. If the server sends non-string elements in the array (e.g. objects, numbers), they pass through unchecked and will render as[object Object]in the pill. Consider(msg.sources as unknown[]).filter((s): s is string => typeof s === 'string')or.map(String).[fixable]
frontend/src/components/BootContextPill.tsx
Clean feature addition with solid test coverage on the state/protocol layers. Main concern is a minor UI bug where the empty-state placeholder shows alongside the boot context pill, plus a defensive-coding gap around the sources array type assertion.
- 🔵 bugs (L35):
key={src}uses the source string as the React key. Ifcontexginever returns duplicaterelativePathentries (e.g. from overlapping config), React will warn about duplicate keys. A safe alternative iskey={idx}orkey={${idx}-${src}}since this list is static per render.[fixable]
server/chat.ts
Clean feature addition with solid test coverage on the state/protocol layers. Main concern is a minor UI bug where the empty-state placeholder shows alongside the boot context pill, plus a defensive-coding gap around the sources array type assertion.
- 🔵 style (L738):
sources.map((s: { relativePath: string }) => s.relativePath)— the inline type annotation forsis effectively unchecked at runtime. If a source element lacksrelativePath, this producesundefinedin the array, which reaches the client as a stringified"undefined". Consider defensive access:s?.relativePath ?? s?.path ?? '<unknown>'.[fixable]
packages/client/__tests__/protocol-parser.test.ts
Clean feature addition with solid test coverage on the state/protocol layers. Main concern is a minor UI bug where the empty-state placeholder shows alongside the boot context pill, plus a defensive-coding gap around the sources array type assertion.
- 🔵 missing_tests (L745): Tests cover missing numeric fields and unknown source, but no test verifies behavior when
sourcescontains non-string elements (e.g.[42, null, {path: 'x'}]). This is the edge theas string[]assertion skips.[fixable]
| {bootContext && <BootContextPill context={bootContext} />} | ||
| {sessionContext && <ContextBlock content={sessionContext} />} | ||
|
|
||
| {messages.length === 0 && !current && !running && !sessionContext && ( |
There was a problem hiding this comment.
🟡 bugs: Empty-state condition checks !sessionContext but not !bootContext. When boot context is present (the pill is rendered) but there is no session context, the "Send a message to start" placeholder still shows beneath the pill. Should include && !bootContext to suppress the empty state when the pill is visible. [fixable]
| case 'boot_context': { | ||
| const source = msg.source === 'contexgin' ? 'contexgin' : 'local-fallback'; | ||
| const sourceCount = typeof msg.sourceCount === 'number' ? msg.sourceCount : 0; | ||
| const tokenCount = typeof msg.tokenCount === 'number' ? msg.tokenCount : 0; |
There was a problem hiding this comment.
🟡 unsafe_assumptions: msg.sources as string[] is a type assertion without element-level validation. If the server sends non-string elements in the array (e.g. objects, numbers), they pass through unchecked and will render as [object Object] in the pill. Consider (msg.sources as unknown[]).filter((s): s is string => typeof s === 'string') or .map(String). [fixable]
| {expanded && ( | ||
| <div className="boot-context-pill-content"> | ||
| {context.sources.map((src) => ( | ||
| <div key={src} className="boot-context-pill-source"> |
There was a problem hiding this comment.
🔵 bugs: key={src} uses the source string as the React key. If contexgin ever returns duplicate relativePath entries (e.g. from overlapping config), React will warn about duplicate keys. A safe alternative is key={idx} or key={${idx}-${src}} since this list is static per render. [fixable]
| sourceCount: sources.length, | ||
| tokenCount: typeof compiled?.bootTokens === 'number' ? compiled.bootTokens : 0, | ||
| trimmedCount: trimmed.length, | ||
| sources: sources.map((s: { relativePath: string }) => s.relativePath), |
There was a problem hiding this comment.
🔵 style: sources.map((s: { relativePath: string }) => s.relativePath) — the inline type annotation for s is effectively unchecked at runtime. If a source element lacks relativePath, this produces undefined in the array, which reaches the client as a stringified "undefined". Consider defensive access: s?.relativePath ?? s?.path ?? '<unknown>'. [fixable]
| sourceCount: 0, | ||
| tokenCount: 0, | ||
| trimmedCount: 0, | ||
| sources: [], |
There was a problem hiding this comment.
🔵 missing_tests: Tests cover missing numeric fields and unknown source, but no test verifies behavior when sources contains non-string elements (e.g. [42, null, {path: 'x'}]). This is the edge the as string[] assertion skips. [fixable]
…session start Threads ContexGin compile result from server through protocol to a tappable pill at the top of the chat. Shows source count, token count, and compilation engine (green dot for ContexGin, amber for local fallback). Expands to list source files and trimmed section count. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tests - Wire bootContext + sessionContext to DesktopChatView (was missing) - Server: async/await with try/catch, validate compiled shape, log errors - Protocol parser: runtime validation instead of bare `as` casts - CSS: define --status-ok/--status-warn vars, use class modifiers - Add 6 tests: SET_BOOT_CONTEXT reducer (3) + boot_context parser (3) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Separate import vs compilation try/catch in server boot context IIFE - Add runtime shape validation for contexgin compile() return value - Validate individual source entries before sending over WS - Filter non-string elements from sources array in protocol parser - Fix empty-state overlap when bootContext is present - Use index-based React keys for source list items - Add tests for sources validation edge cases (non-string, non-array) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9d08b5c to
db96539
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
compile()result from server through WebSocket protocol to a tappable pill at the top of the chat viewimport('contexgin')to avoid bloating module load for testsFiles changed:
server/chat.ts,packages/client/src/slices/messages.ts,packages/client/src/protocol-parser.ts,packages/client/src/index.ts,frontend/src/components/BootContextPill.tsx,frontend/src/components/ChatArea.tsx,frontend/src/pages/ChatView.tsx,frontend/src/styles/global.cssTest plan
npx vitest run server/__tests__/chat.test.tspasses (30/30)npx tsc --noEmitclean for modified files🤖 Generated with Claude Code