feat(components): wire ConversationView into the session store behind a flag (phase 1b-A) - #363
Open
zxch3n wants to merge 4 commits into
Open
Conversation
…s and a Mirror-free history writer (phase 1a) Opening a long conversation today materializes every history container through the session-doc Mirror (357–521 ms on a 171-turn doc with 70k containers). This adds the read/write primitives that let the renderer stop depending on that: - `sessionControlDocSchema`: the session schema with `history` as an ignored root, so the control-plane Mirror (session, mq, preview, fork, runtime config) never touches the history list. Pinned by a 2,000-turn test. - `createConversationViewFromDoc`: an O(window) read model over today's Loro APIs — turn ids and index rows from shallow values, ranges hydrated via per-turn `toJSON()`, LRU eviction that never drops the tail, subscribed or just-requested ranges, and doc-event tracking of appends, in-place updates and deletes. `readAll()` remains for deliberate full-transcript consumers. - `HistoryWriter`: `appendHistoryEntry` / `replaceHistoryEntry` / `respondHistoryPermission` write straight into the doc with the exact container shapes a full-schema `Mirror.setState` produces (restated loro-mirror inference rules), proven by a getDeepValueWithID shape round-trip against the Mirror path. Wiring into `createSessionStore`, the renderer's range-scoped rows and the remaining full-history readers follow in phase 1b. Model: claude-fable-5-1
… a flag (phase 1b-A) `createSessionStore` now composes its state through `providers/session-doc-state-source.ts`. With `isConversationViewEnabled()` (on unless `VITE_LODY_CONVERSATION_VIEW=0` or `localStorage['lody:conversationView']='0'`) the Mirror is built with `sessionControlDocSchema`, the store exposes `conversationView`, and `getState().history` becomes a lazy bridge over `conversationView.readAll()` memoized per (control root, view version) so `prev === next` identity checks keep working. Off is the untouched full-Mirror path, kept as rollback. - `createSessionControlMirror`: loro-mirror 2.3.1 honors an `Ignore` root only in its initial snapshot; its incremental event path applied history deltas into a partial `history` array and registered every new container. The control Mirror filters history events out of the two per-batch methods, pinned by a test that appends, replaces and streams text into history. - `setState` reaching `history` on the view path throws `SessionHistoryWriteThroughMirrorError` (object patch, mutative draft and returned-object shapes) instead of persisting nothing. - Every history write goes through the history writer when the store has a view: `startSession`, `appendSessionTurn`, `appendSessionHistory`, `updateSessionHistory` (indexOf hint), `respondSessionPermission` (hydrated tail hint), and the steer-promotion status flip via the new `patchHistoryEntry`, which sets scalars without recreating item containers. - `ConversationView` gains `retain()`, `planCount`, per-turn `fileDiff()` and an incremental `readAll()` that re-materializes only changed turns and keeps every other entry identity-stable, so identity-keyed render caches keep hitting through the bridge. - Readers converted to index/tail reads: `use-session-doc` (revision compare, `updateHistoryEntry`, `useSessionDocSyncState`), `use-session-actions` (dispatch/steer user-turn reads), `use-task-actions`, `use-remove-local-project`, `managed-preview-surface`, the fork-origin observer in `session-detail`, `use-session-diff-summary` (per-turn `fileDiff` containers), `ai-gui/index.tsx` `lastUserMessageId`, and the active-assistant-turn / scroll-to-message reads in `session-chat-interface`. The rest of `session-chat-interface` stays on the bridge. - `SessionDocument.getHistory()` on the CLI is documented as full materialization reserved for import hashing and dispatch scans. Model: claude-fable-5-1
…use-session-doc The Electron web tsconfig maps `@/lib`, `@/components` and `@/ui` into the components package but not `@/providers`, so the alias broke `typecheck:web`; hooks reach providers by relative path, as `atoms/runtime.ts` already does. Model: claude-fable-5-1
…stive default Type-aware lint (consistent-return) rejected the bare switch; the default is a `never` guard so a new ContainerKind fails to compile instead of returning undefined. Model: claude-fable-5-1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1b-A of the long-conversation work (stacks on #359 and carries the phase 1a foundation commit
17ab602).createSessionStorenow composes its state throughproviders/session-doc-state-source.ts:isConversationViewEnabled()next toisElectronLocalDataPlaneEnabled— on unlessVITE_LODY_CONVERSATION_VIEW=0orlocalStorage['lody:conversationView']='0'. Off is the untouched full-Mirror path, kept as rollback.sessionControlDocSchema,SessionDocStore.conversationViewexposes theConversationView, andgetState().historyis a lazy bridge overconversationView.readAll(), memoized per (control root, view version) soprev === nextidentity checks keep working.subscribefires on view changes too.use-session-doc.tscompares snapshots throughreadSessionDocHistoryRevisioninstead of touchinghistory.Ignoreroot only in its initial snapshot; its incremental event path applied history deltas into a partialhistoryarray and registered every new container.createSessionControlMirrorfilters history events out of the two per-batch methods; pinned by a test that appends, replaces and streams text into history with the control Mirror attached.setStatereachinghistoryon the view path throwsSessionHistoryWriteThroughMirrorError(object patch, mutative draft, returned object) instead of persisting nothing.startSession,appendSessionTurn,appendSessionHistory,updateSessionHistory(indexOf hint) andrespondSessionPermission(hydrated-tail hint) go throughlib/conversation-view/history-writerwhen the store has a view. The steer-promotion status flip inuse-session-actionsuses the newpatchHistoryEntry, which writes scalars without recreating item containers.retain(),planCount, per-turnfileDiff(), and an incrementalreadAll()that re-materializes only changed turns and keeps every other entry identity-stable (so identity-keyed render caches keep hitting through the bridge).SessionDocument.getHistory()documented as full materialization reserved for import hashing and dispatch scans. No behavior change.Converted readers (index/tail, no full hydrate)
use-session-doc(revision compare,updateHistoryEntry,useSessionDocSyncState),use-session-actions(dispatch/steer user-turn reads, promotion),use-task-actions.resolveTaskProposal,use-remove-local-project,managed-preview-surface, the fork-origin observer insession-detail,use-session-diff-summary(per-turnfileDiffcontainers),ai-gui/index.tsxlastUserMessageId, andsession-chat-interface's active-assistant-turn and scroll-to-message reads.Still on the bridge (
sessionDoc.history, one incremental full read on first access)session-chat-interface.tsx: conversation config / source fence / runtime config resolvers, capacity retry, the search block index (useIncrementalSearchBlocks), copy-as-markdown, latest Codex proposed plan, end-timing analytics, pin list, permission scans (scanPermissionRequests,FloatingPermissionRequest), activity-from-history, billable turn count;draft-session-chat-interface.tsxparent config;use-acp-session-config-selection. These are phase 1c.Tests
tests/session-doc-state-source.test.ts(bridge, guard, control Mirror filter),tests/conversation-view.test.ts(retain, planCount, readAll identity),tests/conversation-view-turn-selectors.test.ts,tests/workspace-writer.test.ts(writer routing produces the Mirror's container shapes, never callssetState).🤖 Generated with Claude Code
Benchmarks (from the stacked PR C,
pnpm --filter @lody/history-import bench:open)before = full-schema Mirror open (the path this stack replaces), after =
view.open(import +ConversationView+ tail hydrate → renderable rows). M-series laptop, means unless noted.unreachable)Acceptance:
open≤ 50 ms at ×10 — met on the synthetic ×10 fixture (46.2 ms); on the item-heavy real fixture (~100 items per turn) it is 90 ms at ×1 and 199 ms at ×10 (117 ms of which is decoding the 48.5 MiB snapshot), where the full-Mirror path fails outright, dominated by the 20-turn tail hydrate.streamp99 ≤ 4 ms — met everywhere.Rollback:
VITE_LODY_CONVERSATION_VIEW=0at build time orlocalStorage['lody:conversationView'] = '0'at runtime restores the full-Mirror path.