Motivation
The phone companion (src/remote-client) mounts the real desktop Feed, but it gets there through copies instead of shared code, and each copy has drifted. Fixes keep landing in only one copy, which is why the phone keeps looking broken after many fixes.
These divergences are observable on the phone:
- It renders raw
entries instead of selectMergedEntries, so the ghost fallback is missing.
- It skips provider
normalizeConditions and uses a separate condition dispatch.
lastJsonlEntryAt is hardcoded to 0, so the collapsed-running rule never fires (ARCHITECTURE §8.3).
- Main's remote tap has no semantic coalescing and no ordering barriers against JSONL, history-boundary or removal. The desktop forwarder has both.
- The phone never receives
transcript-diagnostic or provider-session-changed.
- A second sub-agent watcher polls the same directories as the first.
- History loading sits outside the
SessionFeed contract, with two separate paths.
SessionView.tsx hand-mirrors TileLeaf's Feed props, citing line numbers that are out of date.
- Six Vite aliases stub renderer modules, and the app-store stub is not type-checked.
There are also three rendering-modularity debts:
- Per-provider policy is hard-coded in the shared decide layer:
SUPPRESSION_POLICY, plus inline provider === ... checks.
- Provider rows import feed and workspace internals.
'claude' is used as a silent fallback for unknown providers.
A planned screenshot viewer, which runs live agents and captures their rendering, needs exactly the same seams.
Intended behavior
Each concern gets one implementation, with the transport or host injected:
- Main: one transport-neutral session feed tap, with window and remote sinks.
- Contract:
SessionFeed.loadHistory on the contract.
- Ingest: one pure session-ingest core used by both the desktop hub and the phone store. Desktop-only planes stay layered on top.
- Host: a typed
RendererHost context in place of the phone aliases and the stray window.api calls in the feed subtree.
- View: one
AgentFeedView, mounted by both TileLeaf and the phone.
- Provider policy: moves into the renderer registry.
- Imports: provider rows import feed primitives only through
features/feed/public, and the reverse import boundary is tested.
- Docs: ARCHITECTURE and the rendering docs are updated to match.
Plan: docs/plans/2026-09-24-phone-shared-rendering.md
Acceptance criteria
- The phone paints from the same merged entries, normalized conditions and
lastJsonlEntryAt as the desktop.
- The phone receives the same ordering barriers and the two missing channels.
- The remote-client Vite config has no component or store aliases, and its bundle contains no Monaco.
- A test replays one recording through both clients' ingest and gets the same ledger items.
- No behavior change on the desktop: the existing suites pass, and
npx tsc -b is clean.
Motivation
The phone companion (
src/remote-client) mounts the real desktopFeed, but it gets there through copies instead of shared code, and each copy has drifted. Fixes keep landing in only one copy, which is why the phone keeps looking broken after many fixes.These divergences are observable on the phone:
entriesinstead ofselectMergedEntries, so the ghost fallback is missing.normalizeConditionsand uses a separate condition dispatch.lastJsonlEntryAtis hardcoded to0, so the collapsed-running rule never fires (ARCHITECTURE §8.3).transcript-diagnosticorprovider-session-changed.SessionFeedcontract, with two separate paths.SessionView.tsxhand-mirrors TileLeaf's Feed props, citing line numbers that are out of date.There are also three rendering-modularity debts:
SUPPRESSION_POLICY, plus inlineprovider === ...checks.'claude'is used as a silent fallback for unknown providers.A planned screenshot viewer, which runs live agents and captures their rendering, needs exactly the same seams.
Intended behavior
Each concern gets one implementation, with the transport or host injected:
SessionFeed.loadHistoryon the contract.RendererHostcontext in place of the phone aliases and the straywindow.apicalls in the feed subtree.AgentFeedView, mounted by both TileLeaf and the phone.features/feed/public, and the reverse import boundary is tested.Plan:
docs/plans/2026-09-24-phone-shared-rendering.mdAcceptance criteria
lastJsonlEntryAtas the desktop.npx tsc -bis clean.