feat(workspace): session view console as tabs, with New terminal
The 1/2/4 split view becomes a tab bar (ConsoleTabs.tsx): one tab per live tmux slot (0-3) of the workspace, the active one being ?run=, every live tab's pane kept mounted and hidden with CSS. The + menu opens a new tab in the first free slot: New terminal (a plain shell - shell: true on POST /api/lanes/:id/start, spawnShell, tool 'shell', no run_id/status change), any installed enabled AI tool started bare, or Settings -> Agents. A tab's x stops its session. splitViewStorage, &pane= and the sidebar's Ctrl/Cmd+click are gone.
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* @file splitViewStorage.test.ts
|
||||
* @description Tests for the splitViewStorage module.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import {
|
||||
readSplitViewState,
|
||||
writeSplitViewState,
|
||||
defaultSplitViewState,
|
||||
} from "../splitViewStorage";
|
||||
|
||||
describe("splitViewStorage", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("returns the default state when nothing is stored", () => {
|
||||
expect(readSplitViewState()).toEqual(defaultSplitViewState());
|
||||
});
|
||||
|
||||
it("defaults to a single unselected pane", () => {
|
||||
expect(defaultSplitViewState()).toEqual({ layout: 1, paneLaneIds: [null] });
|
||||
});
|
||||
|
||||
it("round-trips a written state", () => {
|
||||
writeSplitViewState({ layout: 4, paneLaneIds: [1, 2, null, null] });
|
||||
expect(readSplitViewState()).toEqual({ layout: 4, paneLaneIds: [1, 2, null, null] });
|
||||
});
|
||||
|
||||
it("falls back to the default when stored JSON is malformed", () => {
|
||||
localStorage.setItem("agenthub.workspace.splitView", "{not json");
|
||||
expect(readSplitViewState()).toEqual(defaultSplitViewState());
|
||||
});
|
||||
|
||||
it("falls back to the default when the stored layout is not 1, 2, or 4", () => {
|
||||
localStorage.setItem(
|
||||
"agenthub.workspace.splitView",
|
||||
JSON.stringify({ layout: 3, paneLaneIds: [] })
|
||||
);
|
||||
expect(readSplitViewState()).toEqual(defaultSplitViewState());
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
/**
|
||||
* @file laneOrderStorage.ts
|
||||
* @description Persists the Workspace lane strip's user-chosen card order to
|
||||
* localStorage, the same read/validate/fallback convention as
|
||||
* splitViewStorage.ts.
|
||||
* localStorage, the same read/validate/fallback convention as useTheme.ts.
|
||||
*
|
||||
* The order lives in the browser rather than the lanes table on purpose: it is
|
||||
* a per-viewer preference, like the split-pane layout beside it, and nothing
|
||||
* a per-viewer preference, and nothing
|
||||
* server-side (the CLI's `agenthub lanes`, the pipeline-stats picker) has an
|
||||
* opinion about it. Ceiling: the order does not follow the user to another
|
||||
* browser or to the CLI — move it to a `lanes.sort_order` column if it ever
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* @file splitViewStorage.ts
|
||||
* @description Persists the Workspace page's split-terminal layout (1/2/4
|
||||
* panes) and each pane's chosen lane id to localStorage, so the layout
|
||||
* survives a page reload. Follows the same read/write-with-fallback
|
||||
* convention as useTheme.ts.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
export type SplitLayout = 1 | 2 | 4;
|
||||
|
||||
export interface SplitViewState {
|
||||
layout: SplitLayout;
|
||||
paneLaneIds: (number | null)[];
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "agenthub.workspace.splitView";
|
||||
|
||||
export function defaultSplitViewState(): SplitViewState {
|
||||
return { layout: 1, paneLaneIds: [null] };
|
||||
}
|
||||
|
||||
function isValidLayout(value: unknown): value is SplitLayout {
|
||||
return value === 1 || value === 2 || value === 4;
|
||||
}
|
||||
|
||||
function isValidState(value: unknown): value is SplitViewState {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const v = value as Record<string, unknown>;
|
||||
return (
|
||||
isValidLayout(v.layout) &&
|
||||
Array.isArray(v.paneLaneIds) &&
|
||||
v.paneLaneIds.every((id) => id === null || typeof id === "number")
|
||||
);
|
||||
}
|
||||
|
||||
export function readSplitViewState(): SplitViewState {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return defaultSplitViewState();
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
return isValidState(parsed) ? parsed : defaultSplitViewState();
|
||||
} catch {
|
||||
return defaultSplitViewState();
|
||||
}
|
||||
}
|
||||
|
||||
export function writeSplitViewState(state: SplitViewState): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch {
|
||||
/* ignore quota / disabled storage */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user