feat(home): unified session feed with message preview#409
Conversation
Replace separate Active Sessions + What's Next sections with a single SessionFeed component. Adds chip-based filtering (All / Needs me / In progress / Done), message preview capture from last assistant turn, and oldest-first sort within the working batch (48h window). Backend: preview column in EventStore, capture at turn-end in query-loop, in-memory cache in SessionOverviewEmitter, REST + SSE exposure. Frontend: useSessionFeed hook with working-batch logic, SessionFeed component with filter chips and 5-item scrollable viewport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 6 issue(s) (3 warning).
server/query-loop.ts
Solid refactor that simplifies the home feed, but the preview cache isn't wired up for real-time updates (query-loop doesn't call emitter.updatePreview), and the removal of Telos/ATB attention items from the home page is a notable behavioral regression that should be intentional.
- 🟡 bugs (L569): Preview is persisted to the EventStore but
SessionOverviewEmitter.updatePreview()is never called, so the in-memory preview cache stays stale. Live SSE broadcasts viagetCachedPreview()will miss the latest preview until the cache is opportunistically populated bygetCachedSpeaker()on the next tick. TheupdatePreview()method exists on the emitter but is unreachable from query-loop (no reference passed in). Preview will eventually appear after a page refresh or cache miss, but real-time updates will lag.[fixable] - 🔵 bugs (L563):
previewText.slice(-300)takes the last 300 characters. For a user-facing preview, taking the first 300 characters (.slice(0, 300)) would be more useful — the beginning of a response typically has the most context (greeting, summary, key decision). The trailing 300 chars might be mid-sentence code or boilerplate.[fixable]
frontend/src/pages/SessionList.tsx
Solid refactor that simplifies the home feed, but the preview cache isn't wired up for real-time updates (query-loop doesn't call emitter.updatePreview), and the removal of Telos/ATB attention items from the home page is a notable behavioral regression that should be intentional.
- 🟡 regressions (L312): Telos (todo) and ATB (task board) attention items are no longer surfaced on the home page. The deleted
useAttentionFeedaggregated items from three sources (telos, ATB, sessions); the replacementuseSessionFeedonly shows sessions. Starred/urgent todos and pending_review/blocked/failed tasks previously appeared as tier-1 attention items on the home screen and now only appear in the CommandCenter sidebar (visible only inside a chat session). This may be intentional but is a significant behavioral change worth calling out.
frontend/src/components/SessionFeed.tsx
Solid refactor that simplifies the home feed, but the preview cache isn't wired up for real-time updates (query-loop doesn't call emitter.updatePreview), and the removal of Telos/ATB attention items from the home page is a notable behavioral regression that should be intentional.
- 🟡 missing_tests: No component test for
SessionFeed.tsx. The deletedAttentionFeed.tsxandSessionOverview.tsxboth had comprehensive component tests (rendering, navigation, state transitions). The replacement has hook-level tests but no component-level tests for rendering, filter chip interaction, card tap navigation, empty state display, or thecounts.all === 0early return.[fixable]
packages/protocol/src/event-store.ts
Solid refactor that simplifies the home feed, but the preview cache isn't wired up for real-time updates (query-loop doesn't call emitter.updatePreview), and the removal of Telos/ATB attention items from the home page is a notable behavioral regression that should be intentional.
- 🔵 missing_tests (L589): No test coverage for the new
updateLastAssistantPreview()method, themigratePreviewColumn()migration, or thelastAssistantPreviewfield inrowToSession(). The existing EventStore test file has no assertions for preview persistence or retrieval.[fixable]
frontend/src/hooks/useSessionFeed.ts
Solid refactor that simplifies the home feed, but the preview cache isn't wired up for real-time updates (query-loop doesn't call emitter.updatePreview), and the removal of Telos/ATB attention items from the home page is a notable behavioral regression that should be intentional.
- 🔵 unsafe_assumptions (L96):
workingBatchusesDate.now()insideuseMemobut only recomputes whenactivitieschanges. If no new SSE events arrive, expired done sessions (past the 48h window) won't be evicted from the batch until the next activity update. This is minor — SSE events are frequent — but could show stale sessions on a quiet day.
| if (store && resolvedSessionId) { | ||
| store.recordUsage(resolvedSessionId, usageData); | ||
| if (lastPreview) { | ||
| store.updateLastAssistantPreview(resolvedSessionId, lastPreview); |
There was a problem hiding this comment.
🟡 bugs: Preview is persisted to the EventStore but SessionOverviewEmitter.updatePreview() is never called, so the in-memory preview cache stays stale. Live SSE broadcasts via getCachedPreview() will miss the latest preview until the cache is opportunistically populated by getCachedSpeaker() on the next tick. The updatePreview() method exists on the emitter but is unreachable from query-loop (no reference passed in). Preview will eventually appear after a page refresh or cache miss, but real-time updates will lag. [fixable]
| .map((b) => b.content) | ||
| .join('\n') | ||
| .trim(); | ||
| const lastPreview = previewText.length > 300 ? previewText.slice(-300) : previewText; |
There was a problem hiding this comment.
🔵 bugs: previewText.slice(-300) takes the last 300 characters. For a user-facing preview, taking the first 300 characters (.slice(0, 300)) would be more useful — the beginning of a response typically has the most context (greeting, summary, key decision). The trailing 300 chars might be mid-sentence code or boilerplate. [fixable]
| @@ -311,9 +310,7 @@ export function SessionList() { | |||
|
|
|||
| <BriefingCard /> | |||
|
|
|||
There was a problem hiding this comment.
🟡 regressions: Telos (todo) and ATB (task board) attention items are no longer surfaced on the home page. The deleted useAttentionFeed aggregated items from three sources (telos, ATB, sessions); the replacement useSessionFeed only shows sessions. Starred/urgent todos and pending_review/blocked/failed tasks previously appeared as tier-1 attention items on the home screen and now only appear in the CommandCenter sidebar (visible only inside a chat session). This may be intentional but is a significant behavioral change worth calling out.
| this.stmts.updateLastSpeaker.run(speaker, sessionId); | ||
| } | ||
|
|
||
| updateLastAssistantPreview(sessionId: string, preview: string): void { |
There was a problem hiding this comment.
🔵 missing_tests: No test coverage for the new updateLastAssistantPreview() method, the migratePreviewColumn() migration, or the lastAssistantPreview field in rowToSession(). The existing EventStore test file has no assertions for preview persistence or retrieval. [fixable]
| }, []); | ||
|
|
||
| // Working batch: all sessions in the user's current active set | ||
| const workingBatch = useMemo(() => { |
There was a problem hiding this comment.
🔵 unsafe_assumptions: workingBatch uses Date.now() inside useMemo but only recomputes when activities changes. If no new SSE events arrive, expired done sessions (past the 48h window) won't be evicted from the batch until the next activity update. This is minor — SSE events are frequent — but could show stale sessions on a quiet day.
- Move preview DB write before forceFlush so turn_end handler can read it and populate the in-memory cache for real-time SSE - Wire overviewEmitter.updatePreview() in the turn_end handler - Add EventStore preview round-trip tests (3 tests) - Add SessionFeed component tests (8 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review responseFixed in 7264748:
Design decisions (intentional, no change):
|
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 7 issue(s) (2 warning).
server/session-overview.ts
Solid replacement of two overlapping home-screen sections with a cleaner unified feed. Main concerns: previewCache not cleared in destroy(), loss of Telos/ATB attention items with no obvious replacement, and paused-session visibility change.
- 🟡 bugs: The new
previewCacheMap is not cleared indestroy(). The diff addspreviewCache(line 82 in diff) and cleans it inforget()(line 140), butdestroy()(lines 457-473, unchanged) only clearslastEventTimes,uncommittedCache, andspeakerCache. Addthis.previewCache.clear()alongside the other cache clears indestroy().[fixable]
frontend/src/hooks/useSessionFeed.ts
Solid replacement of two overlapping home-screen sections with a cleaner unified feed. Main concerns: previewCache not cleared in destroy(), loss of Telos/ATB attention items with no obvious replacement, and paused-session visibility change.
- 🟡 regressions: The old
AttentionFeedaggregated items from three sources — Telos todos, ATB tasks, and sessions — with tiered priority sorting. The newSessionFeedonly shows sessions. Telos starred/urgent items and ATBpending_review/blocked/failedtasks no longer surface on the home screen. If this is intentional, consider whether those signals need a new home or if removing them is the desired UX change. - 🔵 regressions (L33): The old
SessionOverviewdisplayedpausedsessions (it only excludedidleandinit). The newisInWorkingBatch()excludespausedsessions — they won't appear in any filter. TheSTATE_CONFIGinSessionFeed.tsxdefines apausedentry that can never be reached. If paused sessions should be visible, adda.state === 'paused'toisInWorkingBatch().[fixable] - 🔵 unsafe_assumptions (L86): The SSE callback casts
data as SessionActivity[]without runtime validation. The olduseAttentionFeedfiltered withtypeof d.sessionId === 'string' && typeof d.state === 'string'. While this matches the pattern used byuseSessionOverviewanduseSessionList, the lack of validation means a malformed SSE payload could propagate undefined fields into the filter/sort logic. Low risk since the server controls the data, but worth noting.[fixable]
frontend/src/components/SessionFeed.tsx
Solid replacement of two overlapping home-screen sections with a cleaner unified feed. Main concerns: previewCache not cleared in destroy(), loss of Telos/ATB attention items with no obvious replacement, and paused-session visibility change.
- 🔵 regressions (L63): The old
SessionOverviewandAttentionFeedcalledselectionChanged()(haptic feedback) on card tap. The newSessionFeed.handleTapnavigates without haptics. If this is a mobile-first app, users may notice the missing tactile feedback on session card taps.[fixable] - 🔵 style (L13): Dead config entry:
STATE_CONFIGincludespausedandinitentries, butisInWorkingBatch()in the hook excludes both states from ever reaching the feed. These entries can never be rendered. Consider removing them or adding a comment explaining they're kept for completeness.[fixable]
frontend/src/hooks/__tests__/useSessionFeed.test.ts
Solid replacement of two overlapping home-screen sections with a cleaner unified feed. Main concerns: previewCache not cleared in destroy(), loss of Telos/ATB attention items with no obvious replacement, and paused-session visibility change.
- 🔵 missing_tests: No test covers
pausedsession exclusion from the working batch. The test at line 62 verifiesidleandinitare excluded but doesn't assert thatpausedis also filtered out. Add apausedentry to that test's activity list to prevent accidental inclusion in the future.[fixable]
| if (a.state === 'working' || a.state === 'waiting') return true; | ||
| // Awaiting reply or uncommitted work — always relevant | ||
| if (a.awaitingReply || a.uncommittedWork) return true; | ||
| // Done sessions within the time window |
There was a problem hiding this comment.
🔵 regressions: The old SessionOverview displayed paused sessions (it only excluded idle and init). The new isInWorkingBatch() excludes paused sessions — they won't appear in any filter. The STATE_CONFIG in SessionFeed.tsx defines a paused entry that can never be reached. If paused sessions should be visible, add a.state === 'paused' to isInWorkingBatch(). [fixable]
|
|
||
| useEffect(() => { | ||
| const unsubActivity = eventBus.on('session_activity', (data) => { | ||
| setActivities(data as SessionActivity[]); |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The SSE callback casts data as SessionActivity[] without runtime validation. The old useAttentionFeed filtered with typeof d.sessionId === 'string' && typeof d.state === 'string'. While this matches the pattern used by useSessionOverview and useSessionList, the lack of validation means a malformed SSE payload could propagate undefined fields into the filter/sort logic. Low risk since the server controls the data, but worth noting. [fixable]
| <button | ||
| className="feed-card" | ||
| onClick={() => onTap(activity.sessionId)} | ||
| style={{ '--card-accent': iconColor } as React.CSSProperties} |
There was a problem hiding this comment.
🔵 regressions: The old SessionOverview and AttentionFeed called selectionChanged() (haptic feedback) on card tap. The new SessionFeed.handleTap navigates without haptics. If this is a mobile-first app, users may notice the missing tactile feedback on session card taps. [fixable]
| waiting: { icon: '⚠', color: '#ff6d6d', label: 'waiting' }, | ||
| done: { icon: '✓', color: '#4ade80', label: 'done' }, | ||
| idle: { icon: '○', color: 'var(--text-dim)', label: 'idle' }, | ||
| init: { icon: '○', color: 'var(--text-dim)', label: 'starting' }, |
There was a problem hiding this comment.
🔵 style: Dead config entry: STATE_CONFIG includes paused and init entries, but isInWorkingBatch() in the hook excludes both states from ever reaching the feed. These entries can never be rendered. Consider removing them or adding a comment explaining they're kept for completeness. [fixable]
Summary
Changes
Backend:
event-store.ts:last_assistant_previewcolumn + migration + prepared statementquery-loop.ts: extract preview from snapshot blocks at turn-endsession-overview.ts: preview cache (avoids per-broadcast DB lookups), expose in activity outputapp.ts: include preview in REST/api/sessionsresponseFrontend:
useSessionFeed.ts(new): working-batch filter, chip filters, oldest-first sort, localStorage persistenceSessionFeed.tsx(new): filter chips + feed cards with status icon, repo tag, title, preview, metaSessionList.tsx: swap old components for unified<SessionFeed />global.css: remove old overview/attention CSS (~210 lines), add feed stylesRemoved:
SessionOverview.tsx,AttentionFeed.tsx,useAttentionFeed.ts+ their testsTest plan
useSessionFeedhook (working batch, filters, sort, persistence)🤖 Generated with Claude Code