Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion docs/rendering/legacy-deletion-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,41 @@ Legend: **DELETE** = removed entirely · **GUT** = file stays, ownership logic
removed (becomes dumb/plumbing) · **ABSORB** = behavior re-encoded in the
ledger, then source deleted · **KEEP** = not ownership code, survives as-is.

## Cutover status — 2026-07 (the ledger is now the sole decision core)

The cutover shipped in two slices because a late discovery split it: the
**remote phone client** (`src/remote-client/`) is a SECOND `<Feed>` consumer
that was mounting the legacy `deriveFeedRenderModel` path. The manifest below
was written assuming one consumer, so "delete deriveFeedRenderModel" was
blocked until the phone also fed the ledger.

**DONE (this cutover PR):**
- The ledger producer (`useLedgerFeedItems`) is UNCONDITIONAL. The
`AGENT_CODE_RENDER_PIPELINE` flag + its whole probe are gone; so is
`AGENT_CODE_RENDER_SHADOW` and the `renderShadowEnabled`/
`renderPipelineEnabled` dev-debug plumbing.
- **The phone was migrated**: `remote/ui/SessionView.tsx` builds a minimal
SessionRuntime view (empty ghost plane — the phone has no optimistic echo)
and drives the SAME `useLedgerFeedItems`, so it now passes
`renderItemsOverride` like the desktop.
- `deriveFeedRenderModel` + its sort/visibility helpers + `FeedRenderModelInput`
**DELETED** from `renderModel.ts`. Feed's legacy branch **DELETED** — it maps
the ledger's items unconditionally.
- Shadow subsystem **DELETED**: `shadow/useRenderShadow.ts` and the
`shadowParity.test.ts` CI diff. `shadow/shadowDiff.ts` KEPT (pure diff util —
bundleCorpus/recordingCorpus still assert ledger output through it).

**STILL ALIVE — the block-level un-collapse slice (next PR):** the view bridge
still COLLAPSES the ledger's block-level semantic rows back into turn-level
items so `SemanticStreamingTurn` can render them. Killing that component (and
with it ghost rule 3, `selectMergedEntries` as Feed's entries source, and
`committedClaudeMessageTurnIds`) requires the ledger to own live turns
block-by-block through the shared registry dispatch — the hardest slice, and
the one that needs live visual verification the turn-grain corpus can't give.
Until then the completeness grep below still returns `SemanticStreamingTurn` /
`selectMergedEntries` / `committedClaudeMessageTurnIds` hits BY DESIGN;
`deriveFeedRenderModel` / `buildSemanticRenderUnits` are already clean.

## Deleted at Stage 3 cutover (the big reviewed PR)

| file | phys LOC | fate | notes |
Expand Down Expand Up @@ -46,7 +81,12 @@ ledger, then source deleted · **KEEP** = not ownership code, survives as-is.

## Verification of completeness at cutover

The Stage 3 PR is not mergeable until:
FULL completeness (the final block-level un-collapse PR) is reached when:
`grep -rn "SemanticStreamingTurn\|selectMergedEntries\|deriveFeedRenderModel\|committedClaudeMessageTurnIds\|buildSemanticRenderUnits" src/` returns only
hits inside `src/renderer/src/rendering/` history comments — i.e. every entry
in the DELETE/ABSORB tables above is gone or gutted, with its fixture green.

As of the 2026-07 cutover (see status section up top) `deriveFeedRenderModel`
and `buildSemanticRenderUnits` are already clean; the remaining three tokens
(`SemanticStreamingTurn`, `selectMergedEntries`, `committedClaudeMessageTurnIds`)
stay until the block-level un-collapse slice retires `SemanticStreamingTurn`.
14 changes: 0 additions & 14 deletions src/main/ipc/devDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,6 @@ import type { SessionRecorderManager } from '@main/recording/SessionRecorderMana

