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,7 +9,7 @@
|
||||
* worktree is still the workspace home's prompt box) — plus a link to
|
||||
* Settings → Agents. Four tabs is the server's limit (`MAX_PANE_SLOT`). The
|
||||
* bar is the console's only header: Home (the workspace's home) at its left
|
||||
* end, `actions` (the workspace's Tools menu) at its right end. A click shows a tab in the focused pane, Ctrl/Cmd+click
|
||||
* end, `actions` (the single pane's split menus) at its right end. A click shows a tab in the focused pane, Ctrl/Cmd+click
|
||||
* (on a tab or a `+` item) opens it in a split to the right, and a tab drags
|
||||
* onto the panes (`PaneGrid.tsx`); a tab another pane shows is marked. A
|
||||
* right-click on a tab opens the same actions as a menu (`ContextMenu`).
|
||||
@@ -29,6 +29,31 @@ import { freeSlot } from "../lanes/WorkspaceHome";
|
||||
import { RunIcon, startPaneDrag } from "./PaneGrid";
|
||||
import { ContextMenu, MenuItem, MenuSeparator, useContextMenu } from "../ContextMenu";
|
||||
|
||||
type AiToolsState = ReturnType<typeof useAiTools>;
|
||||
|
||||
/** The tools a new tab or split offers: the workspace's own whatever its
|
||||
* state, then every installed, enabled one. */
|
||||
export function newTabTools(lane: Lane, aiTools: AiToolsState): string[] {
|
||||
return [
|
||||
...new Set([
|
||||
lane.tool,
|
||||
...aiTools.tools.filter((x) => x.installed && x.enabled).map((x) => x.id),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/** The start body of a bare tool on its default model, no prompt. Only a
|
||||
* different tool is named, as the home does: the workspace's own keeps
|
||||
* starting even once disabled. */
|
||||
export function newTabBody(id: string, lane: Lane, aiTools: AiToolsState) {
|
||||
const model = aiTools.byId(id)?.defaultModel;
|
||||
return {
|
||||
prompt: "",
|
||||
...(id !== lane.tool ? { tool: id } : {}),
|
||||
...(model ? { model } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function ConsoleTabs({
|
||||
lane,
|
||||
tabs,
|
||||
@@ -52,7 +77,7 @@ export function ConsoleTabs({
|
||||
onClosed: (runId: string) => void;
|
||||
/** The workspace's home (its prompt box, Recents, context). */
|
||||
onHome?: () => void;
|
||||
/** Right end of the bar (the workspace's Tools menu). */
|
||||
/** Right end of the bar (the single pane's split menus). */
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation("lanes");
|
||||
@@ -79,13 +104,7 @@ export function ConsoleTabs({
|
||||
|
||||
const slot = freeSlot(lane.id, tabs);
|
||||
const label = (r: RunHandle) => (r.tool === "shell" ? t("tabs.terminal") : aiTools.label(r.tool));
|
||||
// The workspace's own tool whatever its state, then every installed, enabled one.
|
||||
const toolIds = [
|
||||
...new Set([
|
||||
lane.tool,
|
||||
...aiTools.tools.filter((x) => x.installed && x.enabled).map((x) => x.id),
|
||||
]),
|
||||
];
|
||||
const toolIds = newTabTools(lane, aiTools);
|
||||
|
||||
const open = async (body: Record<string, unknown>, split = false) => {
|
||||
setMenuOpen(false);
|
||||
@@ -217,33 +236,19 @@ export function ConsoleTabs({
|
||||
{t("tabs.newTerminal")}
|
||||
</button>
|
||||
<div className="my-1.5 border-t border-border" />
|
||||
{toolIds.map((id) => {
|
||||
const model = aiTools.byId(id)?.defaultModel;
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid={`console-tab-new-${id}`}
|
||||
className={item}
|
||||
onClick={(e) =>
|
||||
void open(
|
||||
{
|
||||
prompt: "",
|
||||
// Only a different tool is named, as the home does: the
|
||||
// workspace's own keeps starting even once disabled.
|
||||
...(id !== lane.tool ? { tool: id } : {}),
|
||||
...(model ? { model } : {}),
|
||||
},
|
||||
e.ctrlKey || e.metaKey
|
||||
)
|
||||
}
|
||||
>
|
||||
<AiToolIcon tool={{ id, label: aiTools.label(id) }} className="h-4 w-4 !p-0" />
|
||||
{aiTools.label(id)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{toolIds.map((id) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid={`console-tab-new-${id}`}
|
||||
className={item}
|
||||
onClick={(e) => void open(newTabBody(id, lane, aiTools), e.ctrlKey || e.metaKey)}
|
||||
>
|
||||
<AiToolIcon tool={{ id, label: aiTools.label(id) }} className="h-4 w-4 !p-0" />
|
||||
{aiTools.label(id)}
|
||||
</button>
|
||||
))}
|
||||
<div className="my-1.5 border-t border-border" />
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -17,16 +17,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Columns2,
|
||||
GripVertical,
|
||||
Plus,
|
||||
Rows2,
|
||||
SquareTerminal,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Check, ChevronDown, Columns2, GripVertical, Rows2, SquareTerminal, X } from "lucide-react";
|
||||
import type { RunHandle } from "../../lib/api";
|
||||
import { ContextMenu, MenuItem, MenuSeparator, useContextMenu } from "../ContextMenu";
|
||||
import {
|
||||
@@ -174,7 +165,7 @@ const ITEM =
|
||||
"flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-sm text-fg-secondary hover:bg-surface-4 hover:text-fg-primary";
|
||||
|
||||
/** A pane's split-right / split-down menus: move a hidden run beside
|
||||
* `slot`, or start a new terminal there. In a pane header when the view is
|
||||
* `slot`, or start a new terminal or AI tool there. In a pane header when the view is
|
||||
* split, beside Tools in the tab bar while one pane is shown. */
|
||||
export function SplitMenus({
|
||||
slot,
|
||||
@@ -183,6 +174,8 @@ export function SplitMenus({
|
||||
label,
|
||||
onChange,
|
||||
canStart,
|
||||
tools,
|
||||
toolLabel,
|
||||
onNewSplit,
|
||||
}: {
|
||||
slot: number;
|
||||
@@ -192,7 +185,11 @@ export function SplitMenus({
|
||||
label: (r: RunHandle) => string;
|
||||
onChange: LayoutUpdate;
|
||||
canStart: boolean;
|
||||
onNewSplit: (target: number, edge: Edge) => void;
|
||||
/** AI tool ids a new split can start, beside the terminal. */
|
||||
tools: string[];
|
||||
toolLabel: (id: string) => string;
|
||||
/** `tool`: an AI tool id; none — a terminal. */
|
||||
onNewSplit: (target: number, edge: Edge, tool?: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation("lanes");
|
||||
const full = shown.length >= MAX_PANES;
|
||||
@@ -246,9 +243,27 @@ export function SplitMenus({
|
||||
onNewSplit(slot, edge);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<SquareTerminal className="h-4 w-4" />
|
||||
{t("panes.newTerminal")}
|
||||
</button>
|
||||
{tools.map((id) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid={`pane-split-new-${edge}-${slot}-${id}`}
|
||||
disabled={!canStart}
|
||||
title={canStart ? undefined : t("tabs.full")}
|
||||
className={`${ITEM} disabled:opacity-40 disabled:hover:bg-transparent`}
|
||||
onClick={() => {
|
||||
close();
|
||||
onNewSplit(slot, edge, id);
|
||||
}}
|
||||
>
|
||||
<AiToolIcon tool={{ id, label: toolLabel(id) }} className="h-4 w-4 !p-0" />
|
||||
{toolLabel(id)}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</PaneMenu>
|
||||
@@ -265,6 +280,8 @@ export function PaneGrid({
|
||||
renderPane,
|
||||
onChange,
|
||||
canStart,
|
||||
tools,
|
||||
toolLabel,
|
||||
onNewSplit,
|
||||
}: {
|
||||
layout: PaneLayout;
|
||||
@@ -277,8 +294,11 @@ export function PaneGrid({
|
||||
onChange: LayoutUpdate;
|
||||
/** A slot is free for a new terminal. */
|
||||
canStart: boolean;
|
||||
/** Start a new terminal into a split of `target`. */
|
||||
onNewSplit: (target: number, edge: Edge) => void;
|
||||
/** AI tool ids a new split can start, beside the terminal. */
|
||||
tools: string[];
|
||||
toolLabel: (id: string) => string;
|
||||
/** Start a new terminal (or AI tool) into a split of `target`. */
|
||||
onNewSplit: (target: number, edge: Edge, tool?: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation("lanes");
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
@@ -450,6 +470,8 @@ export function PaneGrid({
|
||||
label={label}
|
||||
onChange={onChange}
|
||||
canStart={canStart}
|
||||
tools={tools}
|
||||
toolLabel={toolLabel}
|
||||
onNewSplit={onNewSplit}
|
||||
/>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user