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:
2026-10-05 09:37:48 +07:00
parent 65fe680d38
commit ad6e80192d
80 changed files with 2914 additions and 489 deletions
@@ -5,9 +5,10 @@
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { render, screen, fireEvent, within } from "@testing-library/react";
// render is used inside renderCard helper
import { MemoryRouter } from "react-router-dom";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import userEvent from "@testing-library/user-event";
import { AgentCard } from "../AgentCard";
import type { Agent } from "../../lib/types";
import { formatModelName, fmtCost } from "../../lib/format";
@@ -325,4 +326,42 @@ describe("AgentCard", () => {
);
expect(screen.getByText(/ran 5m 30s/)).toBeInTheDocument();
});
it("right-click opens the session menu; its items act without clicking the card", async () => {
function Where() {
const loc = useLocation();
return <span data-testid="where">{loc.pathname + loc.search}</span>;
}
const onClick = vi.fn();
render(
<MemoryRouter initialEntries={["/"]}>
<Routes>
<Route
path="*"
element={
<>
<AgentCard
agent={makeAgent()}
session={{ id: "sess-1", lane_id: 4, cwd: "/r/app" } as never}
onClick={onClick}
/>
<Where />
</>
}
/>
</Routes>
</MemoryRouter>
);
const user = userEvent.setup();
await user.pointer({ keys: "[MouseRight]", target: screen.getByText("Main Agent") });
const menu = screen.getByTestId("session-row-menu-sess-1");
await user.click(within(menu).getByText("Copy folder path"));
expect(await navigator.clipboard.readText()).toBe("/r/app");
expect(onClick).not.toHaveBeenCalled();
await user.pointer({ keys: "[MouseRight]", target: screen.getByText("Main Agent") });
await user.click(screen.getByText("Open in its workspace"));
expect(screen.getByTestId("where")).toHaveTextContent("/workspace?lane=4&session=sess-1");
expect(onClick).not.toHaveBeenCalled();
});
});