Skip to content

feat: detachable plugin windows as a bundled Windows plugin - #102

Draft
thomaspblock wants to merge 11 commits into
mainfrom
feat/detachable-plugin-windows
Draft

thomaspblock wants to merge 11 commits into
mainfrom
feat/detachable-plugin-windows

Conversation

@thomaspblock

@thomaspblock thomaspblock commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Desktop windows become browser-like tab containers. Any page (Messages, Projects, Agents, Workflows, external plugins) or launcher panel (Bestie, Agent Activity) can leave the main window into its own OS window, and windows can hold any combination of tabs. Home and Settings stay in main. The feature ships as the bundled Windows plugin (buzz.windows), so it has a toggle in Settings → Plugins like Terminal.

  • Move by drag or menu. Drag a tab or launcher out of the strip: past 6px the tab lifts out of its slot into a native always-on-top pill (same size, icon and resolved styles as the tab, follows the app theme) that stays visible across the whole screen. Drop on another Buzz window to merge, anywhere else to open a new window under the pointer; release back in the strip to cancel. Right-click offers Move to new window / main window / Window N. The hovered window highlights where the tab will land (strip for pages, launcher row for panels), is focused on drop, and selects the moved tab.
  • One place at a time. A tab lives in exactly one window. Closing a detached window returns its tabs to main; closing main quits. Layout is persisted per profile (<profile>/windows.json) and restored on launch. Disabling or removing a plugin removes its tab wherever it lives; an emptied window shows an explanation with Close window.
  • Every window runs the full app, rendering only its assigned tabs. Only main raises desktop notifications; detached windows follow the community selected in main.
  • Plugin switch. Rust owns the windowing runtime (src-tauri/src/windows.rs); the new ctx.windows capability exposes the layout to plugins and enable(). The Windows plugin is ctx.effect(() => ctx.windows.enable()). Switching it off hides menus/drag and gathers every tab back into main; a disabled plugin at launch discards the saved layout. App teardown never resets.

Design and decisions: docs/plans/2026-09-14-001-feat-detachable-plugin-windows-plan.md, docs/shell-design.md (Detached tab windows), docs/plugin-architecture.md (Desktop tab windows).

Demo

Video — dragging tabs and Bestie between windows:

windowmagic.mp4

Settings → Plugins with the Windows plugin:

Settings → Plugins showing the Windows plugin toggle

Reviewer notes

  • Tabs render the shared NavigationItem pill and launchers the chrome IconButton disc from main; PageTab adds the move menu and pointer drag on top of them. Window commands are generic over the Tauri runtime so they sit in the shared commands() handler.
  • FOUNDATION files touched, additively: src/app/services.ts (compose WindowsService, bind notifications only in main), src/plugins/author.ts (export Windows, WindowLayout, WindowSnapshot), src/app/App.tsx.
  • tauri.conf.json turns on macOSPrivateApi (with the matching Cargo feature) for the transparent rounded drag pill. Apple rejects that API for App Store submission; bundling is off in this repo, and the fallback is an opaque pill.
  • Capabilities: tabs-* windows get the same permissions as main plus event listen/unlisten and window close. The drag-ghost window has no capability and no IPC.
  • Not included (documented as later phases): restoring window positions/sizes, reordering tabs within a strip, z-order awareness when windows overlap, moving tabs from plugin code.

Test plan

Automated (all green on the head, macOS): Biome, tsc, design-foundation checks, frontend build, Vitest 1635/1635 (layout filtering, web fallback, drag coalescing, activation, drop-target, lifted-tab ghost, plugin enable/reset/teardown, live toggle in the app integration test), Clippy -D warnings, Rust layout/persistence/reset tests, browser journeys 411/418 in Chromium and WebKit.

  • CI: all checks green, including both WebKit browser shards. (Locally on my Mac, tests/browser/emoji.spec.mjs fails on WebKit on main as well — a local environment difference, not this branch.)
  • Known local-only: terminal::tests::production_environment_probe fails on machines whose login shell prints a banner; module unchanged.

Manual, desktop build (bin/just desktop):

  • Drag Messages out to a new window; drag Agents onto it; close it with the traffic light; both are back in main.
  • Drag Bestie onto a detached window: the launcher row highlights, Bestie lands there and opens as the companion; drag it back to main.
  • Relaunch: the same distribution of tabs comes back.
  • A mention while Messages is detached raises exactly one notification.
  • Settings → Plugins → Windows off with two detached windows open: they close, tabs return, no move menu/drag. On again: dragging works.
  • Quit with windows detached, disable the plugin, relaunch: nothing restored.
  • Multi-display: drop coordinates land on the right screen (deferred check from the plan).

