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:
@@ -61,7 +61,7 @@ import type { Lane, LaneFeature, ProofFeature } from "../lib/types";
|
||||
import { keepIfSame } from "../lib/keepIfSame";
|
||||
import { useLanes } from "../hooks/useLanes";
|
||||
import { LaneConsolePane } from "../components/run/LaneConsolePane";
|
||||
import { ConsoleTabs } from "../components/run/ConsoleTabs";
|
||||
import { ConsoleTabs, newTabBody, newTabTools } from "../components/run/ConsoleTabs";
|
||||
import { type LayoutUpdate, PaneGrid, SplitMenus, usePaneLayout } from "../components/run/PaneGrid";
|
||||
import {
|
||||
type Edge,
|
||||
@@ -77,7 +77,7 @@ import { useAiTools } from "../hooks/useAiTools";
|
||||
import { freeSlot } from "../components/lanes/WorkspaceHome";
|
||||
import { LIVE_RUNS_EVENT } from "../components/WorkspaceTree";
|
||||
import PipelineMap from "../components/lanes/PipelineMap";
|
||||
import LaneCard, { since, LaneToolsMenu } from "../components/lanes/LaneCard";
|
||||
import LaneCard, { since } from "../components/lanes/LaneCard";
|
||||
import { AddLaneModal } from "../components/lanes/AddLaneModal";
|
||||
import { LaneChangesPanel } from "../components/lanes/LaneChangesPanel";
|
||||
import { WorkspaceHome } from "../components/lanes/WorkspaceHome";
|
||||
@@ -285,16 +285,19 @@ function ConsoleArea({
|
||||
else onNewSession?.();
|
||||
};
|
||||
|
||||
// A split menu's "New terminal": a shell in the first free slot, placed on
|
||||
// that side of the pane — unless the workspace changed while it started.
|
||||
// A split menu's "New terminal" or AI tool (bare, as the tab bar's `+`
|
||||
// starts it) in the first free slot, placed on that side of the pane —
|
||||
// unless the workspace changed while it started.
|
||||
const free = lane ? freeSlot(lane.id, tabs) : null;
|
||||
const newSplit = async (target: number, edge: Edge) => {
|
||||
const splitTools = lane ? newTabTools(lane, aiTools) : [];
|
||||
const newSplit = async (target: number, edge: Edge, tool?: string) => {
|
||||
if (!lane || free === null) return;
|
||||
const laneId = lane.id;
|
||||
const slot = free;
|
||||
setSplitError(null);
|
||||
try {
|
||||
const r = await api.lanes.action(laneId, "start", { slot, shell: true });
|
||||
const body = tool ? newTabBody(tool, lane, aiTools) : { shell: true };
|
||||
const r = await api.lanes.action(laneId, "start", { slot, ...body });
|
||||
onRunsChanged();
|
||||
if (!r.run_id || laneRef.current !== laneId) return;
|
||||
pending.current.set(slot, activeRuns);
|
||||
@@ -327,11 +330,10 @@ function ConsoleArea({
|
||||
}}
|
||||
onClosed={(runId) => ended(slotOf(runId))}
|
||||
onHome={onNewSession}
|
||||
// Per-workspace actions (mcp sync, sync-base check, readme), and
|
||||
// while one pane is shown its split menus (a split pane has its own).
|
||||
// While one pane is shown, its split menus (a split pane has its own).
|
||||
actions={
|
||||
<div className="flex items-center gap-1">
|
||||
{shown.length === 1 && shown[0] !== undefined && (
|
||||
shown.length === 1 && shown[0] !== undefined ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<SplitMenus
|
||||
slot={shown[0]}
|
||||
shown={shown}
|
||||
@@ -340,11 +342,12 @@ function ConsoleArea({
|
||||
// A layout never saved is the one-pane `view`: split that.
|
||||
onChange={(fn) => change((l) => fn(l.root ? l : view))}
|
||||
canStart={free !== null}
|
||||
onNewSplit={(target, edge) => void newSplit(target, edge)}
|
||||
tools={splitTools}
|
||||
toolLabel={aiTools.label}
|
||||
onNewSplit={(target, edge, tool) => void newSplit(target, edge, tool)}
|
||||
/>
|
||||
)}
|
||||
<LaneToolsMenu lane={lane} />
|
||||
</div>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -360,7 +363,9 @@ function ConsoleArea({
|
||||
label={label}
|
||||
onChange={change}
|
||||
canStart={free !== null}
|
||||
onNewSplit={(target, edge) => void newSplit(target, edge)}
|
||||
tools={splitTools}
|
||||
toolLabel={aiTools.label}
|
||||
onNewSplit={(target, edge, tool) => void newSplit(target, edge, tool)}
|
||||
renderPane={(slot) => (
|
||||
<LaneConsolePane
|
||||
lanes={lanes}
|
||||
|
||||
@@ -25,6 +25,7 @@ type LaneFixture = {
|
||||
detected_signal: string | null;
|
||||
run_id: string | null;
|
||||
active_feature_id?: number | null;
|
||||
tool?: string;
|
||||
};
|
||||
|
||||
let lanesToReturn: LaneFixture[] = [
|
||||
@@ -806,6 +807,17 @@ describe("split panes", () => {
|
||||
expect(api.lanes.action).toHaveBeenCalledWith(1, "start", { slot: 2, shell: true });
|
||||
});
|
||||
|
||||
it("a split menu starts an AI tool, bare, in the first free slot", async () => {
|
||||
lanesToReturn = lanesToReturn.map((l) => ({ ...l, tool: "claude" }));
|
||||
withRuns([live(0), live(1)]);
|
||||
await open("/workspace?lane=1&run=agenthub-lane-1");
|
||||
await ctrlClick(screen.getByTestId("console-tab-1"));
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByTestId("pane-split-right-1"));
|
||||
await user.click(screen.getByTestId("pane-split-new-right-1-claude"));
|
||||
expect(api.lanes.action).toHaveBeenCalledWith(1, "start", { slot: 2, prompt: "" });
|
||||
});
|
||||
|
||||
it("with one pane, the split menus sit beside Tools and split it", async () => {
|
||||
withRuns([live(0), live(1)]);
|
||||
await open("/workspace?lane=1&run=agenthub-lane-1");
|
||||
|
||||
Reference in New Issue
Block a user