Skip to content

feat(web): read-only browser mirror of one live review (Phase 5, PR 1) - #746

Open
benvinegar wants to merge 24 commits into
mainfrom
claude/review-rebuild-phase-5-mirror
Open

feat(web): read-only browser mirror of one live review (Phase 5, PR 1)#746
benvinegar wants to merge 24 commits into
mainfrom
claude/review-rebuild-phase-5-mirror

Conversation

@benvinegar

@benvinegar benvinegar commented Aug 15, 2026

Copy link
Copy Markdown
Member

Phase 5, PR 1 of the browser-review rebuild (docs/browser-review-rebuild.md): the read-only browser mirror. A browser client that authenticates with the capability from the URL fragment, reads one live review over the Phase 4 HTTP surface, and renders it as a read-only review stream with Pierre. No actions, no note editing, no selection sync — that is PR 2. The browser page itself has no runtime entry until Phase 6; the one user-visible change is a patch changeset for a stat-badge consistency fix the drift audit surfaced (below).

What

New browser tier (src/web/) — Node-free by gate, every export named for its runtime (Browser* = runs in the browser; Web* = server code operating the web path; bare = shared language):

  • reviewApiClient.ts (BrowserReviewApiClient) — publication reads, Range-windowed resource reads reassembled by the shared ReviewChunkAssembler, and the SSE stream read with fetch streaming (the capability travels as a header, which EventSource cannot carry), decoded by reviewEventProtocol's own record decoder and frame parsers. Failures are the shared wire failure shape with catalog wording.
  • reviewMirror.ts (BrowserReviewMirror) — holds one publication; orders candidates with classifyReviewPublication only; recovers from failed loads and dropped streams (reconnecting status over a held document); never moves position backwards; verifies every canonical file by recomputing its content identity.
  • pierreDocument.ts — projects ReviewFileV1 into the per-file render model from the shared geometry/anchors/expansion modules — no sideRange, no bare split("\n"), and expanded-gap lines now come from core's reviewExpandedGapLines, beside its inverse.
  • reviewSources.ts (BrowserReviewSourceStore) — generation-tagged source reads: stale reads dropped at resolve and render, failures surface catalog wording, one read per file, reopening a failed gap is the retry.
  • reviewDigest.ts (browserReviewDigest) — the sanctioned synchronous SHA-256 platform carve-out, pinned differentially against the Node implementation.
  • viewOptions.ts / ReviewStream.tsx / ReviewApp.tsx / main.tsx (BrowserReviewStream, BrowserReviewApp, mountBrowserReviewPage) — client-locus view options over host defaults; one Pierre render per hunk with gap strips; page shell in review order.

Shared additions (each one extraction, adopted by every consumer, old copies deleted):

  • core/review/presentation.tsreviewFileStatBadges, now the only stat-badge formatter: sidebar, diff header, static pager, and browser all consume it (E1, completed by the drift audit).
  • core/review/viewOptions.tsREVIEW_VIEW_OPTION_LOCUS (G1a) and the single LayoutMode union.
  • core/review/expansion.tsreviewExpandedGapLines, the forward gap-row derivation beside resolveReviewExpandedLine, consumed by terminal, browser, and conformance.
  • core/review/document.tsreviewFileContentIdentityOf; the projection hashes the file it built.
  • session/reviewEventProtocol.tsReviewEventSseDecoder, the SSE record grammar colocated with its encoder; five former hand-rolled copies deleted.
  • session/reviewHttpProtocol.tsREVIEW_ERROR_STATUS, reviewHttpFailure(), reviewErrorCodeForStatus(): one owner for the HTTP failure contract, consumed by server and client.
  • packages/session-broker-corecreateReconnectScheduler (C5) and concurrency.ts (inBoundedParallel + ConcurrencyGate), adopted by broker, browser, and producer.

