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:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @file SessionMenu — a session row's right-click menu (sidebar, the workspace
|
||||
* home's Recents, the archived fold), and a bare live tab's (`session` null —
|
||||
* the sidebar's **New session** row). Every item is something the row already
|
||||
* does: open it the way its click does (a live pane attaches, an ended session
|
||||
* opens its conversation — never a resume), open a live one in a split,
|
||||
* rename / archive / unarchive when the list offers them, copy its id, open
|
||||
* its analysis page, and close its live tab (`DELETE /api/run/:id`, what the
|
||||
* tab's X does).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { api, type RunHandle } from "../../lib/api";
|
||||
import type { Session } from "../../lib/types";
|
||||
import { ContextMenu, MenuItem, MenuSeparator, type MenuPoint } from "../ContextMenu";
|
||||
|
||||
export function SessionMenu({
|
||||
laneId,
|
||||
session,
|
||||
run,
|
||||
x,
|
||||
y,
|
||||
onClose,
|
||||
onRename,
|
||||
onArchive,
|
||||
onUnarchive,
|
||||
}: MenuPoint & {
|
||||
laneId: number;
|
||||
session: Session | null;
|
||||
/** The live pane holding it, if any. */
|
||||
run: RunHandle | null;
|
||||
onClose: () => void;
|
||||
onRename?: () => void;
|
||||
onArchive?: () => void;
|
||||
onUnarchive?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation("lanes");
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const runUrl = run && `/workspace?lane=${laneId}&run=${encodeURIComponent(run.id)}`;
|
||||
|
||||
const pick = (fn: () => unknown) => () => {
|
||||
onClose();
|
||||
fn();
|
||||
};
|
||||
const closeTab = async () => {
|
||||
if (!run) return;
|
||||
setError(null);
|
||||
try {
|
||||
await api.run.kill(run.id);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenu x={x} y={y} onClose={onClose} testId={`session-menu-${session?.id ?? run?.id}`}>
|
||||
<MenuItem
|
||||
onSelect={pick(() =>
|
||||
navigate(
|
||||
runUrl ?? `/workspace?lane=${laneId}&session=${encodeURIComponent(session?.id ?? "")}`
|
||||
)
|
||||
)}
|
||||
>
|
||||
{t("sessionMenu.open")}
|
||||
</MenuItem>
|
||||
{runUrl && (
|
||||
<MenuItem onSelect={pick(() => navigate(runUrl, { state: { split: true } }))}>
|
||||
{t("sessionMenu.openSplit")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{onRename && <MenuItem onSelect={pick(onRename)}>{t("menu.rename")}</MenuItem>}
|
||||
{session && (
|
||||
<>
|
||||
<MenuItem onSelect={pick(() => navigate(`/sessions/${session.id}`))}>
|
||||
{t("sessionMenu.details")}
|
||||
</MenuItem>
|
||||
<MenuItem onSelect={pick(() => void navigator.clipboard.writeText(session.id))}>
|
||||
{t("sessionMenu.copyId")}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{(run || onArchive || onUnarchive) && <MenuSeparator />}
|
||||
{onUnarchive && <MenuItem onSelect={pick(onUnarchive)}>{t("tree.unarchive")}</MenuItem>}
|
||||
{onArchive && <MenuItem onSelect={pick(onArchive)}>{t("sessionMenu.archive")}</MenuItem>}
|
||||
{run && (
|
||||
<MenuItem danger onSelect={() => void closeTab()}>
|
||||
{t("tabs.close")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{error && (
|
||||
<p role="alert" className="px-2 py-1 text-[11px] text-status-danger">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user