Desktop windows act as browser-like tab containers: any page or launcher
panel (Bestie, Agent Activity) can move to a new or existing window by
right-click menu or by dragging, while Home and Settings stay in main.
Rust owns the layout (persisted per profile), creates, restores and
closes windows, hit-tests drops, and drives a native always-on-top drag
ghost that stays visible beyond the source window and follows the app
theme. The destination window is focused, highlights while hovered and
selects the moved tab. Each window runs the full app on its assigned
tabs; only main raises notifications, and detached windows follow the
community selected in main.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
…n-windows

Signed-off-by: Thomas Petersen <thomasp@squareup.com>

# Conflicts:
#	src/app/navigation.ts
#	src/app/shell/AppShell.tsx
#	src/app/shell/PanelLaunchers.tsx
…ding spot

Dragging now feels continuous: past the threshold the tab blanks in its
slot and the native pill appears exactly where it was, same size, icon and
resolved styles read from the live DOM, keeping the grab offset. Glass
launchers lift as an opaque icon disc and their images no longer start a
native drag. A hovered window is told which tab is coming, so main rings
its launcher row for a returning panel and the strip for pages.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
A panel dropped on a detached window landed in its tab strip while the
drop highlight pointed at the strip too. Launcher panels now live in the
right-hand launcher row wherever they are and open as the companion card
there; a detached window without pages shows its panel full-size and that
view's close returns it to main. The strip no longer renders panel tabs.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
…n-windows

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
The windowing runtime stays host-owned; the new bundled Windows plugin
(buzz.windows) is its switch, so it appears in Settings -> Plugins like
Terminal. A ctx.windows capability exposes the window layout to plugins and
enable(): the plugin's whole body is ctx.effect(() => ctx.windows.enable()).
Switching it off hides move menus and drag and gathers every tab back into
main; a disabled plugin at launch discards the saved layout instead of
restoring windows. App teardown never resets. Also tolerate a Tauri runtime
without window metadata (browser fixtures) and move two shell classes onto
design-system text roles and a solid surface.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
@thomaspblock
thomaspblock requested review from a team, comp615 and wesbillman as code owners September 20, 2026 18:46
@thomaspblock

Copy link
Copy Markdown
Contributor Author
windows-plugin-settings
windowmagic.mp4

…n-windows

Signed-off-by: Thomas Petersen <thomasp@squareup.com>

# Conflicts:
#	crates/plugin-manager/src/lib.rs
#	src/app/shell/AppShell.tsx
#	src/bundled/index.ts
Since the reading fixture shrank to 20 tall messages (#83), the row above
the anchored one can be taller than a single 300px wheel step, so the first
wholly visible paragraph stayed the same message and "panel restoration
yields to a new wheel reading position" failed on main in both engines.
Keep making bounded, verified wheel progress until the visible reading row
belongs to another message, reusing the shared anchor reader.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
(cherry picked from commit 664b27e)
…n-windows

Adopt the shared shell controls for detachable tabs: PageTab now renders
NavigationItem pills for pages and chrome IconButton discs for launchers,
the drag ghost borrows the selected pill surface, and the window commands
are generic over the Tauri runtime so they join the shared commands()
handler used by the MockRuntime tests. Only main binds the Dock unread
badge, like notifications.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>

Conflicts:
	Cargo.lock
	src-tauri/Cargo.toml
	src-tauri/src/lib.rs
	src/app/App.tsx
	src/app/services.ts
	src/app/shell/AppShell.tsx
	src/app/shell/PanelLaunchers.tsx
	src/plugins/author.ts
…n-windows

Keep the macOS title-bar double-click handlers on the header, actions row
and launcher group of every window, and note in shell-design that the
behaviour and the window capability cover detached tabs-* windows too.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>

Conflicts:
	docs/shell-design.md
	src-tauri/src/lib.rs
	src/app/shell/AppShell.tsx
…n-windows

The global search palette stays in the main window's launcher row, like the
page finder it replaces; detached windows keep only their launchers.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>

Conflicts:
	src/app/App.tsx
	src/app/shell/AppShell.tsx
@wesbillman
wesbillman marked this pull request as draft September 28, 2026 21:12
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