@aragon-agent/cli 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +128 -1
  3. package/dist/agent/controller.d.ts +51 -1
  4. package/dist/agent/controller.js +177 -2
  5. package/dist/agent/headless.d.ts +12 -0
  6. package/dist/agent/headless.js +41 -1
  7. package/dist/agent/reducer.d.ts +48 -0
  8. package/dist/agent/reducer.js +91 -2
  9. package/dist/agent/system-prompt.d.ts +17 -0
  10. package/dist/agent/system-prompt.js +4 -0
  11. package/dist/cli.js +66 -2
  12. package/dist/commands/builtins.js +82 -0
  13. package/dist/config/app-paths.d.ts +17 -0
  14. package/dist/config/app-paths.js +21 -0
  15. package/dist/config/cli-commands.d.ts +26 -2
  16. package/dist/config/cli-commands.js +63 -4
  17. package/dist/config/env.d.ts +2 -2
  18. package/dist/config/env.js +28 -2
  19. package/dist/config/history-commands.d.ts +20 -0
  20. package/dist/config/history-commands.js +71 -0
  21. package/dist/config/load.d.ts +13 -0
  22. package/dist/config/load.js +65 -5
  23. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  24. package/dist/config/migrate-state-out-of-config.js +85 -0
  25. package/dist/config/prompt-history.d.ts +86 -0
  26. package/dist/config/prompt-history.js +332 -0
  27. package/dist/config/schema.d.ts +112 -16
  28. package/dist/config/schema.js +89 -6
  29. package/dist/config/store.js +13 -4
  30. package/dist/config/ui-state.d.ts +52 -0
  31. package/dist/config/ui-state.js +149 -0
  32. package/dist/logging/install.d.ts +85 -0
  33. package/dist/logging/install.js +94 -0
  34. package/dist/logging/logger.d.ts +1 -1
  35. package/dist/session/persist.d.ts +16 -0
  36. package/dist/session/persist.js +19 -1
  37. package/dist/team/activity.d.ts +67 -0
  38. package/dist/team/activity.js +130 -0
  39. package/dist/team/bus.d.ts +190 -0
  40. package/dist/team/bus.js +366 -0
  41. package/dist/team/comm-tools.d.ts +61 -0
  42. package/dist/team/comm-tools.js +221 -0
  43. package/dist/team/human-queue.d.ts +55 -0
  44. package/dist/team/human-queue.js +72 -0
  45. package/dist/team/limits.d.ts +123 -0
  46. package/dist/team/limits.js +128 -0
  47. package/dist/team/normalize.d.ts +39 -0
  48. package/dist/team/normalize.js +106 -0
  49. package/dist/team/panel-rows.d.ts +48 -0
  50. package/dist/team/panel-rows.js +75 -0
  51. package/dist/team/prompt.d.ts +49 -0
  52. package/dist/team/prompt.js +87 -0
  53. package/dist/team/report.d.ts +58 -0
  54. package/dist/team/report.js +205 -0
  55. package/dist/team/retry.d.ts +48 -0
  56. package/dist/team/retry.js +56 -0
  57. package/dist/team/runtime.d.ts +122 -0
  58. package/dist/team/runtime.js +406 -0
  59. package/dist/team/subagent.d.ts +90 -0
  60. package/dist/team/subagent.js +294 -0
  61. package/dist/team/task-tool.d.ts +47 -0
  62. package/dist/team/task-tool.js +118 -0
  63. package/dist/team/types.d.ts +172 -0
  64. package/dist/team/types.js +13 -0
  65. package/dist/tools/index.d.ts +48 -2
  66. package/dist/tools/index.js +26 -1
  67. package/dist/ui/App.js +97 -13
  68. package/dist/ui/StatusBar.d.ts +21 -0
  69. package/dist/ui/StatusBar.js +5 -2
  70. package/dist/ui/TeamPanel.d.ts +57 -0
  71. package/dist/ui/TeamPanel.js +134 -0
  72. package/dist/ui/Transcript.js +10 -0
  73. package/dist/ui/entries/TeamCard.d.ts +31 -0
  74. package/dist/ui/entries/TeamCard.js +39 -0
  75. package/dist/ui/glyphs.d.ts +10 -0
  76. package/dist/ui/glyphs.js +6 -0
  77. package/dist/ui/layout/AppShell.d.ts +11 -1
  78. package/dist/ui/layout/AppShell.js +3 -3
  79. package/dist/ui/overlays/HelpOverlay.js +12 -0
  80. package/dist/ui/transcript-text.js +20 -0
  81. package/dist/ui/use-startup-notices.d.ts +1 -1
  82. package/dist/ui/use-startup-notices.js +2 -1
  83. package/package.json +2 -2