Phase 4 surface completed: resource responses carry whole-resource size/digest headers (colocated writer/parser pair, round-trip tested) so a client can verify reads; the server module is renamed for what it is — daemon code operating the web path: src/session/broker/webReviewServer.ts (WebReviewServer).

Gates extended: browser-tier boundary walk (BROWSER_SAFE_CORE_MODULES), and scripts/review-vocabulary.test.ts scans src/web for re-declared shared constants and hand-written digest patterns.

Conformance

Real consumers registered for geometry (browser projection), ordering (the real mirror), and events (the client reader against a real HTTP surface). Navigation and wire deliberately wait for PR 2 — a read-only client plans no moves and sends no actions; recorded in the audit doc.

Findings repaid

A6, A7, A11, C1/C2 (browser), C3 (closed), C4 (client), C5, D4 (browser), E1 (completed across all four surfaces), G1(a), G4 (browser adoption); G3 gains its first consumer. E2 and G2 are decided and recorded in the audit doc: theme follows the terminal as a wire-delivered host default with client override (shared by wire, not by import); selection is per-client with optional follow-the-terminal. Both land as implementation in PR 2.

Post-open hardening

Two dedicated review passes ran against this branch after it opened, and everything they found is fixed here:

  1. Adversarial correctness review — 8 findings, all confirmed with failing-test-first regressions: mirror recovery (failed loads/dropped streams no longer wedge the client), monotonic position under mid-load acceptance, generation-safe source reads, surfaced source failures, restartable lifecycle (StrictMode-safe), line-number plumbing, source-read dedup, render-model memoization.
  2. Drift/duplication audit — verdict "load-bearing seams clean" (ordering, geometry, verification, locus all single-sourced), plus 7 repairs for what it did find: the incomplete stat-badge extraction (with a different zero-count policy per surface — now one policy, hence the patch changeset), the re-declared SSE record grammar (5 copies → 1 decoder), duplicated bounded-concurrency helpers (3 copies → 1 shared module), the client's re-declared HTTP failure shape and status mapping, invented wording for cataloged error codes, the third copy of the expanded-gap derivation (now in core beside its inverse), and inline content-type/layout-union literals.
  3. Runtime-tier naming — every src/web export carries Browser*/browser*; the daemon's serving module is Web*. One behavior-free commit; the targeted suite is byte-identical across it.

Gates

  • bun run typecheck, bun run lint (0/0), bun run format — clean; patch changeset (file change stats render consistently across sidebar, diff header, pager, and browser, hiding zero counts everywhere).
  • Full bun test at head vs. baseline: failure set no worse (rotating PTY parallel-load flakes that pass in isolation — a full test/pty/ re-run gives 111/0 — plus the two pre-existing website spec errors). Targeted suites at final head: 1,003 tests / 0 fail across web, conformance, gates, core review, broker-core, and the producer→daemon→HTTP→client integration test, independently re-run at review time. src/ui/ + src/opentui/ in full: 1,031/0 (the stat-badge change is TUI-visible and deliberately pinned).

Residuals for PR 2

Review semantic state (selection, filter, expansion, notes) on the wire — the blocker for every note-shaped browser finding. Then: action dispatch, note editing (draft-body intent), selection sync per the decided G2 policy, theme-as-host-default per the decided E2 policy, key bindings and the command palette from the shared catalog, A9 STML parser relocation, navigation + wire conformance consumers. Recorded low-severity audit notes (gap-strip wording, rangeLabel format, reviewSources descriptor lookup) carry to PR 2's cleanup.


Generated by Claude Code

claude added 5 commits August 14, 2026 23:20
…dentity, and reconnect timing

Phase 5 PR 1 needs four facts that already exist in the repo but not in a
place a browser client may reach. Extracting them first keeps the client
from being the second implementation of any of them
(docs/browser-review-seam-audit.md).

- E1: `reviewFileStatBadges` in `core/review/presentation.ts` decides badge
  text and states the two policies behind it — hide a zero, mark truncation
  once. The sidebar's private `formatSidebarStat` is deleted and tombstoned,
  so churn cannot read one way in a terminal and another in a browser.
