Watch
0
0
Fork
You've already forked hyperhive
0

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:
iris 2026-11-05 12:45:35 +01:00
commit d733b1e2a9
3 changed files with 168 additions and 33 deletions

View file

@ -20,6 +20,14 @@
color: inherit;
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:focus-visible {
background: var(--bg);

View file

@ -7,6 +7,10 @@
// (`subagent-list.ts`). Quiet subagents are listed below, dimmed, so a finished
// 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
// answer; a failed fetch shows its reason in the open list.
import { useRef, useState } from "preact/hooks";
@ -31,12 +35,47 @@ function Entry({
sub,
current,
onPick,
onSplit,
}: {
agent: string;
sub: Subagent;
current: boolean;
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 (
<li>
<Link
@ -45,17 +84,7 @@ function Entry({
onClick={onPick}
aria-current={current ? "page" : undefined}
>
<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>
)}
{body}
</Link>
</li>
);
@ -64,10 +93,16 @@ function Entry({
export function SubagentsMenu({
agentName,
current,
onSplit,
shown = [],
}: {
agentName: string;
/** The subagent whose page this is, when it is a subagent's. */
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 [open, setOpen] = useState(false);
@ -122,8 +157,9 @@ export function SubagentsMenu({
key={s.name}
agent={agentName}
sub={s}
current={s.name === current}
current={s.name === current || shown.includes(s.name)}
onPick={() => setOpen(false)}
onSplit={onSplit}
/>
))}
</ul>
@ -140,8 +176,9 @@ export function SubagentsMenu({
key={s.name}
agent={agentName}
sub={s}
current={s.name === current}
current={s.name === current || shown.includes(s.name)}
onPick={() => setOpen(false)}
onSplit={onSplit}
/>
))}
</ul>

View file

@ -41,6 +41,22 @@ export const MIN_SHARE = 0.02;
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). */
export function agentsIn(node: Node): string[] {
return node.kind === "pane" ? [node.agent] : node.children.flatMap(agentsIn);
@ -198,8 +214,10 @@ export function pruneMissing(
existing: ReadonlySet<string>,
): Node | null {
let out: Node | null = root;
for (const agent of agentsIn(root)) {
if (out !== null && !existing.has(agent)) out = removeAgent(out, agent);
for (const key of agentsIn(root)) {
if (out !== null && !existing.has(splitKey(key).agent)) {
out = removeAgent(out, key);
}
}
return out;
}