Repository navigation
feat: show and act on every device's workspaces and sessions in the sidebar - #3725
Merged
Merged
Conversation
Activating an already opened workspace moved it to the front of the host's opened list, so every re-read reordered the sidebar and overwrote the order users arranged. Newly opened workspaces still go first; recency stays in the recent list.
Add a lazily retained, read-only catalog of every online same-account host (get_opened_workspaces and list_persisted_sessions_page over raw HostInvoke reads), kept current by debounced @host/catalog invalidations: session-only hints skip the workspace list, refreshes are held while the window is hidden or a cross-device open runs, and notifications are coalesced. Catalog pages seed each background device's FlowChat container through the shared metadata projection; seeds are promoted before any open and never replace a loaded session. A per-device display order keeps workspaces in place across hosts that still reorder on activation, and remembered totals keep session counts stable while readers restart. A DeviceOpenIntent describes a running cross-device open for the other owners.
The session list groups workspaces by device in a fixed order. The rendered device keeps its live list; other devices draw from their seeded containers with the live list's hierarchy, ordering, filters, disclosure and row marks, and offer the same workspace actions. Acting on another device renders it first: the clicked row and the session tab are held through the switch, and a curtain covers other switching actions so the workbench never flashes. Lists show three sessions and add five per "show more", with one step read ahead; collapsing a workspace starts it over. Device headers name the platform or CLI.
The create dialog offers a personal assistant, a project on this device, or a project on another online device. Choosing a device renders it before the same project form opens, so folders, recent workspaces and sources belong to that device. The assistant is always created on this machine.
The companion lists running, waiting and unread sessions of background devices next to the rendered device's, labelled with their device, and emits only changed payloads. Opening one renders its device first with the same held session tab as the sidebar.
Document catalog reads, freshness, background seeding and the cross-device open contract.
… resets An account change dropped background seeds but kept the remembered totals, so another account's lists could briefly show the previous "+N".
…ut actions The flat list hides a row's workspace label on hover so its actions can take the space. Rows of other devices have no actions, so the label just disappeared. Hide it only when the row has actions.
"Mark all read" cleared only the rendered device's sessions. It now also acknowledges the loaded sessions of every other device on their hosts with save_session_metadata limited to the unread and attention fields, clears their rows, statuses and companion entries at once, then re-reads the session pages. Background sessions loaded in this window pick up host read state on reseed.
… edge Bubbles are as wide as their column, which clips sideways, so the tinted drop shadow and the attention pulse ring were cut off flat at the right edge. Give the drop shadow zero horizontal reach so it fades out below the bubble, and pulse the attention ring inside the bubble's edge.
Cast the bubble drop shadow down and to the right at 45 degrees, and give the bubble column inline-end room for it (offset by a negative margin, so neither the bubbles nor the pet move).
…before the pet Widen the bubble column's inline-end room to 28px and soften the 45 degree shadow so it falls off naturally instead of stopping near the bubble.
…card The card showed "connect and use" with a link glyph for devices without a background control link and "switch" for attached ones, which looked like a difference between device kinds. Attaching is an implementation detail of the switch; every other device now shows the same switch action.
The round glyph already says what it does; the accessible name keeps the words. An incompatible device still explains why it is unavailable.
After a device switch the flat list re-reads its workspaces and inserted a "loading" row under the rows it already showed, pushing "show more" down and back. A populated list now refreshes silently; failures still show the retry button, and an empty list still shows its loading state.
A session open on another device shared the relay link with everything the switch woke up: git status, search index, file tree and watchers, mini apps, and two full session-status reconciliations. While a cross-device open runs, the peer adapter now holds low-priority requests and runs them once it settles, and status reconciliation is deferred to one pass after the open. set_active_workspace, which every session open needs, is high priority.
The host accepts an acknowledgement only when it names its own record of
the unread result (unreadCompletionTarget or last turn). Clearing a session
rebuilt that receipt from loaded turns, after the unread fields had already
been cleared, so sessions whose transcript was never loaded ("mark all
read", sidebar opens of metadata-only rows) sent no or a mismatching
receipt and came back unread after a reload.
- Keep the host's receipt on each session from metadata and activity
summaries, capture it before clearing, and send it as lastTurn and
unreadCompletionTarget.
- Background-device acknowledgements send the same receipt.
- Dismissing a companion bubble of another device acknowledges it on that
device's host instead of only on the rendered one.
…s without it - The create dialog's device list labels hosts like the sidebar: Mac, Win or Linux for clients, CLI for command-line hosts. - The workspace source selector offers WSL only where the host has it. Hosts without WSL, or whose discovery failed, show no WSL choice and no "could not load WSL distributions" error; a selected WSL falls back to the host system. Applies to local and other-device hosts alike.
- Drop sessions a background host deleted or archived from its container. - Show a device's empty or failed first read instead of the last local list. - Clear the session overlay and pending row action when an open fails. - Cap how long a cross-device open holds background requests. - Count host totals only in the unfiltered all-sessions view; read in steps. - Adopt a host order arranged on another controller. - Drop superseded session reads, share the capability ping, re-ask a host that refused a read, and poll hosts without a catalog stream. - Register the device section and switch curtain appearance contracts. - Activate the most recently used workspace after closing the current one.
A session tab used to stand for a workspace: opening another session of that workspace replaced it, and a device switch dropped every session tab. - Key session tabs by device, workspace and session, so several sessions of one workspace and sessions of other devices each keep their own tab; past 12 session tabs the least recently used idle one closes. - Keep session tabs of every device through a switch. A tab of another device shows its device name, renders that device when activated (the same open as its sidebar row), is never a pane fallback, and stays unselected when its open fails. Returning to a device reselects the tab of its active session. - Remove the per-workspace recent-sessions menu the tabs replace.
- Subscribe the session's host stream on click, while the device switch runs, and hand it with its buffered events to the session's history owner once the device is rendered. - Do not hold the switch on the device's config during an open; it reloads in the background. - Stop polling device-control status on a host that refuses it.
…on slot - This machine's home mark is the same glass disc other devices use for their action, in the same place. - A pencil after the device name renames it in place (Enter saves, Escape cancels), offered only when the Relay advertises device_alias_v1.
…m the card - Every Devices & Connections surface carries a state light: icons sit in tinted glass discs, connected cards glow faintly, and states are a dot and a word (a live link breathes) instead of filled pills. - My devices: lit device discs, this machine's home disc in the action slot, quieter row actions, and renaming in place with an underline. - Opening the dialog from the device card grows it out of the card instead of closing one surface and fading in another; the dialog code loads when the card opens.
…er-transform open - Drop the hairline rings on the device card's arrows and discs and on the dialog's icon discs, step numbers, avatar, QR frames and status dots; surfaces are separated by tint, not outlines. - The Devices & Connections dialog is lit like the card it grows from, with frosted glass cards, a quiet sidebar band without a ruled edge, a lit current page, inset row dividers and filled (not outlined) buttons. - Opening it from the card is a container transform: the dialog opens as a window of the card's size and curve that grows to its own while a still copy of the card rides its corner and fades; nothing is scaled.
Colour is kept for meaning only: no tinted page light, card glows or coloured icon discs; the current page is a neutral tint; state is a small dot. This machine is a home glyph after its name, row actions appear on hover, and secondary actions (sign out, disconnect, refresh) are quiet.
- A cross-device open reads a 48-event first page instead of a full 200-event / 768 KiB page over the relay; older pages follow in the background. Every host honours the limit. - The host dialog queue is polled every 2 s only while something is queued or running (10 s otherwise) and not while the window is hidden. - A session open's transition ends when the session shows, not at its timeout, so the work it defers resumes at once. - Activating a workspace no longer swaps the sidebar's live list for the background projection and back; creating a session ignores repeat clicks. - A selected scene tab breathes while its scene is prepared; quick waits no longer flash a spinner (scene fallback, history paging, sidebar loading); the mini-app gallery shows its skeleton while its view loads.
The scene being left stays drawn, inert, above the incoming one and fades out over it once the incoming scene is ready, instead of vanishing while the new scene faded in from half opacity. Reduced motion switches at once.
… idle - Switching workspace (and so opening a session in another workspace) shows the switch as soon as the host has switched; the recent and opened lists refresh afterwards instead of a second round trip first. - Settings' default page and the common scenes load their code while the app is idle, so the first open renders at once. - Switching between loaded sessions settles the transcript into place.
A persisted session whose history was still loading (and a session being opened) was treated as new, so the composer showed its expanded new-session layout and then collapsed when the session arrived.
One soft light from above in a single hue, the sidebar on the same lit surface, frosted glass cards with larger radii and icon discs, and round glass row actions that nudge forward on hover.
- A load that ends before the skeleton would show (it now fades in after 140 ms) reveals the transcript with one 140 ms fade instead of the full skeleton hand-over, which held a ready transcript back for 760 ms. - Slow loads hand over in 440 ms instead of 760 ms. - Resting the pointer on a local session row for 120 ms starts reading its tail, so the history is usually in memory when the click lands.
…ed tails - Opening a session in another workspace starts its history read before activating that workspace, so the two round trips overlap; the switch adopts the read already in flight. - When the history budget first evicts a long session, its latest three turns stay in memory with their catalog range, so reopening it shows them at once; older turns page in as before. Only further pressure drops it to metadata.
The glass disc around each row's chevron rendered without its glyph, as an empty circle.
When the top bar holds tabs of several devices, each device's tabs sit together behind a single quiet device label instead of every tab carrying its own device suffix. The rendered device's label reads a step brighter, groups after the first are set off by a short hairline, and each tab is described by its group's label for assistive tech.
…ne tab Create dialog: drop the step subtitles, the folders-locked helper and the 'works as it does here' wording; choice cards get soft icon discs and a hairline lift instead of an outline, devices are compact rows with an inline host tag and chevron, back buttons are quiet, and the primary action no longer repeats the device name from the title. Scene tabs: the close button no longer carries a hover tooltip, and a hovered tab that grows past the scroller's edge is scrolled fully into view, so the last tab's close action stays reachable when the bar overflows.
The device card's light was purple on this device and blue on a device not yet connected. Both now share one neutral silver drawn from the secondary content colour, so it follows light and dark themes; only a live connection keeps its green, and an incompatible device its amber. The Devices & Connections dialog's ambient light uses the same silver.
…ll arrow Hovering a tab grows it by its close action. On the last tab that growth widened the strip, so the forward scroll arrow reappeared on top of the close button. Reserve the growth once at the strip's end and let the last tab grow into it, so the strip's width never changes on hover; tabs that grow past the visible edge measure against the arrow when it shows.
Opening the composer's + menu refreshes the mode catalog, which publishes new mode objects. The target-mode tool effect keyed on that object, so it cleared the resolved tools and refetched them, hiding 'Set session goal' until the profile config returned. Key the effect on the mode's id, source and default tools, refetch on explicit config events, and keep the last known tools while the same mode re-resolves.
bobleer
force-pushed
the
bob/sidebar-device-workspaces
branch
from
November 4, 2026 09:03
4ccf944 to
9a05508
Compare
…receive ack' The Relay always answers an acknowledged event, so a dropped ack means the controller's own socket closed mid-call, which repeated device connect and disconnect can do. The socket library surfaced that as 'failed to receive ack', which reached the notification center verbatim. Map it to a stable RELAY_REPLY_LOST message, recognise it (and the legacy text) in the Web UI, and show a localized 'connection interrupted, try again' instead.
A peer session's first page held the newest 48 records, which could cut the oldest turn in half. The automatic older page then rebuilt that turn in place a few seconds after open, so the visible list jolted. The limit is now a budget: a page grows back to its oldest turn's header within the existing event and byte bounds, so no turn splits across pages and older pages continue exactly before it. Older controllers accept larger pages.
…er round trips The sidebar's new-session button swallowed repeat clicks behind a hidden ref and gave no feedback while the session was created. It now turns busy and disabled, and shows a spinner once the wait passes 140 ms. Creation reads the project defaults once and shares them with the driver, sends the mode-resolution reads together, resolves the draft's reasoning preset and token limit together, and syncs an assistant session's title after it is shown. On another device each of these was a relay round trip.
Session rows listed under a device the window is not rendering had no more-actions button. They now carry the same button and menu as live rows (rename, copy, export, open worktree, archive; restore and delete when archived). Each action renders the owning device first, then runs the shared handler, now extracted to sessionRowActions; a superseded switch runs nothing, a missing session fails loudly, and actions the device cannot serve stay disabled with their reason. A lost relay reply on these rows reads as a connection interruption.
A desktop controlling a CLI host could not open the composer's context breakdown: the CLI peer host had no get_session_usage_report handler, so the panel failed and the raw refusal reached a notification. The CLI host now builds the report through the same runtime port the desktop uses, and the registry marks the command handled. Against an older CLI host the popover, preview and report panel show a calm unsupported or update-required state instead of an error toast.
…workspaces # Conflicts: # src/crates/contracts/product-domains/src/generated/remote-surface-registry.json # src/web-ui/src/infrastructure/api/generated/remoteSurface.ts
…e tail When an older page arrived while follow-output held the viewport, the prepend compensation was refused and only the follow loop was woken. That left one painted frame at the old offset, with the new history pushing the reader's Turn down before the next frame snapped it back: the jolt a few seconds after opening a session. Apply the displacement before paint as follow-output itself, then wake the loop to keep its own target.
The add menu's additional-modes item is built from the mode's skills, which were fetched only when the menu opened, so it arrived after the rest of the menu and pushed it down. Warm the skills shortly after the composer shows, or when the pointer reaches the add button, and revalidate them in place on each opening.
…tches Leaving a device could show its projects empty under a known session total until the catalog re-read the host: seeding marked the rendered device's catalog pages as projected without projecting them. The left device is now filled at the moment of the switch from the pages the catalog already holds (only sessions its container lacks), and the background list shows its loading row, not 'no sessions', while a page is unread or its listed rows are not in the container yet; 'show more' appears only under real rows. The live list and the background catalog also disagreed on workspace order: the live list kept its own previous order, and several activations on an older host between two reads looked like a deliberate rearrangement. Both views now share one display order, judged against the shown order and the host's last order with this window's activations applied.
…test On macOS CI the final reserve after dropping the last lease failed once. Other tests in the binary spawn processes, and a child forked while the lock file is open shares its open file description until it execs, so the flock release can trail the drop. Retry the reserve briefly; the assertions that a held lease blocks a replacement are unchanged.
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.
Summary
Lists the workspaces and sessions of every online same-account device in the sidebar, in place, and lets users act on them as if they were local. Acting on another device renders it through the existing Peer Device Mode switch first, so capability gates, permission mailboxes and surface fences are unchanged.
infrastructure/peer-device/deviceWorkspaceCatalog.ts): lazily retained, read-only reads ofget_opened_workspaces/list_persisted_sessions_pageover raw HostInvoke; kept fresh by debounced@host/cataloghints (session-only hints skip the workspace list; held while the window is hidden or a cross-device open runs). No polling.workspace/manager.rs): activating an already opened workspace no longer moves it to the front of the opened list (it reordered the sidebar and overwrote user ordering).Commits are split by layer: host fix, catalog/state, sidebar UI, create dialog, companion, docs.
Remote scenarios
workspace_id_references_v1.Verified with unit and component tests only; not exercised on real multiple devices by the author.
Upgrade compatibility
host_stream_v1fall back to visibility refresh; older CLI hosts (noload_persisted_session_metadata) open sessions from the catalog record.openbitfun.sidebar-device-sections.v1, device section disclosure); workspace disclosure and totals stay in memory and reset with the account.Test plan
tsc --noEmit,eslint .(0 errors)cargo test -p openbitfun-core --features workspace-runtime --lib service::workspacei18n:audit,theme:color-audit:all,check:repo-hygiene,check:core-boundaries