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>
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { useAppState, homeTabKey, terminalTabKey } from "./appState";
|
|
|
|
const A = homeTabKey("a");
|
|
const B = terminalTabKey("b");
|
|
const C = terminalTabKey("c");
|
|
|
|
function seed(tabOrder: string[], activeTabKey: string | null = null) {
|
|
useAppState.setState({
|
|
tabOrder,
|
|
activeTabKey,
|
|
activeSessionId: null,
|
|
selectedProjectId: null,
|
|
});
|
|
}
|
|
|
|
const order = () => useAppState.getState().tabOrder;
|
|
|
|
describe("tab reordering", () => {
|
|
beforeEach(() => seed([A, B, C]));
|
|
|
|
it("moves a tab to an earlier slot", () => {
|
|
useAppState.getState().moveTab(C, 0);
|
|
expect(order()).toEqual([C, A, B]);
|
|
});
|
|
|
|
it("moves a tab to a later slot", () => {
|
|
useAppState.getState().moveTab(A, 2);
|
|
expect(order()).toEqual([B, C, A]);
|
|
});
|
|
|
|
it("clamps a destination past the ends rather than dropping the tab", () => {
|
|
useAppState.getState().moveTab(A, 99);
|
|
expect(order()).toEqual([B, C, A]);
|
|
useAppState.getState().moveTab(A, -5);
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("ignores a tab that isn't in the strip", () => {
|
|
useAppState.getState().moveTab("term:gone", 0);
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("does not change what's active — dragging a tab is not selecting it", () => {
|
|
seed([A, B, C], B);
|
|
useAppState.getState().moveTab(C, 0);
|
|
const state = useAppState.getState();
|
|
expect(state.tabOrder).toEqual([C, A, B]);
|
|
expect(state.activeTabKey).toBe(B);
|
|
});
|
|
|
|
it("nudges the active tab with the keyboard, in both directions", () => {
|
|
seed([A, B, C], B);
|
|
useAppState.getState().moveActiveTab(-1);
|
|
expect(order()).toEqual([B, A, C]);
|
|
useAppState.getState().moveActiveTab(1);
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("stops the active tab at the ends instead of wrapping it around", () => {
|
|
seed([A, B, C], A);
|
|
useAppState.getState().moveActiveTab(-1);
|
|
// A held-down key must not teleport the tab to the far end.
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("does nothing when no tab is active", () => {
|
|
seed([A, B, C], null);
|
|
useAppState.getState().moveActiveTab(1);
|
|
expect(order()).toEqual([A, B, C]);
|
|
});
|
|
|
|
it("keeps Ctrl+1..9 addressing the strip as reordered", () => {
|
|
seed([A, B, C], A);
|
|
useAppState.getState().moveTab(C, 0);
|
|
useAppState.getState().focusTabIndex(0);
|
|
expect(useAppState.getState().activeTabKey).toBe(C);
|
|
});
|
|
});
|