- G1(a): `REVIEW_VIEW_OPTION_LOCUS` in `core/review/viewOptions.ts` says
  which options belong to the review and which to one client, as a table
  over the option schema rather than as a consequence of which module
  happens to read a value. Totality is a `Record` over the keys.
- D4 reader half: `reviewFileContentIdentityOf` recomputes a projected
  file's identity from the file itself, so a reader can check that what
  arrived still describes the content it declares without carrying a field
  list of its own. Projection now hashes the file it built rather than a
  parallel description of it, so there is one derivation.
- C5: `createReconnectScheduler` in `@hunk/session-broker-core` owns "one
  pending attempt, a delay, an unref'd timer". `SessionBrokerConnection` and
  `HunkSessionBrokerClient` both drop their own copy; their fixed 3s timing
  is unchanged (the default factor is 1), and a browser client can now ask
  the same scheduler for backoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
…urces, event stream

The browser client's transport half. It composes the Phase 4 contracts rather
than restating them: routes and authorization from `reviewHttpProtocol`,
frame names and envelopes from `reviewEventProtocol`, chunk verification from
the shared `ReviewChunkAssembler`, wording from `reviewErrorCatalog`. That is
what closes C4's client half and C2's browser site — the prototype re-declared
all of it and its client bounds were compatible with the server's only by luck
(docs/browser-review-seam-audit.md).

- `reviewApiClient.ts` reads the publication, reads a resource as several
  bounded windows joined by the shared assembler, and follows the event
  stream with `fetch` — not `EventSource`, which cannot present the
  capability header, and whose built-in reconnect would be a second
  scheduler (C5).
