Skip to content

Device-local tabs + spaces as a searchable sidebar filter - #14

Open
wingleeio wants to merge 9 commits into
mainfrom
tabs-spaces-restructure
Open

Device-local tabs + spaces as a searchable sidebar filter#14
wingleeio wants to merge 9 commits into
mainfrom
tabs-spaces-restructure

Conversation

@wingleeio

@wingleeio wingleeio commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Restructures the shell around wing's proposal: tabs stop being synced session state, spaces stop being a navigation spine.

What changed

Tabs are now device-local. A tab is a local viewport onto a synced session (ui-settings.jsonopenTabs; the vec is the drag order). Closing a tab is local-only — the session keeps running on its host and stays in the sidebar; clicking a sidebar row (re)opens it as a tab. Archiving is a separate, explicit action in the session row's context menu (unchanged — it was already there). The strip is cross-space and gets a context menu: close, close others, close to the right, close to the left (rows that would close nothing are omitted). Existing installs migrate by seeding openTabs from the last selected space's sessions, so nobody's tabs vanish on upgrade.

Spaces are a searchable dropdown (same recipe as the ref picker: ranked substring search, keyboard nav) that filters only the sidebar session list. "All spaces" is the first row; "Add space…" (the existing ⌘K palette) is the last; rename/remove live on row context menus inside the dropdown.

New sessions. + opens the canvas defaulting to the sidebar's filter space — or the last selected space under "All". The composer gains a space chip on the canvas only; picking a space re-keys the ref selector, worktree plan, and harness/model catalogs through the existing selected_space observer (no new invalidation paths).

The synced Loro doc schema is untouched — this is purely a view-layer restructure.

Screenshots

Cross-space tab strip; sidebar sessions with the dropdown trigger on top:

overview

The spaces dropdown — searchable, "All spaces", device tags, "Add space…":

dropdown

search

Filtered to soccertcg — note the two comet-space tabs stay open (the filter never touches the strip):

filtered

New-session canvas defaults to the filter space, with the new space chip:

canvas

The canvas space picker, and the ref footer re-keying after switching to a git space (Current checkout / main appears because comet has git; soccertcg didn't):

picker

re-keyed

Tab context menu (last tab → no "close to the right"):

tab menu

Demo video

Filter → open-from-sidebar → + with space pick → send (mock harness streams) → close the streaming tab locally (session keeps running in the sidebar) → reopen with transcript intact → close others:

demo

MP4 version · assets live on the assets/tabs-spaces branch, not in this diff.

Verification

  • cargo test --workspace green (settings round-trip covers the new fields; tab-neighbor logic keeps its unit tests).
  • Exercised end-to-end against a seeded mock engine on a headless compositor — every capture above is the real app.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@wingleeio

Copy link
Copy Markdown
Contributor Author

Follow-up from wing's review: the filter trigger's caret now sits at the right edge of the row, and long space names truncate against it (label flexes; caret is flex_none).

truncated trigger

(The dropdown rows and session-row sublines already truncated against their @ device tags — verified in the same pass.)

wing-anara and others added 2 commits August 4, 2026 08:42
Tabs detach from session existence: a tab is a device-local viewport
onto a synced session (UiSettings.openTabs, the vec is the order).
Closing a tab is local-only — the session keeps running and stays in
the sidebar, where a click reopens it; archiving stays an explicit
sidebar-row action. The strip goes cross-space and gains a context
menu (close / close others / close left / right). Old files migrate
by seeding openTabs from the last space's sessions.

Spaces stop being a navigation spine: the sidebar section becomes a
searchable dropdown (ref-picker recipe) with "All spaces", filtering
only the session list and hosting space management. The new-session
canvas gains a space picker chip defaulting to the sidebar filter,
else the last selected space; picking a space re-keys the ref
selector and harness/model catalogs through the existing
selected_space observer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The label now flexes, so long space names ellipsize against the
trigger width instead of pushing the caret out of the row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wingleeio
wingleeio force-pushed the tabs-spaces-restructure branch from d9f7abf to de7f886 Compare August 4, 2026 08:44
anchored_menu pins to the trigger's top-left, which covered the
button. New anchored_menu_below primitive pins the floating layer to
the trigger's bottom edge — a dropdown proper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wingleeio

Copy link
Copy Markdown
Contributor Author

Dropdown now opens below the trigger instead of covering it (new anchored_menu_below popover primitive pinning to the trigger's bottom edge):

dropdown below

The selected row's wash (menu_row active styling) is the selection
signal; the trailing check fought the @device tag for the row end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wingleeio

Copy link
Copy Markdown
Contributor Author

Check glyph removed from the space-filter dropdown — the selected row's wash carries the selection signal:

no check

The selected row's wash (menu_row active styling) is the selection
signal everywhere — ref/checkout/model pickers, the canvas space
picker, and the account device menu — matching the space-filter
dropdown. menu_check retires with its last caller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wingleeio

Copy link
Copy Markdown
Contributor Author

Wash-only selection is now consistent across all menus — removed the check glyph from the ref, checkout, and model pickers, the canvas space picker, and the account device menu (popover::menu_check deleted with its last caller). The current/worktree/@ device tags get the row end to themselves:

space picker
ref picker

wingleeio and others added 4 commits August 4, 2026 11:38
…-down

Dropdowns/menus previously vanished the frame their state dropped. Every
menu state is now a popover::Popup<T> with an exit phase: close paths
funnel through a begin_close that keeps the card mounted ~100ms playing
menu-out (fade + 2px lift, interactivity deadened), then a reap timer
drops the state.

The exit progress reads the Popup's closing instant at render time
rather than with_animation's element-state clock — that clock replays
from 0 on a missed frame, which flashed the dying menu back to full
opacity. The frosted backdrop blur ignores element_opacity, so its
radius rides the same progress down to zero instead of popping off at
unmount.

Also: clicking an open trigger now just closes its menu (the outside-
click dismissal no longer immediately reopens via the same click), the
same just-dismissed guard the user menu already carried.

Converted: spaces filter dropdown, user menu, session/tab/space context
menus, composer pickers, settings device menu. The @-mention completion
stays instant on purpose (keystroke-driven dismissal would read as lag).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The host device was only visible inside the space pickers' rows. The
sidebar filter trigger now shows it right after the space name (caret
stays at the right edge), and the composer's space chip shows it as the
muted suffix — amber "· offline" when the host's heartbeat is stale,
same as the rows. Formatting shared via AppState::space_device_tag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both the sidebar filter dropdown and the composer space picker listed
spaces in sync-arrival order. They now share AppState::spaces_sorted
(case-insensitive by display name, id tie-break): search still ranks
prefix matches first — alphabetical within each rank — and the picker's
opening highlight indexes the sorted order.

Co-Authored-By: Claude Fable 5 <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.

2 participants