Skip to content
Merged
9 changes: 3 additions & 6 deletions webviews/src/agent-session/acpmux/bottomStack.css
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,6 @@
stupid-ass fucking padding"). */
min-height: var(--ui-row-height, var(--ds-control-row, 28px));
}
.acpmux-dock > .acpmux-running-tray[data-narrow] {
--acpmux-tray-header-lines: 2;
min-height: calc(2 * var(--ui-row-height, var(--ds-control-row, 28px)));
}
@media (min-height: 360px) {
/* Not while the fold plays (conversation/fold.ts clips it then and marks it data-fold-clip): the
fold's own height carries the tray, so open and close never jump by the reservation. Never a
Expand All @@ -183,8 +179,9 @@
> .acpmux-running-tray:has(
> .acpmux-running-tray__fold:not([data-fold-clip]) > .acpmux-running-tray__finished
) {
/* View all needs one row in addition to the header's one or two lines. */
min-height: calc((var(--acpmux-tray-header-lines, 1) + 1) * var(--ui-row-height, var(--ds-control-row, 28px)) + var(--ds-space-1) / 2 + 2 * var(--cmux-hairline));
/* View all needs one row in addition to the header's one, narrow too: the header's time follows
its counts on its one line (subagents.css), so it never takes a second. */
min-height: calc(2 * var(--ui-row-height, var(--ds-control-row, 28px)) + var(--ds-space-1) / 2 + 2 * var(--cmux-hairline));
}
}
/* A pane too short for both pinned rows and the composer (a webview under 360px) keeps the composer
Expand Down
15 changes: 10 additions & 5 deletions webviews/src/agent-session/acpmux/subagents/AgentLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,17 @@ export function AgentLine({
<span className="acpmux-agent-line__status">
{/* Running turns to Waiting or Done with a tick, never a jump (trayMotion.tsx). */}
<Tick className="acpmux-summary-meta acpmux-summary-agent__state" text={state} />
{chevron && onOpen && (
<span className="acpmux-agent-line__chevron" aria-hidden="true">
<Icon name="disclosure.collapsed" size={ROW_ICON} row />
</span>
)}
</span>
{/* Stop's track (the stop slot, outside this button, lies over it), then the chevron's,
last at the line's end: the column the header's and View all's chevrons end in. */}
<span className="acpmux-agent-line__stop-track" aria-hidden="true" />
{chevron && onOpen ? (
<span className="acpmux-agent-line__chevron" aria-hidden="true">
<Icon name="disclosure.collapsed" size={ROW_ICON} row />
</span>
) : (
<span className="acpmux-agent-line__chevron-track" aria-hidden="true" />
)}
</>
) : (
<>
Expand Down
26 changes: 19 additions & 7 deletions webviews/src/agent-session/acpmux/subagents/SubagentTray.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
// stop button. An agent another one spawned sits right under it.
//
// One item is its line alone. Several get a header first, as the Codex and Claude desktop apps
// head their running agents: "2 subagents running · 1 command", the time since the first started
// and a chevron; between turns it says "Running in background · 2 subagents · 1 command". The
// head their running agents: a still running-work mark (never the spinner, which stays on each
// working line, #27103), "2 subagents running · 1 command", the time since the first started as
// quiet meta right after the counts (never a column of its own far from them), and the chevron in
// the trailing column every line's chevron ends in; between turns it says "Running in background ·
// 2 subagents · 1 command". The
// header counts exactly the lines under it: what runs or waits on the person. The chat's subagents
// and the chats its agent started are all "subagents" (Leo 2026-10-21: "SHOULDN'T THIS SAY
// SUBAGENTS?"). What ended stays as compact status cards after them, never counted. A command is
Expand Down Expand Up @@ -66,6 +69,7 @@ import { itemStart, type TrayItem } from "./runningTray";
import { doingDetail, toolDetail } from "./trayDetail";
import { trayPanelLook } from "./trayPanel.experiment";
import { trayMotionLook } from "./trayMotion.experiment";
import { trayHeadLook } from "./trayHead.experiment";
import { Tick, useTrayMotion } from "./trayMotion";
import type { StartedSession } from "../started/startedSessions";
import type { SubagentTrayPlacement } from "./subagentTrayPlacement";
Expand Down Expand Up @@ -138,6 +142,7 @@ export const SubagentTray = memo(function SubagentTray({
// Read once per tray: the arm is the gallery's override or a dogfood key, never a per-tick read.
const [look] = useState(() => experimentArm(trayPanelLook));
const [motion] = useState(() => experimentArm(trayMotionLook));
const [head] = useState(() => experimentArm(trayHeadLook));
// Only the bottom stack's tray leaves on a snapshot: one placed in the transcript moves with it.
const motionRef = useTrayMotion(shown, enters, placement === "tray");
const setTrayRef = useCallback(
Expand Down Expand Up @@ -277,7 +282,7 @@ export const SubagentTray = memo(function SubagentTray({
</li>
)),
];
// The header's spinner is a running line's: it turns while anything under it runs.
// The header's mark is in the accent while anything under it runs.
const runs = running > 0 || commands > 0;
// View all sits under the list, outside its scroller, so it never scrolls away either.
const more = finished.length > TRAY_OPEN_MAX && (
Expand All @@ -304,6 +309,7 @@ export const SubagentTray = memo(function SubagentTray({
data-look={look}
data-motion={motion}
data-narrow={narrow || undefined}
data-head={head}
>
{headed && (
<button
Expand All @@ -313,17 +319,20 @@ export const SubagentTray = memo(function SubagentTray({
aria-controls={open ? listId : undefined}
onClick={() => setOpened(!open)}
>
<span className="acpmux-lineage__icon" data-state={runs ? "running" : "done"}>
<Icon name="agent.session" size={14} />
</span>
{head !== "bare" && (
<span className="acpmux-lineage__icon" data-state={runs ? "running" : "done"}>
<Icon name={head === "session" ? "agent.session" : HEAD_ICON} size={ROW_ICON} row />
</span>
)}
<Tick className="acpmux-summary-text acpmux-running-tray__title" text={title} />
{/* Quiet meta right after the counts, never a column of its own far from them. */}
<span className="acpmux-summary-meta acpmux-running-tray__time" aria-hidden={starts.length === 0}>
{starts.length > 0 ? formatDuration(Math.max(0, now - Math.min(...starts))) : undefined}
</span>
{/* The one trailing column every line's chevron and View all's end in. */}
<span className="acpmux-running-tray__status">
<Icon name={open ? "disclosure.expanded" : "disclosure.collapsed"} size={ROW_ICON} row />
</span>
<span className="acpmux-running-tray__stop-slot" aria-hidden="true" />
</button>
)}
{fold.present && (
Expand Down Expand Up @@ -461,6 +470,9 @@ export const SubagentTray = memo(function SubagentTray({
/// A subagent's mark before its name, in place of the word: a branch off this chat, never the
/// chat bubble a session's own icon is (Leo, 2026-10-28: "Shouldn't the subagent icon be different?").
const SUBAGENT_ICON = "agent.fork";
/// The header's fixed mark for background work (trayHead.experiment.ts): a running ring, never the
/// chat bubble, which reads as a message, and never the spinner, which each working line keeps.
const HEAD_ICON = "app.sample.runningagents";
const NO_ITEMS: readonly TrayItem[] = [];
const NO_ARRIVALS: ReadonlySet<string> = new Set();
/// The most lines that rise in from one update; a bigger burst is simply there.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// l10n-allow-file: gallery fixtures (prompts, subagent names, commands), not shipped UI.
// The running tray's header and trailing column (SubagentTray.tsx, AgentLine.tsx). The collapsed
// header said "Running in background · N subagents · M commands" behind a chat bubble, with the
// elapsed time floating in a column of its own far from the words, and the open tray's Stop buttons
// hung below and right of their lines, off their centre, with the chevrons in two columns. Here the
// header's fixed mark is work that runs (three arms, trayHead.experiment.ts; view Compare arms), its time
// follows the counts as quiet meta, every Stop sits in its line on the line's centre, and the
// header's, every line's and View all's chevrons end on one x.
import { agentPaneEntry } from "../../../gallery/format";
import { activity, assistant, chat, row, summary, tool, user } from "../../../gallery/fixtures/acpmux";
import { minutesAgo } from "../../../gallery/clock";
import type { PlayContext } from "../../../gallery/play";
import { SUBAGENTS, type Subagent } from "./subagentFold";
import { trayHeadLook } from "./trayHead.experiment";

const running = (name: string, minutes: number, action: string): Subagent => ({
id: `agent-${name.toLowerCase().replaceAll(" ", "-")}`,
parent: null,
name,
task: name,
kind: "general-purpose",
action,
state: "running",
background: true,
startedAt: minutesAgo(minutes),
});

const command = (line: string, description: string, minutes: number) =>
tool(line, "execute", "completed", {
inputSummary: JSON.stringify({ command: line, description, run_in_background: true }),
startedAt: minutesAgo(minutes),
});

/** Between turns: three background subagents and two background commands still run. */
const betweenTurns = chat([
user("Fan the review out and keep the checks running while I look", 163),
row(SUBAGENTS, 163, {
subagents: [
running("Review the tray", 163.3, "Read SubagentTray.tsx"),
running("Review the fold", 120, "Grep useFold"),
running("Audit the stack", 45, "Read bottomStack.css"),
],
}),
activity(
[
command("cd ~/Projects/app && bun run dev", "Run the dev server", 150),
command("cd ~/Projects/app/webviews && bun test --watch", "Watch the tests", 30),
],
150,
),
assistant("The reviewers and the checks keep running in the background.", 2),
summary(2, { status: "completed", durationMs: 9_800_000, toolCount: 3 }),
]);

const head = (ctx: PlayContext) => ctx.document.querySelector<HTMLElement>(".acpmux-running-tray__head");

const open = async (ctx: PlayContext) => {
await ctx.waitFor(() => head(ctx));
if (head(ctx)!.getAttribute("aria-expanded") !== "true") await ctx.click({ selector: ".acpmux-running-tray__head" });
await ctx.waitFor(() => ctx.document.querySelectorAll(".acpmux-running-tray__list .acpmux-agent-line").length >= 3);
};

export default agentPaneEntry({
id: "agent-pane.subagent-tray-head",
title: "Running tray header and trailing column",
area: "Agent pane",
height: 420,
anchors: [{ selector: ".acpmux-header" }, { selector: ".acpmux-composer" }],
covers: [
"agent-session/acpmux/subagents/SubagentTray.tsx",
"agent-session/acpmux/subagents/AgentLine.tsx",
"agent-session/acpmux/subagents/trayHead.experiment.ts",
],
experiment: {
definition: trayHeadLook,
script: [
{
name: "the folded header",
run: async (ctx) => {
await ctx.waitFor(() => head(ctx));
},
},
{ name: "open the tray", run: open },
],
},
variants: {
folded: {
note: "Between turns, folded: the header's mark, its counts, the elapsed time right after them as quiet meta, and the chevron in the lines' trailing column.",
play: async (ctx) => {
await ctx.waitFor(() => head(ctx));
},
snapshot: betweenTurns,
},
open: {
note: "Open: every Stop sits inside its line on the line's centre, before the chevron; the header's and every line's chevrons end on one x.",
play: open,
snapshot: betweenTurns,
},
},
});
70 changes: 51 additions & 19 deletions webviews/src/agent-session/acpmux/subagents/subagents.css
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,10 @@ button.cv-subagent > :last-child {
container-type: inline-size;
container-name: running-tray;
--acpmux-tray-time-width: calc(80px * var(--cmux-interface-scale, 1));
--acpmux-tray-status-width: 96px;
--acpmux-tray-status-width: 74px;
--acpmux-tray-stop-width: 24px;
/* The one trailing column every chevron ends in: the header's, each line's and View all's. */
--acpmux-tray-chevron-width: 14px;
box-sizing: border-box;
padding: 0;
overflow: hidden;
Expand Down Expand Up @@ -501,7 +503,6 @@ button.cv-subagent > :last-child {
.acpmux-running-tray__head {
height: var(--ui-row-height, 28px);
}
.acpmux-running-tray__time,
.acpmux-running-tray .acpmux-agent-line__time {
box-sizing: border-box;
flex: 0 0 var(--acpmux-tray-time-width);
Expand All @@ -510,7 +511,6 @@ button.cv-subagent > :last-child {
text-align: right;
white-space: nowrap;
}
.acpmux-running-tray__status,
.acpmux-running-tray .acpmux-agent-line__status {
box-sizing: border-box;
flex: 0 0 var(--acpmux-tray-status-width);
Expand All @@ -528,28 +528,59 @@ button.cv-subagent > :last-child {
text-overflow: ellipsis;
white-space: nowrap;
}
.acpmux-running-tray__stop-slot {
/* The header (Leo's collapsed strip, 2026-10-31: the time floated in a column of its own far from
the words it belongs to): the counts take only their width, the elapsed time follows them as quiet
meta one gap later, and the chevron ends the row in the trailing column the lines' chevrons and
View all's end in. A long title truncates; the time never does. */
.acpmux-running-tray__head > .acpmux-running-tray__title {
flex: 0 1 auto;
}
.acpmux-running-tray__time {
flex: 1 0 auto;
text-align: left;
white-space: nowrap;
}
.acpmux-running-tray__time:empty {
margin-left: -8px;
}
.acpmux-running-tray__status,
.acpmux-running-tray .acpmux-agent-line__chevron,
.acpmux-running-tray .acpmux-agent-line__chevron-track {
box-sizing: border-box;
flex: 0 0 var(--acpmux-tray-stop-width);
min-width: var(--acpmux-tray-stop-width);
flex: 0 0 var(--acpmux-tray-chevron-width);
width: var(--acpmux-tray-chevron-width);
margin: 0;
display: inline-flex;
align-items: center;
justify-content: flex-end;
}
/* A line's trailing tracks, in order: its time, its state, Stop's track (empty where it has no Stop,
so nothing moves), then the chevron's. */
.acpmux-running-tray .acpmux-agent-line__stop-track {
flex: 0 0 var(--acpmux-tray-stop-width);
}
/* Stop is a button of its own, so it lies outside the line's button, over the line's Stop track: in
the row's box, on its centre line (Leo's open tray, 2026-10-31: each Stop hung below and right of
its line). It is one row tall from the line's top, so a line with a second line of progress keeps
it beside its state, never between the two. */
.acpmux-running-tray .acpmux-agent-line__stop-slot {
box-sizing: border-box;
position: absolute;
top: 0;
right: 8px;
bottom: 0;
right: calc(8px + var(--acpmux-tray-chevron-width) + 8px);
height: var(--ui-row-height, 28px);
width: var(--acpmux-tray-stop-width);
min-width: var(--acpmux-tray-stop-width);
display: inline-flex;
align-items: center;
justify-content: flex-end;
justify-content: center;
pointer-events: none;
}
.acpmux-running-tray .acpmux-agent-line__stop-slot > button {
position: static;
top: auto;
right: auto;
margin: 0;
pointer-events: auto;
}
/* Under the header's divider the first line keeps the tray's 2px gap, as the last keeps its rim's. */
Expand Down Expand Up @@ -595,9 +626,8 @@ button.cv-subagent > :last-child {
padding-block: 4px;
}
.acpmux-running-tray .acpmux-agent-line > .acpmux-agent-line__button {
/* Reserve the final track while keeping the stop control outside the button. */
/* A nested line's indent; its trailing tracks (Stop's, then the chevron's) are its own children. */
padding-left: calc(8px + var(--acpmux-tray-indent, 0px));
padding-right: calc(8px + var(--acpmux-tray-stop-width) + 8px);
}
.acpmux-agent-line > .acpmux-agent-line__stop-slot > .cv-subagent__stop {
position: static;
Expand All @@ -611,11 +641,12 @@ button.cv-subagent > :last-child {
same two-line layout: title above, intact duration and status beside Stop below. Query the
tray itself so split panes and gallery frames follow the same rule. */
@container running-tray (max-width: 400px) {
.acpmux-running-tray__head,
.acpmux-running-tray .acpmux-agent-line > .acpmux-agent-line__button {
position: relative;
display: grid;
grid-template-columns: var(--acpmux-tray-time-width) minmax(0, 1fr) var(--acpmux-tray-stop-width);
grid-template-columns:
var(--acpmux-tray-time-width) minmax(0, 1fr) var(--acpmux-tray-stop-width)
var(--acpmux-tray-chevron-width);
grid-template-rows: var(--ui-row-height, 28px) var(--ui-row-height, 28px);
column-gap: 4px;
row-gap: 0;
Expand All @@ -628,38 +659,39 @@ button.cv-subagent > :last-child {
height: auto;
align-items: center;
}
.acpmux-running-tray__head > .acpmux-lineage__icon,
.acpmux-running-tray .acpmux-agent-line__button > .acpmux-lineage__icon {
position: absolute;
top: 7px;
left: calc(8px + var(--acpmux-tray-indent, 0px));
}
.acpmux-running-tray__head > .acpmux-summary-text,
.acpmux-running-tray .acpmux-agent-line__button > .acpmux-summary-text {
grid-column: 1 / -1;
grid-row: 1;
margin-left: calc(22px + var(--acpmux-tray-indent, 0px));
}
.acpmux-running-tray__time,
.acpmux-running-tray .acpmux-agent-line__time {
grid-column: 1;
grid-row: 2;
}
.acpmux-running-tray__status,
.acpmux-running-tray .acpmux-agent-line__status {
grid-column: 2;
grid-row: 2;
min-width: 0;
gap: 4px;
}
.acpmux-running-tray__stop-slot {
.acpmux-running-tray .acpmux-agent-line__stop-track {
grid-column: 3;
grid-row: 2;
}
.acpmux-running-tray :is(.acpmux-agent-line__chevron, .acpmux-agent-line__chevron-track) {
grid-column: 4;
grid-row: 2;
}
/* The narrow line's controls are its second row: Stop sits on it, over its track. */
.acpmux-running-tray .acpmux-agent-line__stop-slot {
top: auto;
bottom: 0;
height: var(--ui-row-height, 28px);
right: calc(8px + var(--acpmux-tray-chevron-width) + 4px);
}
}
/* Quiet at rest. The line is the tray's main control, so its wash covers the whole line
Expand Down
Loading
Loading