@@ -0,0 +1,134 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { pickGlyphs } from './glyphs.js';
5
+ import { formatDuration } from '../agent/usage.js';
6
+ import { describeToolActivity, sanitizeActivity } from '../team/activity.js';
7
+ import { TEAM_LIMITS } from '../team/limits.js';
8
+ import { selectPanelRows } from '../team/panel-rows.js';
9
+ function isRunning(run) {
10
+ return run.phase === 'starting' || run.phase === 'thinking' || run.phase === 'tool' ||
11
+ run.phase === 'waiting';
12
+ }
13
+ function isSettled(run) {
14
+ return run.phase === 'done' || run.phase === 'failed' || run.phase === 'aborted';
15
+ }
16
+ function statusColor(run, theme) {
17
+ if (run.phase === 'failed' || run.phase === 'aborted')
18
+ return theme.toolError;
19
+ if (run.phase === 'done')
20
+ return theme.toolDone;
21
+ if (isRunning(run))
22
+ return theme.toolRunning;
23
+ return theme.muted;
24
+ }
25
+ /**
26
+ * How many characters the activity column may spend at this width (P0-1).
27
+ *
28
+ * `TEAM_LIMITS.activityChars` is a STORAGE ceiling; a terminal has a COLUMN
29
+ * budget, and the two are not the same number. The row is
30
+ *
31
+ * marker(3) + label(8) + description(flex) + 2 + activity + 2 + elapsed(~6)
32
+ *
33
+ * and the comment on the description cell below promises that activity and
34
+ * elapsed never lose characters, i.e. that `description` absorbs ALL width
35
+ * pressure. A 64-character prose tail plus `writing: ` is 73, which on an
36
+ * 80-column terminal needs 94 columns before `description` gets one. The promise
37
+ * cannot be kept by wishing; the column has to be sized from `cols`.
38
+ *
39
+ * `WIDE_CHAR_ALLOWANCE` is the concession to there being no display-width helper
40
+ * in production code: this budget is denominated in code units while the
41
+ * terminal is denominated in columns, and CJK and emoji occupy two columns per
42
+ * unit, so the budget is halved against the reserve rather than spent to the
43
+ * last cell. Erring small costs a few characters of prose; erring large costs
44
+ * the elapsed column.
45
+ */
46
+ const ROW_RESERVED_COLS = 3 + 8 + 2 + 2 + 8 + 12; // marker, label, gaps, elapsed, min description
47
+ const WIDE_CHAR_ALLOWANCE = 2;
48
+ export function activityBudget(cols) {
49
+ const spare = Math.floor((cols - ROW_RESERVED_COLS) / WIDE_CHAR_ALLOWANCE);
50
+ return Math.max(TEAM_LIMITS.activityMinChars, Math.min(TEAM_LIMITS.activityChars, spare));
51
+ }
52
+ /**
53
+ * The middle column: what this child is doing right now, in a few words.
54
+ *
55
+ * Two width policies, answering different questions. `wide` decides WHICH FACT
56
+ * to show - a whole path or its basename, a whole command line or its program
57
+ * name - mirroring `StatusBar`'s `statusCompactCols` degradation rather than
58
+ * inventing a second threshold. `activityBudget(cols)` decides HOW MUCH OF IT
59
+ * FITS, and it is a continuous function of width rather than a threshold,
60
+ * because the failure it prevents - a row wider than the terminal - is
61
+ * continuous.
62
+ *
63
+ * Exported for the same reason `buildSubagentTools` is: the width bound is the
64
+ * property this round has to keep, and asserting it through a rendered frame
65
+ * cannot distinguish "the arithmetic clamped it" from "Ink truncated it" - which
66
+ * is precisely the confusion that let v1's fixed-width column look fine at 100
67
+ * columns and break at 80.
68
+ */
69
+ export function activityLine(run, cols) {
70
+ const wide = cols >= TEAM_LIMITS.activityWideCols;
71
+ const budget = activityBudget(cols);
72
+ switch (run.phase) {
73
+ case 'queued':
74
+ return 'queued';
75
+ case 'starting':
76
+ // The one row that legitimately restarts its clock, saying why (P2-4):
77
+ // F-2 ranks the running group by `startedAt` and a retry re-stamps it.
78
+ return run.retries ? 'starting (retry)' : 'starting';
79
+ case 'thinking':
80
+ // Clamped AT RENDER, against this terminal's budget. `run.activity` was
81
+ // clamped at STORE against `activityChars`; that is a ceiling, not a fit.
82
+ return run.activity ? `writing: ${sanitizeActivity(run.activity, budget)}` : 'thinking';
83
+ case 'tool':
84
+ case 'waiting':
85
+ return run.lastTool
86
+ ? sanitizeActivity(describeToolActivity(run.lastTool, run.activityArgs, wide), budget)
87
+ : run.phase === 'waiting'
88
+ ? 'waiting for mail'
89
+ : 'tool';
90
+ case 'done':
91
+ return `done ${run.turns} turns`;
92
+ case 'failed':
93
+ return 'failed';
94
+ case 'aborted':
95
+ return 'aborted';
96
+ }
97
+ }
98
+ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, }) {
99
+ const glyphs = pickGlyphs(caps);
100
+ const at = now ?? Date.now();
101
+ const running = snapshot.runs.filter(isRunning).length;
102
+ const done = snapshot.runs.filter(isSettled).length;
103
+ const elapsed = formatDuration(Math.max(0, at - snapshot.startedAt));
104
+ const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [running, " running ", glyphs.midDot, " ", done, " done ", glyphs.midDot, " ", elapsed] })] }), snapshot.messageCount > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.teamMail, " ", snapshot.messageCount] }))] }));
105
+ // Below 20 rows the roster is more expensive than the transcript it displaces,
106
+ // so the panel degrades to the one line that still answers "is something
107
+ // running, and for how long" (§6.4).
108
+ if (rows < TEAM_LIMITS.panelCollapseRows) {
109
+ return _jsx(Box, { flexDirection: "column", flexShrink: 0, children: header });
110
+ }
111
+ // RANKED, not sliced by array index (F-2). The slot pool hands out indices in
112
+ // order, so `slice(0, 5)` showed the five children that finished FIRST and hid
113
+ // everything still working behind `+N more` - at exactly the fan-out widths
114
+ // the requirement permits.
115
+ const { visible, hiddenTotal, hiddenRunning } = selectPanelRows(snapshot.runs, TEAM_LIMITS.panelMaxRows);
116
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [header, visible.map((run) => {
117
+ const color = statusColor(run, theme);
118
+ // One spinner per RUNNING row, and the row cap is 5 precisely because
119
+ // five is the documented maximum number of simultaneous spinners. Both
120
+ // `reducedMotion` and a non-Unicode terminal fall back to the same
121
+ // static marker, exactly as `AssistantEntry` and `ToolCard` already do.
122
+ const animate = isRunning(run) && !reducedMotion && caps.unicode;
123
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
124
+ }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject] }))] }));
125
+ }
126
+ function marker(run, glyphs) {
127
+ if (run.phase === 'done')
128
+ return glyphs.toolDone;
129
+ if (run.phase === 'failed' || run.phase === 'aborted')
130
+ return glyphs.toolError;
131
+ if (run.phase === 'queued')
132
+ return glyphs.toolPending;
133
+ return glyphs.spinnerStill;
134
+ }
@@ -26,6 +26,7 @@ import { EntryFrame } from './entries/EntryFrame.js';
26
26
  import { UserEntry } from './entries/UserEntry.js';
