Reorder tabs by dragging, and pop the browser view into its own window
Build App / compute-version (pull_request) Successful in 3s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-linux (pull_request) Successful in 5m35s
Build App / build-windows (pull_request) Successful in 6m9s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
Build App / compute-version (pull_request) Successful in 3s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-linux (pull_request) Successful in 5m35s
Build App / build-windows (pull_request) Successful in 6m9s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
Two things the UI couldn't do: rearrange the tab strip, and watch the browser while working somewhere else. **Drag to reorder.** `moveTab`/`moveActiveTab` on the store, HTML5 drag on the strip with a marker showing where the drop lands, `Ctrl+Shift+←/→` for the same thing without a mouse. Reordering deliberately does not select what it moves, so a drag aimed at a background tab doesn't yank the main area away from a terminal mid-run. A tab being renamed is not draggable — a draggable ancestor swallows the mouse-drag that selects text in its input. **Pop the browser view out.** `browser_view/popout.rs` opens the view's existing token-bearing loopback URL as a second OS window, with a "Keep on top" toggle so it can float above the app. Window-only: the viewer, the proxy and the container are untouched, so popping out and back interrupts nothing. Three things it rests on: - No capability lists that window, so it has no IPC surface — right for a page served out of a container, and it must stay that way. - The app CSP is irrelevant to it: `frame-src` constrains what the app's document may *embed*, and this is a top-level document. The port range and the token gate are what actually protect it, unchanged. - The window is owned by the session, so the supervisor's teardown closes it. A window onto a viewer that no longer exists is worse than none. The pane drops its iframe while popped out — two viewers can both *drive* the browser, and two cursors on one page is not a feature. `lib.rs`'s `on_window_event` is now guarded on `label() == "main"`. It fires for every window and its body stops every container and exits, so without the guard closing a pop-out would quit the app. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,10 @@ interface AppState {
|
||||
setActiveTabKey: (key: string) => void;
|
||||
cycleTab: (delta: number) => void;
|
||||
focusTabIndex: (index: number) => void;
|
||||
/** Reorder: put `key` at `toIndex` in the strip. Never changes what's active. */
|
||||
moveTab: (key: string, toIndex: number) => void;
|
||||
/** Nudge the active tab left/right — the keyboard route to the same thing. */
|
||||
moveActiveTab: (delta: number) => void;
|
||||
|
||||
// Inline container progress, replacing the blocking progress modal.
|
||||
containerProgress: Record<string, string>;
|
||||
@@ -274,6 +278,35 @@ export const useAppState = create<AppState>((set) => ({
|
||||
? { ...patch, selectedProjectId: tabKeyId(key) }
|
||||
: patch;
|
||||
}),
|
||||
// Reordering is deliberately *only* a reordering: dragging a tab does not
|
||||
// select it, so a drag can be aimed at a background tab without yanking the
|
||||
// main area (and a running terminal's focus) away mid-gesture.
|
||||
moveTab: (key, toIndex) =>
|
||||
set((state) => {
|
||||
const from = state.tabOrder.indexOf(key);
|
||||
if (from === -1) return {};
|
||||
const to = Math.max(0, Math.min(toIndex, state.tabOrder.length - 1));
|
||||
if (from === to) return {};
|
||||
const tabOrder = [...state.tabOrder];
|
||||
tabOrder.splice(from, 1);
|
||||
tabOrder.splice(to, 0, key);
|
||||
return { tabOrder };
|
||||
}),
|
||||
moveActiveTab: (delta) =>
|
||||
set((state) => {
|
||||
const key = state.activeTabKey;
|
||||
if (!key) return {};
|
||||
const from = state.tabOrder.indexOf(key);
|
||||
if (from === -1) return {};
|
||||
// Clamped, not wrapped: a tab dragged off the end would otherwise
|
||||
// reappear at the other end, which reads as a bug on a held-down key.
|
||||
const to = Math.max(0, Math.min(from + delta, state.tabOrder.length - 1));
|
||||
if (from === to) return {};
|
||||
const tabOrder = [...state.tabOrder];
|
||||
tabOrder.splice(from, 1);
|
||||
tabOrder.splice(to, 0, key);
|
||||
return { tabOrder };
|
||||
}),
|
||||
|
||||
// Container progress
|
||||
containerProgress: {},
|
||||
|
||||
Reference in New Issue
Block a user