feat: right-click menus everywhere, workspace/session cost, long-term DB safety
Right-click menus: one portal shell (components/ContextMenu.tsx) for every item with actions - sidebar/Recents/archived session rows (SessionMenu), console tabs, pane headers, the Changes panel's files/tree/commits, the workspace home, sessions outside a workspace (SessionRowMenu: Sessions table, Dashboard, Activity, Workflow runs), prompt images, every Settings list and every Claude Code/Codex config list (FileItemMenu). Buttons stay; a delete keeps its button's confirm; the shell stops portal clicks from reaching the card that opened it. Status bar: Workspace $ and Session $ on the Workspace page, from ?lane= on GET /api/pricing/cost and the focused/replayed session's cost. Database, no data dropped: synchronous=NORMAL, journal_size_limit 64 MB, hourly PASSIVE checkpoint + PRAGMA optimize, idx_events_session_created (replacing the redundant idx_events_session), watchdog reads sessions.transcript_path, transcript parsed before the hook's write transaction, GET /api/sessions/:id?events=0 for the web page (reloads only on its own session, debounced), streamed export, transactional clear-data and chunked cleanup, subagent import reads tool_use_ids once, runtime logs rotated past 5 MB.
This commit is contained in:
@@ -71,6 +71,7 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
items: [{ id: "agenthub-lane-1-2", laneId: 1, status: "running", sessionId: "s-live" }],
|
||||
})),
|
||||
cwds: vi.fn(async () => ({ items: [] })),
|
||||
kill: vi.fn(async () => ({ ok: true })),
|
||||
},
|
||||
aiTools: {
|
||||
list: vi.fn(async () => ({ tools: [], default: "claude" })),
|
||||
@@ -163,6 +164,40 @@ describe("WorkspaceTree", () => {
|
||||
expect(api.sessions.patch).toHaveBeenCalledWith("s-live", { archived: true });
|
||||
});
|
||||
|
||||
it("right-click on a session opens its menu: split, rename, archive, close its tab", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderTree();
|
||||
await user.click(await screen.findByTestId("workspace-toggle-1"));
|
||||
const right = (target: Element) => user.pointer({ keys: "[MouseRight]", target });
|
||||
|
||||
await right(await screen.findByTestId("workspace-session-s-old"));
|
||||
let menu = screen.getByTestId("session-menu-s-old");
|
||||
// Ended: no pane to split or close.
|
||||
expect(within(menu).queryByText("Open in split")).toBeNull();
|
||||
expect(within(menu).queryByText("Close tab (stops its session)")).toBeNull();
|
||||
await user.click(within(menu).getByText("Rename"));
|
||||
expect(screen.queryByTestId("session-menu-s-old")).toBeNull();
|
||||
expect(screen.getByTestId("workspace-session-rename-s-old")).toBeInTheDocument();
|
||||
await user.keyboard("{Escape}");
|
||||
|
||||
await right(screen.getByTestId("workspace-session-s-live"));
|
||||
menu = screen.getByTestId("session-menu-s-live");
|
||||
await user.click(within(menu).getByText("Open in split"));
|
||||
expect(screen.getByTestId("where")).toHaveTextContent(
|
||||
"/workspace?lane=1&run=agenthub-lane-1-2"
|
||||
);
|
||||
|
||||
await right(screen.getByTestId("workspace-session-s-live"));
|
||||
await user.click(within(screen.getByTestId("session-menu-s-live")).getByText("Archive"));
|
||||
expect(api.sessions.patch).toHaveBeenCalledWith("s-live", { archived: true });
|
||||
|
||||
await right(screen.getByTestId("workspace-session-s-live"));
|
||||
await user.click(
|
||||
within(screen.getByTestId("session-menu-s-live")).getByText("Close tab (stops its session)")
|
||||
);
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-2");
|
||||
});
|
||||
|
||||
it("archiving a worktree session removes its worktree first, after asking", async () => {
|
||||
vi.mocked(api.sessions.list).mockImplementation(async () => ({
|
||||
sessions: [
|
||||
|
||||
Reference in New Issue
Block a user