27
27
  import { AssistantEntry } from './entries/AssistantEntry.js';
28
28
  import { ToolCard, statusColor, toolEntryGlyph } from './entries/ToolCard.js';
29
+ import { TeamCard, teamCardColor } from './entries/TeamCard.js';
29
30
  /** Keep the last K entries in the live region so a mutating tail stays hot. */
30
31
  const LIVE_TAIL = 1;
31
32
  /**
@@ -47,6 +48,13 @@ export function computeSettledCount(entries, expandedToolIds) {
47
48
  break;
48
49
  if (e.kind === 'tool' && e.status !== 'done' && e.status !== 'error')
49
50
  break;
51
+ // A live dispatch is not settled: its `runs` are rewritten on every
52
+ // `teamUpdate`, and `<Static>` cannot un-print what it has already drawn.
53
+ // The boundary is held MONOTONIC above, which is also why a `team` entry
54
+ // that never settles would be re-rendered on every frame for the rest of the
55
+ // session — the failure `session/persist.ts` normalizes away on load (P1-5).
56
+ if (e.kind === 'team' && e.active)
57
+ break;
50
58
  count = i + 1;
51
59
  }
52
60
  return count;
@@ -81,6 +89,8 @@ function EntryView({ entry, prev, expanded, thinkingVisible, reducedMotion, dens
81
89
  }
82
90
  case 'tool':
83
91
  return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
92
+ case 'team':
93
+ return frame(glyphs.teamAgent, teamCardColor(entry.runs, entry.active, entry.aborted, theme), _jsx(TeamCard, { requested: entry.requested, runs: entry.runs, aborted: entry.aborted, durationMs: entry.durationMs, active: entry.active, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
84
94
  case 'notice':
85
95
  return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
86
96
  default:
@@ -0,0 +1,31 @@
1
+ /**
2
+ * TeamCard — the settled-dispatch transcript entry (team-subagents §6.3).
3
+ *
4
+ * ASCII ONLY: every glyph comes from `pickGlyphs`.
5
+ *
6
+ * The panel is EPHEMERAL and this card is the HISTORY. It renders like a
7
+ * `ToolCard` — same `EntryFrame` supplied by `EntryView`, same left rail for the
8
+ * expanded body — so a dispatch reads as one more step in the transcript rather
9
+ * than a second kind of object.
10
+ *
11
+ * `Ctrl+O` expands it to the per-agent summaries, which is why `App.tsx`'s
12
+ * Ctrl+O finder had to widen from `kind === 'tool'` to `tool || team`.
13
+ */
14
+ import React from 'react';
15
+ import type { Theme } from '../theme.js';
16
+ import type { TermCapabilities } from '../capabilities.js';
17
+ import type { SubagentRun } from '../../team/types.js';
18
+ export interface TeamCardProps {
19
+ requested: number;
20
+ runs: SubagentRun[];
21
+ aborted: boolean;
22
+ durationMs?: number;
23
+ active: boolean;
24
+ expanded?: boolean;
25
+ reducedMotion?: boolean;
26
+ theme: Theme;
27
+ caps: TermCapabilities;
28
+ }
29
+ /** The card's aggregate colour: any failure dominates. */
30
+ export declare function teamCardColor(runs: SubagentRun[], active: boolean, aborted: boolean, theme: Theme): string | undefined;
31
+ export declare function TeamCard(props: TeamCardProps): React.ReactElement;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { pickGlyphs } from '../glyphs.js';
5
+ import { railBorderProps } from '../layout/Gutter.js';
6
+ import { formatDuration } from '../../agent/usage.js';
7
+ function isOk(run) {
8
+ return run.phase === 'done' && !run.error;
9
+ }
10
+ /** The card's aggregate colour: any failure dominates. */
11
+ export function teamCardColor(runs, active, aborted, theme) {
12
+ if (active)
13
+ return theme.toolRunning;
14
+ if (aborted || runs.some((r) => !isOk(r)))
15
+ return theme.toolError;
16
+ return theme.toolDone;
17
+ }
18
+ export function TeamCard(props) {
19
+ const { requested, runs, aborted, durationMs, active, expanded, reducedMotion, theme, caps } = props;
20
+ const glyphs = pickGlyphs(caps);
21
+ const color = teamCardColor(runs, active, aborted, theme);
22
+ const ok = runs.filter(isOk).length;
23
+ const failed = runs.length - ok;
24
+ // A dispatch entry restored from `/resume` is settled and aborted but has no
25
+ // duration: nothing measured it, because the process it belonged to is gone.
26
+ // Saying so plainly beats rendering `0.0s (0 ok, 3 failed)`, which reads like
27
+ // a dispatch that ran and lost (§5.3 / P1-5).
28
+ const interrupted = !active && aborted && durationMs === undefined;
29
+ const settled = runs.filter((r) => r.phase === 'done' || r.phase === 'failed' || r.phase === 'aborted').length;
30
+ const headline = interrupted
31
+ ? 'interrupted (session resumed)'
32
+ : active
33
+ ? `${runs.length - settled} running`
34
+ : `${formatDuration(durationMs ?? 0)} (${ok} ok, ${failed} failed)`;
35
+ const count = `${runs.length} ${runs.length === 1 ? 'subagent' : 'subagents'}`;
36
+ const dropped = requested > runs.length ? ` (${runs.length} of ${requested} requested)` : '';
37
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isOk(run) ? theme.toolDone : theme.toolError, children: isOk(run) ? glyphs.toolDone : glyphs.toolError }), _jsxs(Text, { color: theme.primary, children: [" ", run.label.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? 0) - run.startedAt)) : '', ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] })] }, run.label))), expanded &&
38
+ runs.map((run) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: [run.label, " ", glyphs.midDot, " ", run.description] }), _jsx(Text, { color: theme.muted, children: run.summary ?? '(no summary)' })] }, `s-${run.label}`))), !expanded && !active && runs.length > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", runs.length, " ", runs.length === 1 ? 'summary' : 'summaries', " (Ctrl+O)"] })), expanded && _jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" })] })] }));
39
+ }
@@ -86,6 +86,16 @@ export interface Glyphs {
86
86
  scrollThumb: string;
87
87
  /** Redaction dot for `maskSecret` in the settings screen (§4.1 tier B). */
88
88
  maskDot: string;
89
+ /**
90
+ * Team mode (team-subagents §6.1). The rail marker for a dispatch card and
91
+ * the prefix on the panel's incoming-message preview.
92
+ *
93
+ * They live HERE rather than in `TeamPanel` / `TeamCard` for the reason this
94
+ * file's header gives: a literal in a component bypasses `pickGlyphs` entirely
95
+ * and shows mojibake on a legacy console, whatever the capability probe said.
96
+ */
97
+ teamAgent: string;
98
+ teamMail: string;
89
99
  /**
90
100
  * Ink `borderStyle` name for every boxed region (composer, completion popup,
91
101
  * overlay frame).
package/dist/ui/glyphs.js CHANGED
@@ -59,6 +59,8 @@ const UNICODE_GLYPHS = {
59
59
  scrollThumb: '█',
60
60
  maskDot: '•',
61
61
  boxStyle: 'round',
62
+ teamAgent: '◆',
63
+ teamMail: '✉',
62
64
  tool: {
63
65
  read_file: '▤',
64
66
  write_file: '✎',
@@ -69,6 +71,7 @@ const UNICODE_GLYPHS = {
69
71
  list_dir: '▤',
70
72
  ask_user: '?',
71
73
  submit_plan: '◈',
74
+ task: '◆',
72
75
  },
73
76
  toolDefault: '•',
74
77
  };
@@ -113,6 +116,8 @@ const ASCII_GLYPHS = {
113
116
  scrollThumb: '#',
114
117
  maskDot: '*',
115
118
  boxStyle: 'classic',
119
+ teamAgent: '*',
120
+ teamMail: '@',
116
121
  tool: {
117
122
  read_file: '=',
118
123
  write_file: '+',
@@ -123,6 +128,7 @@ const ASCII_GLYPHS = {
123
128
  list_dir: '=',
124
129
  ask_user: '?',
125
130
  submit_plan: '#',
131
+ task: '*',
126
132
  },
127
133
  toolDefault: '*',
128
134
  };
@@ -18,6 +18,16 @@ export interface AppShellProps {
18
18
  cols: number;
19
19
  header: React.ReactNode;
20
20
  viewport: React.ReactNode;
21
+ /**
22
+ * The live subagent roster (team-subagents §6.1), or nothing.
23
+ *
24
+ * IT RENDERS INSIDE `bottomRef` IN THE FULL-SCREEN BRANCH, and it has to
25
+ * (I-10): `geometryRef.current.bottom` is MEASURED from that box, and the
26
+ * wheel router uses it to decide whether a notch belongs to the transcript or
27
+ * to prompt history. A panel rendered above the measured box would make the
28
+ * composer band boundary stale the instant a dispatch started.
29
+ */
30
+ team?: React.ReactNode;
21
31
  toast: React.ReactNode;
22
32
  composer: React.ReactNode;
23
33
  status: React.ReactNode;
@@ -30,4 +40,4 @@ export interface AppShellProps {
30
40
  */
31
41
  geometryRef?: React.MutableRefObject<FrameGeometry>;
32
42
  }
33
- export declare function AppShell({ mode, rows, cols, header, viewport, toast, composer, status, geometryRef, }: AppShellProps): React.ReactElement;
43
+ export declare function AppShell({ mode, rows, cols, header, viewport, team, toast, composer, status, geometryRef, }: AppShellProps): React.ReactElement;
@@ -14,7 +14,7 @@ import { useLayoutEffect, useRef } from 'react';
14
14
  import { Box, measureElement } from 'ink';
15
15
  import { frameHeight } from './frame.js';
16
16
  import { UNMEASURED_GEOMETRY } from './regions.js';
17
- export function AppShell({ mode, rows, cols, header, viewport, toast, composer, status, geometryRef, }) {
17
+ export function AppShell({ mode, rows, cols, header, viewport, team, toast, composer, status, geometryRef, }) {
18
18
  const bottomRef = useRef(null);
19
19
  // DECLARED ABOVE THE INLINE EARLY RETURN, and it has to stay there: React
20
20
  // keys hooks by call order, and a hook that only runs in one mode changes
@@ -38,7 +38,7 @@ export function AppShell({ mode, rows, cols, header, viewport, toast, composer,
38
38
  };
39
39
  });
40
40
  if (mode === 'inline') {
41
- return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, toast, composer, status] }));
41
+ return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, team, toast, composer, status] }));
42
42
  }
