swarm-ui: choosing a subagent in the subagents menu splits its terminal in beside the agent's
The menu on an agent's terminal page used to open the subagent on a tab of its own. It now starts a group of this terminal and the subagent's, split to the right; in a group's pane it splits that pane. A pane's layout key is the agent, or `agent/subagent` for a subagent's pane, so the layout operations stay as they are and a destroyed agent takes its subagent panes with it. A subagent already in the group shows a marker and does nothing when chosen, and maximize opens a subagent pane's terminal on its own.
This commit is contained in:
parent
5665bbf827
commit
d733b1e2a9
7 changed files with 168 additions and 33 deletions
|
|
@ -113,7 +113,10 @@ state.
|
||||||
|
|
||||||
On a terminal's own page (`/agents/<name>/terminal`, or a subagent's) the
|
On a terminal's own page (`/agents/<name>/terminal`, or a subagent's) the
|
||||||
header has an **N running subagents** badge. It opens a list of the agent's
|
header has an **N running subagents** badge. It opens a list of the agent's
|
||||||
subagents, each a link to its terminal; on a subagent's page the current one
|
subagents. Choosing one puts its terminal to the right of this one in a
|
||||||
|
[group](#groups-of-terminals); in a group, it splits that pane. A subagent
|
||||||
|
already in the group shows a marker and does nothing when chosen, and **maximize**
|
||||||
|
opens a pane's terminal on its own. On a subagent's own page the current one
|
||||||
gets a marker and the list starts with a link back to the agent's own terminal. A
|
gets a marker and the list starts with a link back to the agent's own terminal. A
|
||||||
subagent counts as running when it published a row in the last 15 minutes. The
|
subagent counts as running when it published a row in the last 15 minutes. The
|
||||||
swarm doesn't report more than when the newest row came, so a subagent that
|
swarm doesn't report more than when the newest row came, so a subagent that
|
||||||
|
|
@ -167,7 +170,9 @@ the same drag onto a pane splits that pane, as many times as you like, and
|
||||||
dragging a pane's header badge onto another pane moves it there. The **split**
|
dragging a pane's header badge onto another pane moves it there. The **split**
|
||||||
button in a pane's header does the same for an agent whose terminal tab is open:
|
button in a pane's header does the same for an agent whose terminal tab is open:
|
||||||
**right** or **below** puts it beside the pane. **maximize** opens that
|
**right** or **below** puts it beside the pane. **maximize** opens that
|
||||||
agent's own terminal in its tab, or goes to the tab when one is open already. The **×** takes the pane's
|
agent's own terminal in its tab, or goes to the tab when one is open already. A pane
|
||||||
|
can hold a subagent's terminal (its header reads `agent/subagent`), put there by
|
||||||
|
the **subagents** badge in the pane's header. The **×** takes the pane's
|
||||||
agent out of the group, and a group left with one agent goes, landing on that
|
agent out of the group, and a group left with one agent goes, landing on that
|
||||||
agent's own terminal. Drag a divider to resize, or focus it and use the arrow
|
agent's own terminal. Drag a divider to resize, or focus it and use the arrow
|
||||||
keys. Panes have no minimum size; a divider stops at 2% so a pane can't
|
keys. Panes have no minimum size; a divider stops at 2% so a pane can't
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
button.ui-subagents-menu-link {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.ui-subagents-menu-link:hover,
|
.ui-subagents-menu-link:hover,
|
||||||
.ui-subagents-menu-link:focus-visible {
|
.ui-subagents-menu-link:focus-visible {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@
|
||||||
// (`subagent-list.ts`). Quiet subagents are listed below, dimmed, so a finished
|
// (`subagent-list.ts`). Quiet subagents are listed below, dimmed, so a finished
|
||||||
// one can still be opened.
|
// one can still be opened.
|
||||||
//
|
//
|
||||||
|
// With `onSplit` the entries are buttons that put the subagent's terminal
|
||||||
|
// beside the one the menu is in, instead of links to its own page; the ones in
|
||||||
|
// `shown` are marked and do nothing.
|
||||||
|
//
|
||||||
// Nothing shows for an agent with no subagents, and nothing before the first
|
// Nothing shows for an agent with no subagents, and nothing before the first
|
||||||
// answer; a failed fetch shows its reason in the open list.
|
// answer; a failed fetch shows its reason in the open list.
|
||||||
import { useRef, useState } from "preact/hooks";
|
import { useRef, useState } from "preact/hooks";
|
||||||
|
|
@ -31,12 +35,47 @@ function Entry({
|
||||||
sub,
|
sub,
|
||||||
current,
|
current,
|
||||||
onPick,
|
onPick,
|
||||||
|
onSplit,
|
||||||
}: {
|
}: {
|
||||||
agent: string;
|
agent: string;
|
||||||
sub: Subagent;
|
sub: Subagent;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
onPick: () => void;
|
onPick: () => void;
|
||||||
|
onSplit?: (subagent: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const body = (
|
||||||
|
<>
|
||||||
|
<span class="ui-subagents-menu-name">{sub.name}</span>
|
||||||
|
{runLabel(sub) && (
|
||||||
|
<span class="ui-subagents-menu-age ui-subagents-menu-run">
|
||||||
|
{runLabel(sub)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{sub.last_row_unix !== null && (
|
||||||
|
<span class="ui-subagents-menu-age">
|
||||||
|
<RelativeTime epochMs={sub.last_row_unix * 1000} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
if (onSplit !== undefined) {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ui-subagents-menu-link"
|
||||||
|
title={`put ${sub.name}'s terminal beside this one`}
|
||||||
|
aria-current={current ? "true" : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (!current) onSplit(sub.name);
|
||||||
|
onPick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -45,17 +84,7 @@ function Entry({
|
||||||
onClick={onPick}
|
onClick={onPick}
|
||||||
aria-current={current ? "page" : undefined}
|
aria-current={current ? "page" : undefined}
|
||||||
>
|
>
|
||||||
<span class="ui-subagents-menu-name">{sub.name}</span>
|
{body}
|
||||||
{runLabel(sub) && (
|
|
||||||
<span class="ui-subagents-menu-age ui-subagents-menu-run">
|
|
||||||
{runLabel(sub)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{sub.last_row_unix !== null && (
|
|
||||||
<span class="ui-subagents-menu-age">
|
|
||||||
<RelativeTime epochMs={sub.last_row_unix * 1000} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
@ -64,10 +93,16 @@ function Entry({
|
||||||
export function SubagentsMenu({
|
export function SubagentsMenu({
|
||||||
agentName,
|
agentName,
|
||||||
current,
|
current,
|
||||||
|
onSplit,
|
||||||
|
shown = [],
|
||||||
}: {
|
}: {
|
||||||
agentName: string;
|
agentName: string;
|
||||||
/** The subagent whose page this is, when it is a subagent's. */
|
/** The subagent whose page this is, when it is a subagent's. */
|
||||||
current?: string;
|
current?: string;
|
||||||
|
/** Puts a subagent's terminal beside this one; the entries become buttons. */
|
||||||
|
onSplit?: (subagent: string) => void;
|
||||||
|
/** Subagents whose terminal is beside this one already. */
|
||||||
|
shown?: string[];
|
||||||
}) {
|
}) {
|
||||||
const { subagents, error } = useSubagentList(agentName);
|
const { subagents, error } = useSubagentList(agentName);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
@ -122,8 +157,9 @@ export function SubagentsMenu({
|
||||||
key={s.name}
|
key={s.name}
|
||||||
agent={agentName}
|
agent={agentName}
|
||||||
sub={s}
|
sub={s}
|
||||||
current={s.name === current}
|
current={s.name === current || shown.includes(s.name)}
|
||||||
onPick={() => setOpen(false)}
|
onPick={() => setOpen(false)}
|
||||||
|
onSplit={onSplit}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -140,8 +176,9 @@ export function SubagentsMenu({
|
||||||
key={s.name}
|
key={s.name}
|
||||||
agent={agentName}
|
agent={agentName}
|
||||||
sub={s}
|
sub={s}
|
||||||
current={s.name === current}
|
current={s.name === current || shown.includes(s.name)}
|
||||||
onPick={() => setOpen(false)}
|
onPick={() => setOpen(false)}
|
||||||
|
onSplit={onSplit}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,22 @@ export const MIN_SHARE = 0.02;
|
||||||
|
|
||||||
export const pane = (agent: string): Pane => ({ kind: "pane", agent });
|
export const pane = (agent: string): Pane => ({ kind: "pane", agent });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a pane goes by in a layout: an agent's name, or `agent/subagent` for
|
||||||
|
* one of that agent's subagents. Neither name holds a `/`.
|
||||||
|
*/
|
||||||
|
export function paneKey(agent: string, subagent?: string): string {
|
||||||
|
return subagent === undefined ? agent : `${agent}/${subagent}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The agent and, for a subagent's pane, the subagent a pane key names. */
|
||||||
|
export function splitKey(key: string): { agent: string; subagent?: string } {
|
||||||
|
const at = key.indexOf("/");
|
||||||
|
return at < 0
|
||||||
|
? { agent: key }
|
||||||
|
: { agent: key.slice(0, at), subagent: key.slice(at + 1) };
|
||||||
|
}
|
||||||
|
|
||||||
/** The agents of a layout, in reading order (left to right, top to bottom). */
|
/** The agents of a layout, in reading order (left to right, top to bottom). */
|
||||||
export function agentsIn(node: Node): string[] {
|
export function agentsIn(node: Node): string[] {
|
||||||
return node.kind === "pane" ? [node.agent] : node.children.flatMap(agentsIn);
|
return node.kind === "pane" ? [node.agent] : node.children.flatMap(agentsIn);
|
||||||
|
|
@ -198,8 +214,10 @@ export function pruneMissing(
|
||||||
existing: ReadonlySet<string>,
|
existing: ReadonlySet<string>,
|
||||||
): Node | null {
|
): Node | null {
|
||||||
let out: Node | null = root;
|
let out: Node | null = root;
|
||||||
for (const agent of agentsIn(root)) {
|
for (const key of agentsIn(root)) {
|
||||||
if (out !== null && !existing.has(agent)) out = removeAgent(out, agent);
|
if (out !== null && !existing.has(splitKey(key).agent)) {
|
||||||
|
out = removeAgent(out, key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// <AgentGroupPage> — a group of agent terminals side by side, tmux style, at
|
// <AgentGroupPage> — a group of agent terminals side by side, tmux style, at
|
||||||
// `/agents/group/:id`: each pane is one agent's full terminal, split by
|
// `/agents/group/:id`: each pane is one agent's full terminal, split by
|
||||||
// dragging another agent's terminal tab (or a pane's header) onto the edge of
|
// dragging another agent's terminal tab (or a pane's header) onto the edge of
|
||||||
// a pane, or by the split button in a pane's header. Dividers resize, and the
|
// a pane, or by the split button in a pane's header. A pane can also be one of
|
||||||
|
// an agent's subagents, put beside it by the subagents menu. Dividers resize, and the
|
||||||
// layout is saved in the browser (`useTermGroups`). A pane's × takes its agent
|
// layout is saved in the browser (`useTermGroups`). A pane's × takes its agent
|
||||||
// out of the group; a group of one gives way to that agent's own terminal.
|
// out of the group; a group of one gives way to that agent's own terminal.
|
||||||
//
|
//
|
||||||
|
|
@ -12,6 +13,7 @@ import { useLocation, type RouteComponentProps } from "wouter-preact";
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
import { ExpandIcon } from "@hive/shared/icons.js";
|
import { ExpandIcon } from "@hive/shared/icons.js";
|
||||||
import { AgentIcon } from "../../agent/AgentIcon.js";
|
import { AgentIcon } from "../../agent/AgentIcon.js";
|
||||||
|
import { SubagentsMenu } from "../../agent/terminal/SubagentsMenu.js";
|
||||||
import {
|
import {
|
||||||
AgentTermPreview,
|
AgentTermPreview,
|
||||||
termPaths,
|
termPaths,
|
||||||
|
|
@ -20,9 +22,11 @@ import {
|
||||||
agentsIn,
|
agentsIn,
|
||||||
groupHref,
|
groupHref,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
|
paneKey,
|
||||||
pruneMissing,
|
pruneMissing,
|
||||||
removeAgent,
|
removeAgent,
|
||||||
resizeAt,
|
resizeAt,
|
||||||
|
splitKey,
|
||||||
splitPane,
|
splitPane,
|
||||||
terminalAgentOf,
|
terminalAgentOf,
|
||||||
type Side,
|
type Side,
|
||||||
|
|
@ -113,8 +117,12 @@ export function AgentGroupPage({
|
||||||
if (group === undefined || inGroup.length > 1) return;
|
if (group === undefined || inGroup.length > 1) return;
|
||||||
groups.remove(id);
|
groups.remove(id);
|
||||||
const last = inGroup[0];
|
const last = inGroup[0];
|
||||||
close(href, last === undefined ? "/agents" : termPaths(last).page);
|
const lastPage =
|
||||||
if (last !== undefined) navigate(termPaths(last).page);
|
last === undefined
|
||||||
|
? "/agents"
|
||||||
|
: termPaths(splitKey(last).agent, splitKey(last).subagent).page;
|
||||||
|
close(href, lastPage);
|
||||||
|
if (last !== undefined) navigate(lastPage);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- as above.
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- as above.
|
||||||
}, [inGroup.length, group === undefined]);
|
}, [inGroup.length, group === undefined]);
|
||||||
|
|
||||||
|
|
@ -131,48 +139,69 @@ export function AgentGroupPage({
|
||||||
.map((t) => terminalAgentOf(t.href))
|
.map((t) => terminalAgentOf(t.href))
|
||||||
.filter((a): a is string => a !== null && !inGroup.includes(a));
|
.filter((a): a is string => a !== null && !inGroup.includes(a));
|
||||||
|
|
||||||
function renderPane(agent: string) {
|
// `key` is the pane's key: its agent, or `agent/subagent` for a subagent's.
|
||||||
|
function renderPane(key: string) {
|
||||||
|
const { agent, subagent } = splitKey(key);
|
||||||
return (
|
return (
|
||||||
<PaneDropTarget
|
<PaneDropTarget
|
||||||
agent={agent}
|
agent={key}
|
||||||
onDrop={(side, dropped) =>
|
onDrop={(side, dropped) =>
|
||||||
groups.update(id, (r) => splitPane(r, agent, dropped, side))
|
groups.update(id, (r) => splitPane(r, key, dropped, side))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AgentTermPreview
|
<AgentTermPreview
|
||||||
agentName={agent}
|
agentName={agent}
|
||||||
|
subagent={subagent}
|
||||||
fullHeight
|
fullHeight
|
||||||
leading={
|
leading={
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
class="ui-agent-group-grip"
|
class="ui-agent-group-grip"
|
||||||
draggable
|
draggable
|
||||||
title={`drag ${agent} onto another pane to move it`}
|
title={`drag ${key} onto another pane to move it`}
|
||||||
onDragStart={(e) => startAgentDrag(e, agent)}
|
onDragStart={(e) => startAgentDrag(e, key)}
|
||||||
onDragEnd={endAgentDrag}
|
onDragEnd={endAgentDrag}
|
||||||
>
|
>
|
||||||
<Badge icon={<AgentIcon name={agent} small />} value={agent} />
|
<Badge icon={<AgentIcon name={agent} small />} value={key} />
|
||||||
</span>
|
</span>
|
||||||
<SplitMenu
|
<SplitMenu
|
||||||
candidates={candidates}
|
candidates={candidates}
|
||||||
onSplit={(other, side) =>
|
onSplit={(other, side) =>
|
||||||
groups.update(id, (r) => splitPane(r, agent, other, side))
|
groups.update(id, (r) => splitPane(r, key, other, side))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SubagentsMenu
|
||||||
|
key={agent}
|
||||||
|
agentName={agent}
|
||||||
|
shown={inGroup.flatMap((k) => {
|
||||||
|
const s = splitKey(k);
|
||||||
|
return s.agent === agent && s.subagent !== undefined
|
||||||
|
? [s.subagent]
|
||||||
|
: [];
|
||||||
|
})}
|
||||||
|
onSplit={(picked) =>
|
||||||
|
groups.update(id, (r) =>
|
||||||
|
splitPane(r, key, paneKey(agent, picked), "right"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Badge
|
<Badge
|
||||||
icon={<ExpandIcon />}
|
icon={<ExpandIcon />}
|
||||||
value="maximize"
|
value="maximize"
|
||||||
title={`open ${agent}'s terminal on its own`}
|
title={`open ${key}'s terminal on its own`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
open({ href: termPaths(agent).page, label: agent })
|
open({
|
||||||
|
href: termPaths(agent, subagent).page,
|
||||||
|
label: termPaths(agent, subagent).label,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Badge
|
<Badge
|
||||||
variant="quiet"
|
variant="quiet"
|
||||||
value="×"
|
value="×"
|
||||||
title={`take ${agent} out of this group`}
|
title={`take ${key} out of this group`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
groups.update(id, (r) => removeAgent(r, agent) ?? r)
|
groups.update(id, (r) => removeAgent(r, key) ?? r)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
groupHref,
|
groupHref,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
pane,
|
pane,
|
||||||
|
paneKey,
|
||||||
splitPane,
|
splitPane,
|
||||||
} from "../../agent/terminal/split-layout.js";
|
} from "../../agent/terminal/split-layout.js";
|
||||||
import { useTermGroups } from "../../agent/terminal/useTermGroups.js";
|
import { useTermGroups } from "../../agent/terminal/useTermGroups.js";
|
||||||
|
|
@ -32,7 +33,8 @@ import { PaneDropTarget } from "../../ui/pane-tree/PaneDropTarget.js";
|
||||||
import "./AgentTerminalPage.css";
|
import "./AgentTerminalPage.css";
|
||||||
|
|
||||||
// Also mounted at `/agents/:name/subagents/:subagent/terminal`, for one of
|
// Also mounted at `/agents/:name/subagents/:subagent/terminal`, for one of
|
||||||
// the agent's subagents.
|
// the agent's subagents. Choosing a subagent in the header's subagents menu
|
||||||
|
// starts a group of this terminal and that one, split to the right.
|
||||||
export function AgentTerminalPage({
|
export function AgentTerminalPage({
|
||||||
params,
|
params,
|
||||||
}: RouteComponentProps<{ name: string; subagent?: string }>) {
|
}: RouteComponentProps<{ name: string; subagent?: string }>) {
|
||||||
|
|
@ -81,7 +83,18 @@ export function AgentTerminalPage({
|
||||||
onClick={() => setDetailOpen((o) => !o)}
|
onClick={() => setDetailOpen((o) => !o)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<SubagentsMenu key={name} agentName={name} current={subagent} />
|
<SubagentsMenu
|
||||||
|
key={name}
|
||||||
|
agentName={name}
|
||||||
|
current={subagent}
|
||||||
|
onSplit={(picked) => {
|
||||||
|
const here = paneKey(name, subagent);
|
||||||
|
const group = groups.create(
|
||||||
|
splitPane(pane(here), here, paneKey(name, picked), "right"),
|
||||||
|
);
|
||||||
|
open({ href: groupHref(group.id), label: groupTitle(group) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ import {
|
||||||
groupTitle,
|
groupTitle,
|
||||||
MIN_SHARE,
|
MIN_SHARE,
|
||||||
pane,
|
pane,
|
||||||
|
paneKey,
|
||||||
parseGroups,
|
parseGroups,
|
||||||
|
splitKey,
|
||||||
pruneMissing,
|
pruneMissing,
|
||||||
removeAgent,
|
removeAgent,
|
||||||
resizeAt,
|
resizeAt,
|
||||||
|
|
@ -219,6 +221,29 @@ test("a group's header tab and an agent's terminal tab are told apart by their p
|
||||||
assert.equal(terminalAgentOf("/agents/group/ab12"), null);
|
assert.equal(terminalAgentOf("/agents/group/ab12"), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a subagent's pane is keyed by agent/subagent, split beside its agent, and goes with its agent", () => {
|
||||||
|
assert.equal(paneKey("a"), "a");
|
||||||
|
assert.equal(paneKey("a", "s"), "a/s");
|
||||||
|
assert.deepEqual(splitKey("a"), { agent: "a" });
|
||||||
|
assert.deepEqual(splitKey("a/s"), { agent: "a", subagent: "s" });
|
||||||
|
const two = splitPane(pane("a"), "a", paneKey("a", "s"), "right");
|
||||||
|
assert.deepEqual(agentsIn(two), ["a", "a/s"]);
|
||||||
|
// The same subagent twice changes nothing but its place.
|
||||||
|
assert.deepEqual(agentsIn(splitPane(two, "a", paneKey("a", "s"), "left")), [
|
||||||
|
"a/s",
|
||||||
|
"a",
|
||||||
|
]);
|
||||||
|
// The agent a subagent belongs to being gone takes both panes.
|
||||||
|
const three = splitPane(two, "a/s", "b", "bottom");
|
||||||
|
assert.deepEqual(agentsIn(pruneMissing(three, new Set(["b"]))), ["b"]);
|
||||||
|
assert.deepEqual(agentsIn(pruneMissing(three, new Set(["a", "b"]))), [
|
||||||
|
"a",
|
||||||
|
"a/s",
|
||||||
|
"b",
|
||||||
|
]);
|
||||||
|
assert.equal(groupTitle({ id: "x", name: "", root: two }), "a + a/s");
|
||||||
|
});
|
||||||
|
|
||||||
test("a pane already below the floor can grow but never shrinks further, and no share goes negative", () => {
|
test("a pane already below the floor can grow but never shrinks further, and no share goes negative", () => {
|
||||||
const tiny = row([pane("a"), pane("b"), pane("c")], [0.005, 0.005, 0.99]);
|
const tiny = row([pane("a"), pane("b"), pane("c")], [0.005, 0.005, 0.99]);
|
||||||
const grown = resizeAt(tiny, [], 1, 0.5);
|
const grown = resizeAt(tiny, [], 1, 0.5);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue