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
3 changed files with 168 additions and 33 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue