feat(workspace): split menus start AI tools; drop the Tools menu
A pane's split right/down menus offered only New terminal. They now list the same AI tools as the tab bar's + menu (newTabTools/newTabBody, shared from ConsoleTabs), started bare in the first free slot on that side of the pane. The tab bar's Tools menu is gone: its readme run hit 409 ERUNLIVE whenever slot 0 was live (always, in the session view) and depended on a personal skill; MCP sync and the dev-sync check served the dormant ship-feature-lane pipeline. The sidebar row menu's readme item goes too. The routes stay, reached by `agenthub lanes mcp sync` and `agenthub lanes sync-base`. Also: the home's Memory header and file tabs no longer shrink (a long file squeezed the tab row and clipped it).
This commit is contained in:
@@ -9,12 +9,7 @@
|
||||
* lane-detail section, whose header above it already shows them. Neither is
|
||||
* progress: the pipeline map directly below this card draws the same number
|
||||
* as named, coloured nodes, so a bar and a percent beside it were a worse
|
||||
* second telling of it. The repo
|
||||
* tooling dropdown (`LaneToolsMenu`, exported from this file — agents
|
||||
* install / MCP sync / dev-sync check / create-update readme) and the
|
||||
* remove-lane button live in Workspace's page header / pipeline-picker row
|
||||
* instead of here, so they stay reachable and un-clipped regardless of this
|
||||
* card's own scroll area. The working copy — branch, uncommitted counts, the
|
||||
* second telling of it. The working copy — branch, uncommitted counts, the
|
||||
* git action bar — is not here either: it heads the Changes panel
|
||||
* (`LaneGitHeader`), beside the diff it describes.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -116,144 +111,6 @@ export function since(sec: number | null): string {
|
||||
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repo tooling (MCP sync / dev-sync check, and the
|
||||
* create/update-readme run, which moved here from the Add-lane modal) for one lane,
|
||||
* collapsed into a single dropdown. Lives outside LaneCard so it can be
|
||||
* placed in the Workspace page header, next to "Add lane" — it polls its
|
||||
* own runtime state independently of LaneCard's, same pattern as the
|
||||
* separately-polled git/locks facts elsewhere on this page.
|
||||
*/
|
||||
export function LaneToolsMenu({ lane }: { lane: Lane }) {
|
||||
const { t } = useTranslation(["lanes"]);
|
||||
const [toolsMenuOpen, setToolsMenuOpen] = useState(false);
|
||||
const [laneActionBusy, setLaneActionBusy] = useState<"mcp" | "sync-check" | "docs" | null>(null);
|
||||
const [laneActionResult, setLaneActionResult] = useState<string | null>(null);
|
||||
const runtime = useLaneRuntime(lane.id, 0);
|
||||
|
||||
const runLaneAction = async (which: "mcp" | "sync-check" | "docs", fn: () => Promise<string>) => {
|
||||
setLaneActionBusy(which);
|
||||
setLaneActionResult(null);
|
||||
try {
|
||||
setLaneActionResult(await fn());
|
||||
} catch (err) {
|
||||
setLaneActionResult(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLaneActionBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
// A skill only exists inside a CLI session, so there is nothing server-side
|
||||
// to call: this opens the lane's own terminal run with the slash command as
|
||||
// its first message. Claude-only — codex has no slash commands.
|
||||
const handleWriteDocs = () =>
|
||||
runLaneAction("docs", async () => {
|
||||
await api.run.start({
|
||||
laneId: lane.id,
|
||||
cwd: lane.cwd,
|
||||
initialPrompt: "/sg-all:sg-md-writing",
|
||||
});
|
||||
return t("actions.writeDocsResult");
|
||||
});
|
||||
|
||||
const handleMcpSync = () =>
|
||||
runLaneAction("mcp", async () => {
|
||||
const result = await api.lanes.mcpSync(lane.id);
|
||||
return t("actions.mcpSyncResult", { servers: result.servers.join(", ") || "none" });
|
||||
});
|
||||
|
||||
const handleSyncCheck = () =>
|
||||
runLaneAction("sync-check", async () => {
|
||||
const result = await api.lanes.syncBaseCheck(lane.id);
|
||||
if (result.code === 5 && result.collisions && result.collisions.length > 0) {
|
||||
const c = result.collisions[0]!;
|
||||
return t("actions.syncCheckCollision", { file: c.file, suggestion: c.suggestion });
|
||||
}
|
||||
return t("actions.syncCheckClean", {
|
||||
count: result.devDelta?.length ?? 0,
|
||||
overlap: result.overlap?.length ? result.overlap.join(", ") : "none",
|
||||
});
|
||||
});
|
||||
|
||||
if (!runtime?.available) return null;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="lane-tools-menu"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={toolsMenuOpen}
|
||||
disabled={laneActionBusy !== null}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setToolsMenuOpen((v) => !v);
|
||||
}}
|
||||
className="rounded border border-border-light px-2 py-1 text-xs text-fg-secondary transition-colors hover:text-fg-primary disabled:opacity-50"
|
||||
>
|
||||
{laneActionBusy ? t("actions.busy") : t("actions.tools")}
|
||||
</button>
|
||||
{toolsMenuOpen && (
|
||||
<div
|
||||
role="menu"
|
||||
className="absolute right-0 z-10 mt-1 min-w-44 overflow-hidden rounded-md border border-border-light bg-surface-0 py-1 shadow-lg shadow-black/40"
|
||||
>
|
||||
{lane.tool === "claude" && (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="lane-write-docs"
|
||||
title={t("actions.writeDocsHint")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setToolsMenuOpen(false);
|
||||
void handleWriteDocs();
|
||||
}}
|
||||
className="block w-full px-3 py-1.5 text-left text-fg-secondary hover:bg-surface-2"
|
||||
>
|
||||
{t("actions.writeDocs")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="lane-mcp-sync"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setToolsMenuOpen(false);
|
||||
void handleMcpSync();
|
||||
}}
|
||||
className="block w-full px-3 py-1.5 text-left text-fg-secondary hover:bg-surface-2"
|
||||
>
|
||||
{t("actions.mcpSync")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="lane-sync-check"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setToolsMenuOpen(false);
|
||||
void handleSyncCheck();
|
||||
}}
|
||||
className="block w-full px-3 py-1.5 text-left text-fg-secondary hover:bg-surface-2"
|
||||
>
|
||||
{t("actions.syncCheck")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{laneActionResult && (
|
||||
<p
|
||||
data-testid="lane-action-result"
|
||||
className="absolute right-0 top-full mt-1 w-max max-w-xs truncate rounded border border-border-light bg-surface-0 px-2 py-1 text-[11px] text-fg-secondary shadow-lg shadow-black/40"
|
||||
>
|
||||
{laneActionResult}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LaneCard({
|
||||
lane,
|
||||
onAction,
|
||||
|
||||
Reference in New Issue
Block a user