/** * @file WorkspaceRowMenu — the sidebar workspace row's context menu, opened at * the pointer (right-click) or under the row's `⋯` button. Every item is an * existing call: Start/Stop (`POST /api/lanes/:id/start` slot 0 / `stop`), * Rename (the row's inline field), Create/update readme (claude only, the same run * the lane Tools menu starts), New workspace here (the Add modal prefilled), * Copy path, and Reset/Remove through `DestructiveLaneModal` — Reset only on a * managed lane. The server's guards are the real ones; the menu only hides * what the lane's kind or tool makes meaningless. Closes on Escape, an outside * click or after an item runs. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { api } from "../../lib/api"; import type { Lane } from "../../lib/types"; export type WorkspaceMenuAction = "rename" | "new-here" | "reset" | "remove" | "started"; export function WorkspaceRowMenu({ lane, x, y, onClose, onAction, }: { lane: Lane; x: number; y: number; onClose: () => void; /** Items the tree itself carries out (rename field, modals, selection). */ onAction: (action: WorkspaceMenuAction) => void; }) { const { t } = useTranslation("lanes"); const ref = useRef(null); const [error, setError] = useState(null); useEffect(() => { const onDown = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) onClose(); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("mousedown", onDown); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); }; }, [onClose]); const run = async (fn: () => Promise, then?: WorkspaceMenuAction) => { setError(null); try { await fn(); if (then) onAction(then); onClose(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; const live = !!lane.run_id || lane.status === "running"; const item = "block w-full rounded px-2 py-1 text-left text-xs text-fg-secondary hover:bg-surface-4 hover:text-fg-primary"; return (
{lane.tool === "claude" && ( )}
{lane.kind === "managed" && ( )} {error && (

{error}

)}
); }