export type DevDebugConfig = {
enabled: boolean
/** AGENT_CODE_RENDER_SHADOW=1 — Stage 2 of the rendering rewrite: run
* the new ownership-ledger pipeline beside the legacy renderer and
* record divergences (src/renderer/src/rendering/shadow/). Rides the
* dev-debug config channel instead of minting its own because this
* flag is exactly as temporary as the shadow itself — both are
* DELETE-fated at Stage 3 cutover. */
renderShadowEnabled: boolean
/** AGENT_CODE_RENDER_PIPELINE=1 — Stage 3 of the rendering rewrite:
* Feed paints from the ownership-ledger pipeline's view bridge instead
* of deriveFeedRenderModel. Same temporary-channel rationale as the
* shadow flag above; the flag dies when the pipeline becomes default. */
renderPipelineEnabled: boolean
/** AGENT_CODE_SESSION_RECORD=1 (AND dev-debug on) — continuously record
* each session's rendering-input stream to session-recordings/<id>/,
* replayable in tests. plan: docs/rendering/session-recording-plan-2026-07.md,
Expand Down Expand Up @@ -68,9 +56,7 @@ export function registerDevDebugIpc(sessionRecorders: SessionRecorderManager | n
// runtime switch that works in Electron dev without requiring a
// Vite-prefixed renderer variable or rebuild-time config.
enabled: isDevDebugEnabled(),
renderShadowEnabled: envFlag('AGENT_CODE_RENDER_SHADOW'),
sessionRecordingEnabled: isSessionRecordingEnabled(),
renderPipelineEnabled: envFlag('AGENT_CODE_RENDER_PIPELINE'),
}
})

Expand Down
5 changes: 0 additions & 5 deletions src/preload/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,6 @@ export type {

export type DevDebugConfig = {
enabled: boolean
/** AGENT_CODE_RENDER_SHADOW=1 — rendering-rewrite Stage 2 shadow mode.
* Mirrors src/main/ipc/devDebug.ts (the source of truth). */
renderShadowEnabled: boolean
/** AGENT_CODE_RENDER_PIPELINE=1 — Stage 3 cutover flag. */
renderPipelineEnabled: boolean
/** AGENT_CODE_SESSION_RECORD=1 (+ dev-debug) — session recording active.
* Mirrors src/main/ipc/devDebug.ts (the source of truth). */
sessionRecordingEnabled: boolean
Expand Down
45 changes: 45 additions & 0 deletions src/remote-client/src/ui/SessionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from

import { Feed } from '@renderer/features/feed/ui/Feed'
import { SessionFeedProvider } from '@renderer/features/sessionFeed/SessionFeedContext'
import { useLedgerFeedItems } from '@renderer/rendering/view/useLedgerFeedItems'
import type { SessionRuntime } from '@renderer/workspace/workspaceState'
import type { Entry } from '@shared/types/transcript'
import {
conditionStateByKind,
type ClaudeAskUserQuestionState,
Expand All @@ -10,6 +13,13 @@ import {
import type { WebSocketSessionFeed, ConnectionState } from '../WebSocketSessionFeed'
import type { TranscriptStore } from '../transcript/store'

// The phone has no optimistic-echo plane: it renders committed + semantic
// state streamed from the desktop, never a locally-minted ghost (see the
// "deliberately skipped subsystems" note below). A single frozen empty map
// keeps the ledger's ghost plane cache stable across renders — a fresh map
// each render would defeat the adapter's by-reference plane memoization.
const NO_GHOSTS: ReadonlyMap<string, Entry> = new Map()

// One session, desktop-grade: this mounts the REAL desktop Feed component
// (see the alias table in ../vite.config.ts — the phone renders the same
// component tree the desktop does, with four documented stub
Expand Down Expand Up @@ -66,6 +76,40 @@ export function SessionView({

const provider = store.getKind(sessionId)

// Stage 3 cutover: the phone paints from the SAME ownership-ledger pipeline
// the desktop does. It used to rely on Feed's legacy deriveFeedRenderModel
// path (the only remaining consumer of it after the desktop flip); that
// path is deleted, so the phone must produce renderItemsOverride too. The
// ledger reads a minimal SessionRuntime view — the seven fields
// useLedgerFeedItems/ledgerFeedContextFromRuntime touch — assembled from
// the wire transcript. Only entries + semantic + phase differ across
// renders; ghosts is the frozen empty map (no optimistic plane on the
// phone) and lastJsonlEntryAt is irrelevant with no ghosts to invalidate.
const runtimeView = useMemo(
() =>
({
entries: transcript.entries,
semantic: {
currentTurn: transcript.semanticTurn,
history: transcript.semanticHistory,
},
ghosts: NO_GHOSTS,
streamPhase: transcript.phase.streamPhase,
streamPhasePendingToolName: transcript.phase.streamPhasePendingToolName,
streamPhasePendingToolUseId: transcript.phase.streamPhasePendingToolUseId,
lastJsonlEntryAt: 0,
}) as unknown as SessionRuntime,
[
transcript.entries,
transcript.semanticTurn,
transcript.semanticHistory,
transcript.phase.streamPhase,
transcript.phase.streamPhasePendingToolName,
transcript.phase.streamPhasePendingToolUseId,
],
)
const ledgerFeedItems = useLedgerFeedItems(runtimeView, provider, sessionId)

const askUserQuestionState = useMemo(
() =>
transcript.conditions
Expand Down Expand Up @@ -173,6 +217,7 @@ export function SessionView({
<Feed
sessionId={sessionId}
provider={provider}
renderItemsOverride={ledgerFeedItems}
entries={transcript.entries}
streamPhase={transcript.phase.streamPhase}
streamPhasePendingToolName={transcript.phase.streamPhasePendingToolName}
Expand Down
Loading