Move the fleet rows inline under the delegate tool call

0dbb23cc2e0b · AtlantisPleb · · parent f63a651ddd7b

Move the fleet rows inline under the delegate tool call

A running delegate now shows one row per child directly under the tool
header, then the activity preview box. The separate bottom fleet block is
gone, so the transcript owns the whole scrollback.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified packages/openagents-cli/src/coder-fleet.ts
  • modified packages/openagents-cli/src/coder-ui.ts
  • modified packages/openagents-cli/test/coder-delegate.test.ts
  • modified packages/openagents-cli/test/coder-ui.test.ts

Diff

4 files changed, +28 -76

packages/openagents-cli/src/coder-fleet.ts modified -26

@@ -39,32 +39,6 @@ const MARKS: Record<CoderTaskStatus, string> = {

39 39
  stopped: "■",
40 40
};
41 41
42
/**
43
 * The one-line summary for the status bar, or undefined when there is no fleet.
44
 *
45
 * Running children come first because they are what the reader is waiting on,
46
 * and the terminal counts follow only when there are any, so a plain fan-out
47
 * does not carry a trail of zeroes.
48
 */
49
export function fleetPhrase(tasks: ReadonlyArray<CoderTask>): string | undefined {
50
  if (tasks.length === 0) return undefined;
51
52
  const active = tasks.filter((task) => !isTerminal(task.status));
53
  const done = tasks.filter((task) => task.status === "completed");
54
  const failed = tasks.filter((task) => task.status === "failed");
55
  const unread = tasks.filter((task) => task.unread);
56
57
  const parts: string[] = [];
58
  if (active.length > 0) {
59
    parts.push(`${String(active.length)} ${active.length === 1 ? "agent" : "agents"}`);
60
  }
61
  if (done.length > 0) parts.push(`${String(done.length)} done`);
62
  if (failed.length > 0) parts.push(`${String(failed.length)} failed`);
63
  if (unread.length > 0) parts.push(`${String(unread.length)} unread`);
64
  if (parts.length === 0) return `${String(tasks.length)} agents finished`;
65
  return parts.join(" · ");
66
}
67
68 42
/**
69 43
 * What a child is doing, in one phrase.
70 44
 *
packages/openagents-cli/src/coder-ui.ts modified +25 -45

@@ -30,7 +30,7 @@

30 30
 * own job instead.
31 31
 */
32 32
33
import { activityPhrase, fleetPhrase, fleetRows, latestActivities } from "./coder-fleet.js";
33
import { activityPhrase, fleetRows, latestActivities } from "./coder-fleet.js";
34 34
import { renderMarkdown, visibleWidth, wrapStyled } from "./coder-markdown.js";
35 35
import type { CoderEntry, CoderSession, CoderSnapshot, CoderToolCall } from "./coder-session.js";
36 36
import type { CoderTask, CoderTaskStatus } from "./coder-tasks.js";

@@ -460,8 +460,26 @@ export function runCoderUi(session: CoderSession, options: CoderUiOptions): Prom

460 460
      const rows = [`${mark} ${BOLD}${tool.name}${RESET}`];