- `reviewMirror.ts` orders every arriving publication with
  `classifyReviewPublication` and nothing else, resyncs on `gap` only (a
  generation's document is immutable), and abandons a load a newer
  generation superseded. It verifies each canonical file by recomputing its
  content identity rather than checking a field list of its own (D4).
- `reviewDigest.ts` is a synchronous SHA-256, because the shared assembler
  verifies inline as chunks arrive and Web Crypto is async-only. Platform
  hashing is one of the things the audit deliberately does not unify; the
  test holds it to the session's own digest.

One gap in the Phase 4 surface had to close for any of this to be verifiable:
a published catalog describes resources the producer has not measured yet, so
a reader could not learn what to verify a read against. Every resource
response now states the whole resource's size and digest in headers declared
beside the routes, and the daemon keeps the digest its own assembly verified
against instead of rehashing. `scripts/review-vocabulary.test.ts` gains the
browser tier, so the client cannot re-declare a constant it could import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
…ierre

The browser's rendering half. `pierreDocument.ts` turns a semantic file into
what Pierre draws from and refuses to compute anything core already owns —
hunk extents, gap addresses, note and reveal targets, expansion side, source
splitting, row totals — which is every A-finding the prototype's own adapter
got wrong (docs/browser-review-seam-audit.md, A3/A4/A5/A6/A7/A8/A10).

- The stream renders one Pierre view per hunk rather than per file, so a
  collapsed-region strip appears where the collapsed region is. Isolating a
  hunk is the shared re-basing walk, and the lines are sliced with the
  indices it reports rather than by a second count (A6).
- Gap strips are labelled from `reviewGapAddress`, and opening one reveals
  lines split by `normalizedReviewSourceLines` over the source resource —
  no `\r` glyphs, no phantom last line (A4). Which gaps are open is
  client-local presentation state in a read-only mirror; PR 2 lowers it to
  the shared `expansion/toggle` intent.
- File and hunk anchors are `formatReviewAddress`, giving G3's grammar its
  first consumer, and stat badges are the shared formatter's (E1).
- `viewOptions.ts` resolves only the options `REVIEW_VIEW_OPTION_LOCUS`
  calls per-client, over the host's resolved defaults when the page carries
  them (G1). Theme is deliberately absent: E2 is undecided, so the client
  renders in Pierre's own palette.
- `main.tsx` imports `core/fileLanguage` for its side effect, so `.mts` and
  `.cts` highlight in a browser as they do in a terminal (A11). The boundary
  gate gains a named list of browser-safe core modules for exactly that
  import — it refused it first, which is the gate working.

Three browser consumers join the conformance corpus and run the whole thing:
the projection against the geometry fixtures, the mirror against the ordering
fixtures, and the client's event reader against the C4 framing fixtures,
which is the sender and the reader on one corpus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
…it settled

The audit doc is the ledger this phase is measured against, so the browser
sites it closes are written down where the finding lives rather than in a PR
description: A6, A7, A11, C1, C2, C3, C4's client half, C5, D4, E1, G4, and
G1 part (a), with A3/A4/A5/A8/A10 and B6's geometry half now answered by the
browser projection beside the terminal.

Two entries are answers rather than repayments, and both change what PR 2 has
to do. The publication a client reads carries a review's content and nothing
about its semantic position, so every note-shaped browser finding waits on
review state reaching the wire — a read-only client cannot mirror a selection
by reading harder, and one that derived its own would be the duplication this
seam exists to prevent. And C3 closes by there being one supersede machine
rather than by unifying two: the runtime reload queue it was paired with never
landed in the rebuild.

E2 stays open on purpose — the client renders in Pierre's palette and imports
no terminal theme, so nothing has been decided by accident.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
…inned too

`ReviewStream` and every model module had colocated coverage; the shell that
puts them together did not. A static render is as far as this can go without
a DOM, which is enough for what matters here: the file list is in review
order, links through the shared address grammar, stays absent until a
document exists, and reports a failure in the words the mirror handed it
rather than wording invented at the edge (G4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hunk-web Ignored Ignored Preview Aug 15, 2026 4:14pm

Request Review

@socket-security

socket-security Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​react-dom@​19.2.41001007588100

View full report

The Package gate regenerates nix/bun.lock.nix and fails on drift; this
is the entry its own regeneration printed for the new devDependency,
applied verbatim since this environment has no nix to run the generator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a read-only browser client that authenticates with a fragment capability, mirrors live review publications and resources, and renders the review through Pierre. It also adds shared reconnect scheduling, resource measurement headers, browser boundary gates, conformance consumers, and extensive tests.

  • Adds browser URL parsing, HTTP/SSE transport, verified resource assembly, generation-aware mirroring, and read-only rendering.
  • Extracts shared presentation, view-option, content-identity, and reconnect primitives for existing and browser consumers.
  • Extends the HTTP resource protocol and daemon cache to retain and serve verified whole-resource measurements.

Confidence Score: 4/5

The malformed-path crash should be fixed before merging so invalid or hand-edited review links fail gracefully instead of aborting page initialization.

The new location parser can throw URIError on malformed percent encoding, and its uncaught production caller consequently fails to mount the browser review page.

Files Needing Attention: src/web/reviewApiClient.ts, src/web/main.tsx

Important Files Changed

Filename Overview
src/web/reviewApiClient.ts Adds capability-authenticated publication, resource, and SSE transport; malformed encoded path segments can throw during page initialization.
src/web/reviewMirror.ts Adds ordered, generation-aware browser mirroring with bounded canonical-file loading and integrity checks.
src/web/ReviewApp.tsx Adds the read-only page shell, mirror subscription, source loading, and review-ordered sidebar.
src/web/ReviewStream.tsx Renders each file and hunk through Pierre with shared gap geometry and lazy source expansion.
src/session/broker/browserReviewServer.ts Adds verified whole-resource size and digest headers to bounded range responses.
packages/session-broker-core/src/reconnect.ts Introduces a runtime-neutral single-timer reconnect scheduler reused by broker and browser clients.
src/session/broker/reviewResourceCache.ts Retains each assembled resource’s verified digest alongside its bytes for HTTP serving without rehashing.

Sequence Diagram

sequenceDiagram
  participant Browser as Browser Review
  participant Daemon as Session Daemon
  participant Session as Live Hunk Session
  Browser->>Daemon: GET event stream + capability header
  Daemon-->>Browser: Complete publication and resource catalog
  Browser->>Daemon: Range requests for canonical files
  Daemon->>Session: Load resource chunks
  Session-->>Daemon: Digest-verified chunks
  Daemon-->>Browser: Byte windows + size/digest headers
  Browser->>Browser: Assemble, verify, and render with Pierre
  Daemon-->>Browser: Later publication event
  Browser->>Browser: Order publication or resync generation
Loading
Prompt To Fix All With AI
### Issue 1
src/web/reviewApiClient.ts:135-136
**Malformed paths abort page initialization**

When a review URL has a valid capability fragment but contains malformed percent encoding in any pathname segment, `decodeURIComponent` throws instead of returning an invalid-location result, causing the uncaught exception in `mountReviewPage` to abort page initialization rather than display the invalid-link message.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "test(web): cover the review page shell, ..." | Re-trigger Greptile

Comment thread src/web/reviewApiClient.ts Outdated
claude added 18 commits August 15, 2026 00:48
…owing

decodeURIComponent throws URIError on truncated percent-encoding, and
parseReviewLocation's job is to answer whether a URL is a review link —
a hand-edited one should land on the invalid-link message, not abort
the mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
… PR 2

Theme: the browser follows the terminal's theme as a wire-delivered host
default (client-overridable) — shared by wire, not by import, so no
second palette exists to drift. Selection: per-client with an optional
follow-the-terminal mode, so the B11 cursor fight cannot recur and
actions keep carrying explicit targets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
…tream

The mirror conflated "where the review is" with "what the document was read
for", so a load that failed had no way back: every later publication of the
same generation classified stale or accepted and never re-read it. A dropped
stream took the same failure path over a document the review never
invalidated, and the resync that followed was a replay, so the page stayed
failed with a perfectly good diff behind it.

Ordering stays the shared classifier's alone; load state becomes its own fact
(the generation the document on screen was read for). A publication is read
again when it opens a new generation, or when the mirror has no document for
the generation it names and none is on the way — which is the retry. A
publication over a document the mirror still holds now restores ready, so a
reconnect recovers without re-reading a generation that cannot have changed,
and a stream that drops over a good document degrades to a new reconnecting
status instead of hiding the review.

Two lifetime bugs fall out of the same seam. A finished load published the
address it captured at its start, walking the position back past a revision
accepted while it read; it now publishes whichever of the two the classifier
puts further along. And stop() was permanent — the stopped flag refused every
later start() — so React's double-mount left the page dead; stop() now cancels
rather than stops the shared scheduler, abandons the in-flight load by token,
and start() attaches again unless the session already said goodbye.
The page read a file's whole source on every gap toggle and stored whatever
came back under the file key. Three problems in one callback: a read issued
before a reload landed after it, so gen-A text drew gen-B's gaps; a refused
read was dropped on the floor, leaving the gap claiming forever that it was
loading; and every toggle re-read, re-assembled, and re-hashed the same
megabytes on the main thread.

The lifecycle moves into a ReviewSourceStore the page watches like the mirror:
it captures the generation at request time and drops what lands for another,
reads once per file, and keeps a refusal as a state so the expanded gap can
say — in the shared catalog's words — why there are no lines. Retry stays the
reader's gesture: opening the gap again asks again, and nothing retries on its
own. The request also moves out of the state updater React may run twice.
…e header one

GapStrip took one flag and used it for both questions it answers, so the lines
an opened gap reveals carried numbers only when hunk headers were on — a
reader with line numbers on and headers off got unnumbered lines, and one with
the opposite pair got numbers they had turned off. The two view options are
now plumbed separately.

The file's render model also moves behind a memo keyed on the file it is built
from. It depends on content alone — width reaches Pierre, not the model — so
every resize event was rebuilding each file's model and handing Pierre fresh
object identities to re-highlight against.
…settling

The mirror now re-reads a generation it holds no document for, which is how a
failed load is retried. That makes the adapter's "a read means gap" inference
depend on the first read still being in flight, so the reason is written down
where an editor would otherwise add an await.
…screen

A new generation's document reaches the stream one render before the effect
that clears the source store can run, so an open gap could paint the previous
file's lines under the new file's geometry for a frame. The store now carries
the generation its entries were read for, and the page hands the stream
nothing when that generation is not the one it is drawing.
The diff header and the static pager hand-rolled their own stat badges, so
the same file read `+1 -0` there and `+1` in the sidebar, which already took
its text from the shared review formatter. Both now call
`reviewFileStatBadges`, and a zero count stops being a badge everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
The `id:`/`event:`/`data:` prefixes, the slice offsets behind them, and the
blank-line record boundary were declared four times — once implicitly by
`encodeReviewEventFrame` and three more times by readers that had to guess
compatible offsets (C4). `ReviewEventSseDecoder` now sits beside the writer,
holds a partial record until the read that completes it, and every reader
imports it instead of re-deriving the grammar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
Three copies of "run this under a limit" had already drifted: the mirror's
clamp let a limit of zero start no workers at all, while the broker's guarded
it. `inBoundedParallel` and `ConcurrencyGate` now live together in the
browser-safe broker core, with the guarded clamp, and the mirror, the broker
state, and the producer's resource store all use them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
…oks like

The client re-declared the wire failure type field for field, rebuilt the
server's body-and-wording policy beside it, and read a bare 416 through a
literal while the code-to-status table stayed server-only. The failure
builder and the status table now live in `reviewHttpProtocol` with the rest
of the HTTP contract, and both ends import them; the client reads a
body-less refusal through the table's unambiguous inverse instead of a
hardcoded status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
The page's "incomplete link" text was the catalog's `unauthorized` remedy
pasted in without its statement, and eight other sites replaced the catalog
sentence with wording invented in this tier — so a browser explained the same
failure differently from the terminal beside it (G4). Each now states the
catalog's sentence and appends what only this client knows in parentheses,
which is what `transportFailure` already did.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
…inverse

Core owned "which gap does this line belong to" but not "which lines does
this gap reveal", so the terminal, the browser, and the conformance model
each paired the two sides' ranges with the expansion side themselves (A6) —
one offset apart from mislabelling every expanded line on a side.
`reviewExpandedGapLines` states it beside `resolveReviewExpandedLine`, with a
round trip between the two, and all three read it from there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
Three small re-declarations: the client asked for an event stream by a
literal it could import, a conformance adapter spelled the canonical-file
content type out by hand, and the browser's view options declared their own
`auto | split | stack` beside the terminal's `LayoutMode`. The layout union
now lives with the shared view-option classification, which both tiers
already reach, and the two content types come from the modules that define
them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
Three tiers share this vocabulary and only the import path said which one a
name came from. Under the repo owner's rule — `Browser*` runs in a browser,
`Web*` is the daemon operating the web path, bare names are the shared
language both speak — every `src/web` export takes the `Browser` prefix, and
the daemon's HTTP surface becomes `webReviewServer` / `WebReviewServer`.
File names, the shared protocol modules, and the browser-safe gate lists are
unchanged; so is behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
Every other browser/web-path file in the repo already carries its
runtime prefix; after the symbol rename, src/web's bare file names were
the odd ones out and no longer matched their primary exports. main.tsx
keeps its bundler-conventional name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
…-phase-5-mirror

# Conflicts:
#	bun.lock
#	package.json
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.

2 participants