feat(app)!: tabless — one sidebar, no tab strip (0353) - #564
Merged
Conversation
…e tab strip (0353 P1)
Phase 1 of exploration 0353. Tabs stay ON by default; the tabless path
is complete and switchable via ⌘K ('View: Turn off tabs').
- `tabsEnabled` store preference. When off, EditorArea renders the
router outlet directly — no strip, no editor groups, no
preview/promote — and useTabCommands/useTablessCommands swap so the
two modes never contend for the same chords.
- Route-derived titles: views publish through usePublishTitle (one call
replacing the 8 setTabTitle sites) into routeTitles; EditorHeader
resolves 'what am I looking at' from the route when tabless, from the
active tab when not. Share/Open-with/breadcrumb all follow.
- Chords: ⌘[ / ⌘] walk router history, Ctrl-Tab toggles the recent two
routes (routeHistory, capped at 2 — a toggle, not a stack), ⌘W
returns home, ⌘T still creates a page.
- Recents decouple from tab opening: trackRouteVisit runs on every
route change in BOTH modes; syncRouteToTabs keeps refining entries
with tab titles (touchRecent dedupes by node id).
- WorkingSet: Pinned (durable, user-curated) above Recents (automatic,
decaying) always visible in the sidebar — the tab strip's replacement,
non-destructive by construction so nothing is ever lost by navigating.
- SplitPane: side-by-side as a LAYOUT concern — the secondary pane is a
live 0346 frame over one node, not a tab group; closing it orphans
nothing. splitTarget is ephemeral (never persisted/restored).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
…rt (0353 P2) Phase 2 of exploration 0353: nine hand-rolled navs get one shape. - Contracts (sidebar/contracts.ts): SidebarRowModel / SidebarRowSource / SidebarLens. Rows carry their own sort policy and ONE mute flag; effectiveBadge + shouldBump make mute suppress badge AND unread bump together (shipping those independently is the recurring bug class Telegram/Teams document). - Registry (sidebar/registry.ts): sources and lenses register like slot views, so adding a content type to the sidebar is a registration, not a tenth bespoke nav. sourcesForLens bounds what a lens may mount. - Built-in sources (sidebar/sources.tsx): documents (pages, databases, canvases, dashboards, maps — manual order), channels/DMs (recency + unread bump, reusing ChatsPanel's exact isUnread rule for badge parity), people, saved views. Lenses: All, Docs, Chats, People, Views. - UnifiedTree: lens chips replace the Explorer's type filters AND the surface switcher — picking Chats is the same gesture as picking Docs. Keyed by lens so only the active lens's sources mount (an unselected lens costs zero queries — the per-schema fan-out is real, 0317) and the hook loop stays stable across renders. - Registered as the 'tree' slot view alongside Explorer during migration; store gains activeLensId + mutedRowIds. Tests pin the semantics that make mixed types coexist: docs never reshuffle on edit, unread chats float, muted unread does NOT jump the queue, and manual order compares CODE UNITS (never localeCompare — the fractional sortKey invariant). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: xNet Test <test@xnet.dev>
Phase 3 of exploration 0353: nine navs collapse into one. - User sections (sidebar/sections.ts) replace SURFACES + navPinned + the More roll-out. A section points at a lens, a route, or a pinned node — one grammar, no panel/route fork, so a feature area can no longer mint its own nav (the Teams stacked-rail anti-pattern the exploration names). Per-user and reorderable, following Slack custom sections / Linear personalized sidebar. - The bottom island renders the UnifiedTree under the active lens instead of switching between seven bespoke panels: Chats, Tasks, Today and Data panels retire into lenses/routes; AI stays a dock (a companion, never a nav surface). - SectionsMenu replaces SurfacesMenu behind the same preference, so pinning still curates what stays visible. - Unknown persisted section ids resolve away rather than crashing a shell shared across builds (the 0280 migration lesson). Live-verification fix: the registry's getSources/getLenses returned a fresh array per call, which made useSyncExternalStore loop forever and blanked the shell. Snapshots are now rebuilt once per change and returned by identity. Verified in the browser: section rows + lens chips render, the Chats lens shows channels/DMs where documents were, navigation works with no tab strip, 0346 frames still render inside the opened doc, and the console is clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: xNet Test <test@xnet.dev>
…353 P4) Phase 4 of exploration 0353: the stragglers that would have made 'one sidebar' hollow. - CRM's internal CrmTab bar was a second, unrelated tab system inside a route. Its views are now addressed by the route's ?view= search param and rendered with the shared LensChips primitive — linkable, palette-reachable, and no longer a tab bar. Falls back to local state where the surface doesn't route (tests, embeds). - NavRow: the one primary-row primitive. Section rows and the Settings section list were byte-identical markup in two files; both now render through it. Divergent row markup is how nine navs happened, so the primitive is the mechanical half of keeping one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: xNet Test <test@xnet.dev>
Phase 5 of exploration 0353 — the default flips. - tabsEnabled defaults to false: the workbench opens as one surface with the working set in the sidebar. Store migration v5 adopts it for profiles that never chose and seeds the unified-nav defaults. - Deleted Sidebar.tsx and Rail.tsx (the shipping floating shell never mounted either) plus their slot registrations, and removed the now-dangling placements from the @xnetjs/plugins preset trees; the default tree's left dock leads with the unified 'tree' view. The rail REGION stays as a placement target for user-moved views. - frame routes documented as plain routes when tabless. TWO DELIBERATE DEVIATIONS from the doc's Phase 5, both flagged for review: 1. The tab code path is KEPT behind the preference rather than deleted. The doc's own validation checklist records a dogfood tripwire (the Notion failure signature — users escaping to OS windows), and a tripwire needs something to fall back to. 'View: Turn on tabs' restores the old shell; deleting TabBar/groups now would make the rollout a one-way door. Removal should follow a dogfooding period. 2. calm/CalmSurface.tsx is NOT deleted despite being listed as a dormant remnant: ShellFrame still renders it on the focus/zen path. The survey's 'dormant' label held for Sidebar/Rail, not for it. Migration design note: v5 deliberately KEEPS persisted groups. Wiping them would mean turning tabs back on lost your session — the opposite of a reversible rollout. Tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: xNet Test <test@xnet.dev>
Two defects the 0353 validation pass turned up, both only visible with
tabs off:
- `useNode` keeps the previous node's data while the next id loads, so
`usePublishTitle` published the old title against the new route — the
header showed the node we just left and recents minted an entry under
the new id with the wrong name. Views now pass the loaded record's own
id and the publish waits for the data to catch up.
- The recent-two toggle handed an already-encoded pathname to
`navigate({ to })`, which expects a path template; node ids contain
slashes, so the param never round-tripped and Ctrl-Tab left the
previous document on screen. History navigation now parses the
pathname back into a descriptor and reuses `navigateToNode`.
Signed-off-by: xNet Test <test@xnet.dev>
Three TabNodeTypes had no case in the switch, so navigating to them did nothing at all — invisible while tabs carried navigation, load-bearing now that the route is authoritative and the tabless history chords resolve a remembered route through this same switch. Adds the missing cases and a never-typed default so the next tab type can't be added without one. Signed-off-by: xNet Test <test@xnet.dev>
Two defects, both caught running the 0353 split validation: - The split pane put a `Hairline` — a resizable-panels Separator — in a plain flex row. Separator throws without a Group ancestor, so the whole editor subtree hit the router's catch boundary and the split silently never appeared. The panes are now a real Group/Panel pair, which also makes the divider draggable like every other workbench split. - The command split with `recents[0]`, which is always the node already on screen, so it would have shown a document beside itself. It now picks the most recent entry that isn't the current route. Signed-off-by: xNet Test <test@xnet.dev>
The mobile header read its title, icon and share target from `selectActiveTab`, which is empty once tabs are off — a tabless phone would show a bare "xNet" and a share button wired to nothing. It now derives the same route-based `current` descriptor the desktop header uses, so mobile adopts tabless rather than deferring it. Signed-off-by: xNet Test <test@xnet.dev>
The 0273 disclosure ladder walks Esc down bottom → right → left dock. The split pane is the newest surface and was not on it, so the only way out was the close button or the palette. It is now the first rung, matching the ladder's most-recent-first shape. Also adds the route smoke test the 0353 checklist asks for: every TabNodeType round-trips routeForTab → tabFromPathname, which is what makes tabless navigation and the history chords safe. Signed-off-by: xNet Test <test@xnet.dev>
…ing the left nav Signed-off-by: xNet Test <test@xnet.dev>
LabView publishes its title against the route (0353), so it now reads router state — which the standalone test harness doesn't provide. Stubs `useLocation` rather than standing up a router the test doesn't need. Signed-off-by: xNet Test <test@xnet.dev>
Contributor
🖼️ UI changes in this PRScreensInteractionsAuto-captured by CI · run. Informational — not a blocking check. |
Contributor
|
Preview removed for PR #564. |
The surfaces roll-out put the pin toggle inside the row button, so the row's accessible name became "DiscoverPin" — one control announcing two actions, with the pin unreachable on its own by keyboard. It broke the safety-ui E2E, which locates the row by its exact name, and it would have read the same way to a screen reader. The pin is now a sibling button with its own label; the row is just its label again. Also formats two files the tabless work left unformatted. Signed-off-by: xNet Test <test@xnet.dev>
Eight views stopped calling `useWorkbench`/`useEffect` directly when they moved to `usePublishTitle` (0353 P1), but kept the imports — which fails lint. Signed-off-by: xNet Test <test@xnet.dev>
crs48
added a commit
that referenced
this pull request
Jul 19, 2026
…icon (#582) Exploration doc only — no code changes, no changeset needed. ## What this explores What xNet looks like as an OS-level facility rather than an application-layer library: a Rust systems crate, a system daemon, a filesystem, or silicon. ## Three findings that reframed it **1. The Rust kernel already exists and is unwired.** `rust/xnet-core/` is a 414-line crate with four dependencies that already implements identity, canonical JSON, change hashing, Ed25519, LWW, negotiation, and authz — and passes the same golden vectors as the TypeScript, Swift, and Python kernels. Its README claims it is the only implementation that reproduces a TypeScript signature byte-for-byte. The blocker is a `crate-type = ["lib"]` line and an unavailable uniffi toolchain. `0200` (the spec boundary) is `[x]`; the binding never landed. **2. The filesystem is where this idea has died for thirty years**, and not for the usual reasons. Optimistic replication works — Coda proved it in 1992. What killed FS-level sync: Ficus dispatched merge logic by *regex on the filename* because Unix has no types; Coda's entire vocabulary for "a human must decide" was to make the file look like a broken symlink; and Microsoft owned the Windows FS virtualization driver and abandoned it for their own flagship use case (Scalar matched VFS-for-Git in one-tenth the code once the real, application-level bottleneck was fixed). The mismatch is at the *interface*, not storage — POSIX has nowhere to put two concurrent heads. **3. "Bake it into the processor" is a clean no.** No Ed25519 instruction on any shipping CPU (x86, ARMv9.7, ratified RISC-V all cover symmetric crypto only). Verify throughput flat since Skylake 2015. A BlueField-3's entire PKA subsystem loses to one x86 core doing Ed25519 in software. ## The actionable one Chrome's whole-fleet Windows telemetry puts TPM ECDSA at **P50 200ms — five signatures/second**, which kills per-change hardware signing (~560x slower than our software path). But **0357's `BatchCommit` inverts it**: one hardware signature over a 1000-change root amortises to **0.2ms/change, cheaper than the 357µs software Ed25519 it replaces**. The same arithmetic makes post-quantum affordable — ML-DSA's 2,420-byte signature goes to ~2.4 bytes/change amortised. This argument is only available because the batch envelope already shipped. ## Recommendation A daemon and an attested identity — not a filesystem and not a chip. Wire the Rust kernel (L1), design `xnetd` (L2), prototype an FS projection read-only, and reject the kernel-FS and silicon rungs in writing. ## Charter §6 Applies the three no-ground-rent tests to the one tempting lane this opens ("attestation-as-a-service"). It **fails all three** — attestation binding is a protocol fact computable by anyone holding the two public keys. Recorded as a refusal in writing before anyone builds it. Attestation stays MIT, self-hostable, verifiable offline. ## Documented but explicitly not actioned BLAKE3 is likely a small regression vs SHA-256+SHA-NI on our ~554-byte envelopes (the famous 12x was measured on a CPU without SHA-NI, and BLAKE3 has no SIMD parallelism below 4KiB). Changing it is a protocol major across four kernels for a sub-millisecond win on a 357µs operation — documented as a non-action, revisit only if protocol v5 opens the envelope anyway. ## Note for reviewers Originally numbered 0358; this branch was cut from a stale main (PR #564 era) so the local number scan missed 0358–0367. Renumbered to **0369** after scanning all remote branches and worktrees — 0368 is claimed by an unmerged food-forests exploration. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.







Implements exploration 0353 — Tabless: removing the tab strip and unifying the left nav (30/30 checklist items).
What changes
The tab strip is gone; a working set replaces it. Navigation was already route-authoritative, so the strip was a windowing system layered over routing that already worked. In its place: Pinned (durable) above Recents (decaying) in the sidebar,
Ctrl-Tabto toggle the last two routes,⌘[/⌘]for history, andView: Split with currentfor side-by-side without tab groups. Durable side-by-side belongs on a page as 0346 frames.Nine left navs collapse into one tree plus lenses. Channels, people and saved views now live beside documents in a single tree projected through lenses (All / Docs / Chats / People / Views), with type-scoped sort defaults — chats sort by recency with an unread bump, docs keep manual
sortKeyorder so editing never reshuffles them. Muting is one flag that suppresses badge and bump together.Tabless is the default, and reversible. The v5 migration adopts it for profiles that never chose, but deliberately keeps
groupssoView: Turn on tabsrestores the session it left. The doc records a dogfood tripwire (>3 OS-window duplications/person/week) with that command as the documented fallback.Defects the validation pass caught
Five real bugs, each found by running the checklist rather than reading it:
useNodekeeps the previous node's data while the next id loads, sousePublishTitlepublished the old title against the new route — wrong header, and a recents entry minted under the new id with the old node's name. Views now pass the loaded record's own id and the publish waits for the data to catch up.navigate({ to })— which expects a path template — never round-tripped the param. History navigation now resolves through the same descriptor path a click uses.TabNodeTypes had no case innavigateToNode's switch — invisible while tabs carried navigation, load-bearing once the route is authoritative. Added, plus anever-typed default so the next type can't be added without one.Hairline(a resizable-panelsSeparator) in a plain flex row;Separatorthrows without aGroupancestor, so the whole editor subtree hit the router's catch boundary silently. Now a realGroup/Panelpair, which also makes the divider draggable. It also split withrecents[0]— always the node already on screen — so it would have shown a document beside itself.MobileShellread title, icon and share target fromselectActiveTab, empty when tabless. It now derives the same route-based descriptor the desktop header uses, so mobile adopts tabless rather than deferring it.Verification
pnpm vitest run apps/web/src— 845 tests, 118 files, all greenpnpm --filter xnet-web typecheckclean (this is what proves thenavigateToNodeswitch exhaustive)TabNodeType, the stale-title guard, the v5 migration, and the sidebar sort/mute contracts🤖 Generated with Claude Code