43
- return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsxs(Box, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [toast, composer, status] })] }));
43
+ return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsxs(Box, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [team, toast, composer, status] })] }));
44
44
  }
@@ -25,6 +25,7 @@ const COMMANDS = [
25
25
  ['/plan [on|off|status]', 'Toggle plan mode (same as Shift+Tab)'],
26
26
  ['/theme <name>', 'auto | warm | cool | light'],
27
27
  ['/thinking <level>', 'Set thinking level'],
28
+ ['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
28
29
  ['/tools', 'List active tools'],
29
30
  ['/clear', 'Clear the visible transcript'],
30
31
  ['/reset', 'New conversation'],
@@ -53,6 +54,14 @@ const PLAN_ROWS = [
53
54
  ['submit_plan', 'The agent submits a plan: a approve, r revise, Esc dismiss'],
54
55
  ['--plan / --no-plan', 'Start a session in a given mode (also ARAGON_PLAN=1)'],
55
56
  ];
57
+ const TEAM_ROWS = [
58
+ ['task', 'The tool the agent uses to run 2-5 subagents in parallel'],
59
+ ['/team', 'Status: on/off, max subagents, and the live roster'],
60
+ ['/team on | off', 'Switch team mode for this session (and save it)'],
61
+ ['/team max <n>', 'Fan-out width, 1-10 (5 by default)'],
62
+ ['--team / --no-team', 'Start a session with team mode on or off (also ARAGON_TEAM=0)'],
63
+ ['team_send / team_wait', 'How subagents message each other; 6 each, one every 15s'],
64
+ ];
56
65
  /** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
57
66
  export function helpRows(theme, caps) {
58
67
  const glyphs = pickGlyphs(caps);
@@ -68,6 +77,9 @@ export function helpRows(theme, caps) {
68
77
  rows.push(section('Plan mode'));
69
78
  for (const [c, d] of PLAN_ROWS)
70
79
  rows.push(pair(c, d));
80
+ rows.push(section('Team subagents'));
81
+ for (const [c, d] of TEAM_ROWS)
82
+ rows.push(pair(c, d));
71
83
  rows.push(section('Skills'));
72
84
  for (const [c, d] of SKILL_ROWS)
73
85
  rows.push(pair(c, d));
@@ -64,6 +64,26 @@ function renderEntry(entry, glyphs) {
64
64
  const duration = entry.durationMs === undefined ? '' : ` (${entry.durationMs}ms)`;
65
65
  return [`${TOOL_INDENT}${glyph} ${entry.label || entry.name}${args ? ` ${args}` : ''}${duration}`];
66
66
  }
67
+ case 'team': {
68
+ // Same rows as `TeamCard`, without colour (team-subagents §6.3). A
69
+ // dispatch is often the most expensive thing in a session, so leaving it
70
+ // out of the plain-text replay would make the exported transcript
71
+ // unaccountable for most of its own cost.
72
+ const ok = entry.runs.filter((r) => r.phase === 'done' && !r.error).length;
73
+ const failed = entry.runs.length - ok;
74
+ const head = entry.active || (entry.aborted && entry.durationMs === undefined)
75
+ ? 'interrupted'
76
+ : `${entry.durationMs ?? 0}ms (${ok} ok, ${failed} failed)`;
77
+ const lines = [
78
+ `${TOOL_INDENT}team ${entry.runs.length} subagents ${head}`,
79
+ ...entry.runs.map((run) => {
80
+ const glyph = run.phase === 'done' && !run.error ? glyphs.toolDone : glyphs.toolError;
81
+ const detail = run.error ? ` ${run.error}` : '';
82
+ return `${TOOL_INDENT}${CONTINUATION_INDENT}${glyph} ${run.label} ${run.description}${detail}`;
83
+ }),
84
+ ];
85
+ return lines;
86
+ }
67
87
  case 'notice': {
68
88
  const glyph = entry.level === 'error'
69
89
  ? glyphs.error
@@ -19,7 +19,7 @@ export interface MouseNoticeOptions {
19
19
  * must never be given advice about a mode that is not running.
20
20
  */
21
21
  enabled: boolean;
22
- /** Persist `mouseNoticeSeen`. Called once, after the notice is dispatched. */
22
+ /** Record that it was shown (`state.json`). Called once, after dispatch. */
23
23
  onSeen: () => void;
24
24
  }
25
25
  export declare function useStartupNotices(dispatch: (action: ViewAction) => void, mouseNotice?: MouseNoticeOptions): void;
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { useEffect, useRef } from 'react';
15
15
  import { readConfigFile } from '../config/store.js';
16
+ import { getMouseNoticeSeen } from '../config/ui-state.js';
16
17
  import { getLogger } from '../logging/logger.js';
17
18
  /**
18
19
  * The §6.1 notice (mouse-wheel-region-routing). Shown at most once, ever.
@@ -55,7 +56,7 @@ export function useStartupNotices(dispatch, mouseNotice) {
55
56
  useEffect(() => {
56
57
  if (!mouseEnabled || mouseNoticeShown.current)
57
58
  return;
58
- if (readConfigFile().config?.mouseNoticeSeen)
59
+ if (getMouseNoticeSeen())
59
60
  return;
60
61
  mouseNoticeShown.current = true;
61
62
  dispatch({ type: 'notice', level: 'info', text: MOUSE_SELECTION_NOTICE });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -51,7 +51,7 @@
51
51
  "prepublishOnly": "npm run build"
52
52
  },
53
53
  "dependencies": {
54
- "@aragon-agent/core": "^0.2.2",
54
+ "@aragon-agent/core": "^0.2.3",
55
55
  "chalk": "^5.6.2",
56
56
  "cli-highlight": "^2.0.0",
57
57
  "commander": "^12.1.0",