461 461
462 462
      if (tool.name === "delegate" && tool.status === "running") {
463
        const phrase = fleetPhrase(tasks) ?? "starting children…";
464
        rows.push(`${DIM}→ ${phrase}${RESET}`);
463
        // Working children first when there are more than fit: a finished child
464
        // has already been reported on the transcript, so it is the one to drop.
465
        const shown =
466
          tasks.length <= FLEET_ROWS_MAX
467
            ? tasks
468
            : [
469
                ...tasks.filter((task) => task.status === "running" || task.status === "pending"),
470
                ...tasks.filter((task) => task.status !== "running" && task.status !== "pending"),
471
              ].slice(0, FLEET_ROWS_MAX);
472
473
        for (const child of fleetRows(shown, Math.max(20, width - 9))) {
474
          const color = fleetColor(child.status);
475
          rows.push(
476
            `${DIM}${child.branch}${RESET} ${color}${child.mark}${RESET} ${DIM}${child.text}${RESET}`,
477
          );
478
        }
479
480
        const hidden = tasks.length - shown.length;
481
        if (hidden > 0) rows.push(`${DIM}   +${String(hidden)} more${RESET}`);
482
465 483
        const activities = latestActivities(tasks, PREVIEW_ROWS);
466 484
        if (activities.length > 0) {
467 485
          const boxWidth = Math.max(10, width - 4);

@@ -510,36 +528,6 @@ export function runCoderUi(session: CoderSession, options: CoderUiOptions): Prom

510 528
      return rows;
511 529
    };
512 530
513
    /**
514
     * The fleet block: one row per child.
515
     *
516
     * Drawn above the status line rather than in the transcript, because it is
517
     * live state and the transcript is a record. A reader who scrolled back to
518
     * an earlier tool call still needs to see what the fleet is doing now.
519
     */
520
    const fleetLines = (snapshot: CoderSnapshot, width: number): ReadonlyArray<string> => {
521
      const tasks = snapshot.tasks;
522
      if (tasks.length === 0) return [];
523
524
      // Working children first when there are more than fit: a finished child
525
      // has already been reported on the transcript, so it is the one to drop.
526
      const shown =
527
        tasks.length <= FLEET_ROWS_MAX
528
          ? tasks
529
          : [
530
              ...tasks.filter((task) => task.status === "running" || task.status === "pending"),
531
              ...tasks.filter((task) => task.status !== "running" && task.status !== "pending"),
532
            ].slice(0, FLEET_ROWS_MAX);
533
534
      const out = fleetRows(shown, Math.max(20, width - 8)).map((row) => {
535
        const color = fleetColor(row.status);
536
        return `  ${DIM}${row.branch}${RESET} ${color}${row.mark}${RESET} ${DIM}${row.text}${RESET}`;
537
      });
538
      const hidden = tasks.length - shown.length;
539
      if (hidden > 0) out.push(`  ${DIM}   +${String(hidden)} more${RESET}`);
540
      return out;
541
    };
542
543 531
    /** The newest tool call, which is the one ctrl+o expands. */
544 532
    const focusedTool = (snapshot: CoderSnapshot): string | undefined => {
545 533
      for (let index = snapshot.entries.length - 1; index >= 0; index -= 1) {

@@ -630,26 +618,18 @@ export function runCoderUi(session: CoderSession, options: CoderUiOptions): Prom

630 618
      }
631 619
      const transcriptHeight = Math.max(1, height - STATUS_ROWS - COMPOSER_ROWS - SPACER_ROWS);
632 620
633
      const fleet = fleetLines(snapshot, width);
634
      // The fleet takes its rows from the transcript, not from the chrome:
635
      // the composer stays where the reader's hands expect it. The status
636
      // line is priced into `transcriptHeight`, which keeps every frame the
637
      // same height whether children are running or not.
638
      const transcriptRows = Math.max(1, transcriptHeight - fleet.length);
639
640 621
      const lines = transcriptLines(snapshot, width);
641 622
      lineCount = lines.length;
642
      viewport = transcriptRows;
623
      viewport = transcriptHeight;
643 624
644
      const maxStart = Math.max(0, lines.length - transcriptRows);
625
      const maxStart = Math.max(0, lines.length - transcriptHeight);
645 626
      const start = anchor === undefined ? maxStart : Math.min(anchor, maxStart);
646 627
      // Kept for the status line: a reader scrolled up with nothing saying so
647 628
      // reads a still transcript as a stopped session.
648 629
      const above = start;
649 630
650 631
      const rows: string[] = [];
651
      for (let row = 0; row < transcriptRows; row += 1) rows.push(lines[start + row] ?? "");
652
      rows.push(...fleet);
632
      for (let row = 0; row < transcriptHeight; row += 1) rows.push(lines[start + row] ?? "");
653 633
654 634
      // Bottom chrome, in the order a reader scans it. The status line is the
655 635
      // main agent's state; the delegate preview lives inline under the

@@ -716,7 +696,7 @@ export function runCoderUi(session: CoderSession, options: CoderUiOptions): Prom

716 696
717 697
      paint(
718 698
        rows,
719
        transcriptRows + fleet.length + 3,
699
        transcriptHeight + 3,
720 700
        4 + [...visible].length + 1,
721 701
      );
722 702
    };
packages/openagents-cli/test/coder-delegate.test.ts modified -3

@@ -10,7 +10,6 @@ import {

10 10
} from "../src/coder-delegate.js";
11 11
import {
12 12
  activityPhrase,
13
  fleetPhrase,
14 13
  fleetRows,
15 14
  formatTokens,
16 15
  latestActivities,

@@ -330,7 +329,6 @@ describe("fleet rendering", () => {

330 329
    // tell a slow child from a stuck one.
331 330
    const now = running.startedAt + 95_000;
332 331
    expect(taskActivity(running, now)).toBe("bash(pnpm test) (1m 35s)");
333
    expect(fleetPhrase([running])).toBe("1 agent");
334 332
    const rows = fleetRows([running], 80, now);
335 333
    expect(rows[0]?.branch).toBe("└─");
336 334
    expect(rows[0]?.mark).toBe("◐");

@@ -344,7 +342,6 @@ describe("fleet rendering", () => {

344 342
    expect(done).toBeDefined();
345 343
    if (done === undefined) return;
346 344
    expect(taskActivity(done)).toBe("Done (1 tool use · 8.2k tokens · 3s)");
347
    expect(fleetPhrase([done])).toBe("1 done · 1 unread");
348 345
  });
349 346
350 347
  it("leaves usage out of a row until the harness has reported some", () => {
packages/openagents-cli/test/coder-ui.test.ts modified +3 -2

@@ -980,8 +980,9 @@ describe("the delegate preview inline under the tool call", () => {

980 980
981 981
    const delegateRow = rows.findIndex((row) => row.includes("delegate"));
982 982
    expect(delegateRow).toBeGreaterThan(0);
983
    const status = rows[delegateRow + 1] ?? "";
984
    expect(status).toContain("1 agent");
983
    const childRow = rows[delegateRow + 1] ?? "";
984
    expect(childRow).toContain("inspect the repo");
985
    expect(childRow).toContain("Initializing");
985 986
    // The session status lives at the bottom and does not repeat the fleet.
986 987
    const bottom = rows.filter((row) => row.includes("repo · main")).at(-1) ?? "";
987 988
    expect(bottom).toContain("repo · main");

This page updates live while a promote is in flight · changelog