Skip to content

feat(workspace-rail): a persistent resizable rail of project tabs - #3

Draft
andreiverdes wants to merge 5 commits into
pr2-workspace-switchfrom
pr3-workspace-rail
Draft

feat(workspace-rail): a persistent resizable rail of project tabs#3
andreiverdes wants to merge 5 commits into
pr2-workspace-switchfrom
pr3-workspace-rail

Conversation

@andreiverdes

Copy link
Copy Markdown
Member

Layer 3 of 3. Base: pr2-workspace-switch (#2).

A persistent rail of project tabs on the left: click to switch, current view preserved (browser-tab semantics — switching from Activity lands on Activity). Local .beads/ projects and server:// Dolt connections both appear. Tabs can be renamed and given an emoji, stored in the registry alongside the entry. Resizable 180–420px, collapses to icons, persists both, hidden below 768px where the layout is single-panel.

It renders outside the startup gate deliberately, so it stays on screen while the gate re-checks during a switch instead of vanishing behind a spinner.

Testing

bun run test green at 407 server + 298 client. Beyond that, a plan covering what unit tests cannot observe — real bd data, real files, concurrency, scale, UI surfaces — is recorded in docs/testing/workspace-rail-test-plan.md. It found three defects unit coverage missed; two are fixed here:

  • Keycap emoji rejected as tab icons. SINGLE_EMOJI required an Extended_Pictographic base, but a keycap's base is an ASCII digit, so 1️⃣ / #️⃣ / *️⃣ were refused — while the dialog documents its field as accepting whatever the OS emoji picker produces. Fixed with a keycap alternative that still requires the enclosing U+20E3, so a bare 1 stays rejected.
  • The rail missed workspaces added from the dashboard. A registry write raises no bd change signal and membership changes had no propagation channel (labels already had one), so the tab appeared only when a bd poll fired. Fixed with lib/workspace-registry-events.ts, published from every membership mutation and consumed by both list-holding surfaces.
  • A third, pre-existing, is documented in the plan rather than fixed — it reproduces byte-identically at the merge base.

Scale: a 100-workspace registry lists in 3.6ms with zero bd spawns; 500 beads / 20 epics load in 388ms; the client LRU holds at most 6 payloads.

Also replaces docs/screenshot.png, which predated the rail, and adds a demo GIF.

setWorkspaceLabel's SINGLE_EMOJI required an \p{Extended_Pictographic}
base. A keycap is an ASCII digit, "#" or "*" followed by U+FE0F and
U+20E3, so "1️⃣" / "#️⃣" / "*️⃣" were rejected as "Icon must be a single
emoji." — while workspace-tab-dialog.tsx documents its free-text field
as accepting whatever the OS emoji picker produces, and the macOS
picker offers keycaps.

Adds a keycap alternative to the pattern. U+20E3 stays mandatory, so a
bare "1" or "#" is still rejected.

Found by case A5 of docs/testing/workspace-rail-test-plan.md.
…rface

Adding a workspace on /workspaces left the rail unchanged; the tab only
appeared after opening a project and waiting. A registry write raises no
bd change signal, so the rail had three ways to learn anything: its
mount fetch, the workspace cookie (which updates only the active
highlight, not the list), and useSubscriptionChangeSignal — a bd poll.
That poll was the wait. The rail's own add button worked only because
workspace-rail-panel.tsx called refresh() in its own handler; the
dashboard's handler updated its own cards, and the two share no state.

Labels already hit this wall and solved it with a propagation channel
(lib/workspace-labels.ts). Membership changes had no equivalent, so this
adds the sibling channel and publishes from every membership mutation:
both add dialogs (one wrapper covers the local, server and local-to-
server paths), a successful bd init, and unregisterWorkspace, which
covers removal from all three surfaces that offer it.

The rail and workspaces-page both subscribe and re-read the registry.
Subscribing the dashboard closes the mirror-image bug: an add or removal
driven from the rail left its cards stale. The panel's own refresh()
calls are gone so the channel is the single mechanism — no add path can
be wired up while forgetting to announce. The event carries no payload,
so consumers cannot drift from what a fresh read returns.
Every case ran. A1-A5 pass, B1-B6 and B8-B10 pass, and the pre-PR gates
are green apart from C5, which is a no-op by configuration. B7 fails on
a defect that reproduces byte-identically at the merge base, so it is
not this change set's to block on.

Records the evidence per case (timings, drill outcomes, the quantified
cross-process lost updates) and writes up the three defects the plan
surfaced: the keycap rejection and the stale rail, both now fixed, and
B7's vanished-workspace-reads-as-empty, left documented with its root
cause and the bdExec choke point a fix would use.

Also notes why B4-B9 could not be automated: the client throws
RpcUnavailableError without a Tauri runtime, and Tauri's macOS WKWebView
is not CDP-attachable.
…mo gif

The screenshot predated the rail, so the README described "a resizable rail
of project tabs on the left" above an image with no rail in it.

Replaces it with a capture of three workspaces (orbit, atlas, pulse) and adds
a GIF underneath showing what a still cannot: switching projects, the warm
switch keeping its view across Beads and Activity, and renaming a tab with an
emoji.

The demo workspaces are synthetic and their paths are mocked, so no local
directory names ship in the docs.
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