Skip to content

Remote: the desktop feed on the phone (semantic rendering + history backfill) - #438

Merged
Juliusolsson05 merged 4 commits into
mainfrom
feat/remote-semantic-rendering
Jul 6, 2026
Merged

Juliusolsson05 merged 4 commits into
mainfrom
feat/remote-semantic-rendering

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

The rendering upgrade tracked in #420: the phone now renders sessions with the real desktop feed — markdown prose, streaming turns, tool cards, Edit/Write diffs, todo lists, subagent cards — instead of raw TUI text.

Architecture (docs/superpowers/specs/2026-07-06-remote-semantic-rendering-design.md)

  • Alias substitution, not reimplementation: the phone bundle imports the actual @renderer feed component tree; the Vite alias table in remote-client/vite.config.ts is the compatibility contract — exactly five documented stub swaps (static-hljs CodeBlock, frozen-settings store hook, no-op perf, plain markdown link/inline-code). Everything else renders with literally the desktop's code, styled by the desktop's own Tailwind theme. No monaco in the bundle (450 KB gzip total).
  • Shared reducers, zero drift: the client TranscriptStore drives the feed through the desktop's own foldSemanticEvent, provider transcript mappers, indexEntryIntoMaps, and a stream-phase machine extracted into a shared pure reducer (semantic/streamPhaseMachine.ts) that the desktop hook now consumes too — the fork class is structurally dead.
  • History backfill: new read-only get-history wire message (scope pinned in scope.test.ts both directions), served from a SessionManager transcript-file cache + path-based historyLoader variants (main can't reconstruct {cwd, providerSessionId} for live sessions; the file path rides every jsonl event). Chunks carry the file identity so the client detects transcript rolls (/clear) and discards stale serves.
  • Ingest discipline mirrors the desktop exactly: one seen-uuid set gating backfill+live, history prepends / live appends, chunk-scoped mappers for history vs a session-lifetime live mapper (codex cursor), marker gates on kept entries.

Review

High-effort multi-agent review over the branch confirmed 16 defects (codex cursor corruption, stale /clear serve, AUQ fallback loss, retry gaps, eviction, marker gates, the phase-machine fork…) — all fixed in 17ee6b5, each fix traced to its desktop counterpart in comments.

Verification

  • 265/265 tests, incl. a real-socket pipeline test (server → WS → mapper → dedupe → backfill-prepend → fold → phase) and the client↔server drift suite
  • tsc clean on both projects (remote-client type-checks against the REAL desktop modules); phone bundle builds
  • Needs a real-phone smoke pass (desktop feed rendering on device) before merge — reviewer note

Closes the rendering item in #420.

🤖 Generated with Claude Code

Juliusolsson05 and others added 4 commits July 6, 2026 23:08
…d via alias substitution

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The phone connects mid-session; live frames only cover what happens after.
New read-only get-history message (scope widened consciously, pinned both
ways in scope.test.ts): beforeMarker absent = newest-N initial chunk,
present = the page immediately before it, raw records in the live frames'
entry shape so the client runs ONE mapper path for backfill and live.

Main-side key resolution: SessionManager cannot reconstruct
{cwd, providerSessionId} for a live session, but the transcript FILE path
rides every jsonl-entry event — cached now (same lifetime as the snapshot
caches) with a getter, and historyLoader gains path-based variants
(the resolver entrypoints become thin wrappers; desktop behavior
unchanged). Also fixes vitest alias drift: opencode-headless was missing
from the deliberately-duplicated map, breaking any test whose import graph
reaches registry.main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…anscript store

The phone now mounts the REAL desktop Feed component tree (rows, semantic
streaming turns, markdown prose, diffs, todos, subagent cards) — zero
rendering drift by construction. The vite alias table in
remote-client/vite.config.ts is the compatibility contract: exactly five
substitutions (static-hljs CodeBlock, frozen-settings app-store hook,
no-op perf client, plain-anchor SafeMarkdownLink, plain SafeInlineCode),
each stub documenting its divergence; everything else is literally the
desktop's code. Desktop styles + theme boot via @renderer/styles.css and
applyTheme(DEFAULT_SETTINGS) under @tailwindcss/vite.

TranscriptStore is the phone's minimal SessionRuntime, built from the
desktop's OWN reducers (foldSemanticEvent, registry transcript mappers,
indexEntryIntoMaps): one seen-uuid set gates backfill and live entries,
history prepends / live appends, one stateful mapper per session (codex
turn cursor), the stream-phase machine reduced outside the fold exactly
like useIpcSubscriptions. Ghosts and the desktop-only subsystems are
deliberately skipped (documented in the design doc + store header).

SessionView maps store snapshot → Feed props mirroring TileLeaf's own
runtime→Feed contract; SessionFeedProvider wraps the tree so desktop rows
(AskUserQuestionRow) answer through the WS transport unmodified. Bundle:
450 KB gzip, no monaco. Pipeline pinned end-to-end by a real-socket test
(mapper + dedupe + backfill-prepend + fold + phase).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The big one: the stream-phase machine is now a SHARED pure reducer
(semantic/streamPhaseMachine.ts) extracted verbatim from
useIpcSubscriptions — the desktop hook and the phone TranscriptStore both
call it, so the hand-copied fork (with its three silent divergences) is
structurally impossible now, not merely reviewed against.

TranscriptStore ingest corrections, each traced to its desktop
counterpart: history chunks map through FRESH chunk-scoped mappers (the
session-lifetime codex cursor was being corrupted in both directions);
the prepend marker only advances on raws that yielded KEPT entries; a
missing marker kills the load-older affordance instead of leaving a dead
button; failed initial backfills retry on session-list frames (reconnect)
and on live-entry arrival; per-session state evicts when sessions leave
the list; the kind fallback is never baked into a memoized mapper.

Transcript ROLL handling (/clear, resume onto a new provider session):
get-history replies now carry the file identity, live frames already do,
and the store resets + discards on disagreement — the stale-cache window
documented on SessionManager.lastTranscriptFile can no longer prepend the
previous conversation permanently.

Phone UX regressions restored: the TUI-text fallback renders for
pre-transcript states (trust dialog body, login errors) instead of a
blank feed, and AskUserQuestion is back in the server-verified tap bar as
the guaranteed answer path when the inline row can't render its options.

Also: RemoteServer.apply is exception-guarded (structured error reply,
not a phone-side timeout), historyLoader's resolver entrypoints delegate
to their FromFile twins (span/catch scaffolding deduped, filePath back in
failure records), and dead code dropped (unused lastTimestamp, App's
unreachable second PairScreen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant