@aragon-agent/cli 0.5.2 → 0.5.4
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.
- package/CHANGELOG.md +127 -0
- package/README.md +193 -3
- package/dist/agent/controller.d.ts +51 -1
- package/dist/agent/controller.js +177 -2
- package/dist/agent/headless.d.ts +12 -0
- package/dist/agent/headless.js +41 -1
- package/dist/agent/reducer.d.ts +48 -0
- package/dist/agent/reducer.js +91 -2
- package/dist/agent/system-prompt.d.ts +17 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +98 -7
- package/dist/commands/builtins.d.ts +2 -2
- package/dist/commands/builtins.js +133 -3
- package/dist/config/app-paths.d.ts +17 -0
- package/dist/config/app-paths.js +21 -0
- package/dist/config/cli-commands.d.ts +26 -2
- package/dist/config/cli-commands.js +83 -6
- package/dist/config/env.d.ts +3 -2
- package/dist/config/env.js +44 -6
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +107 -6
- package/dist/config/migrate-state-out-of-config.d.ts +32 -0
- package/dist/config/migrate-state-out-of-config.js +85 -0
- package/dist/config/prompt-history.d.ts +86 -0
- package/dist/config/prompt-history.js +332 -0
- package/dist/config/schema.d.ts +176 -18
- package/dist/config/schema.js +162 -8
- package/dist/config/store.js +40 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/logging/install.d.ts +85 -0
- package/dist/logging/install.js +94 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.d.ts +16 -0
- package/dist/session/persist.js +19 -1
- package/dist/team/activity.d.ts +67 -0
- package/dist/team/activity.js +130 -0
- package/dist/team/bus.d.ts +190 -0
- package/dist/team/bus.js +366 -0
- package/dist/team/comm-tools.d.ts +61 -0
- package/dist/team/comm-tools.js +221 -0
- package/dist/team/human-queue.d.ts +55 -0
- package/dist/team/human-queue.js +72 -0
- package/dist/team/limits.d.ts +123 -0
- package/dist/team/limits.js +128 -0
- package/dist/team/normalize.d.ts +39 -0
- package/dist/team/normalize.js +106 -0
- package/dist/team/panel-rows.d.ts +48 -0
- package/dist/team/panel-rows.js +75 -0
- package/dist/team/prompt.d.ts +49 -0
- package/dist/team/prompt.js +87 -0
- package/dist/team/report.d.ts +58 -0
- package/dist/team/report.js +205 -0
- package/dist/team/retry.d.ts +48 -0
- package/dist/team/retry.js +56 -0
- package/dist/team/runtime.d.ts +122 -0
- package/dist/team/runtime.js +406 -0
- package/dist/team/subagent.d.ts +90 -0
- package/dist/team/subagent.js +294 -0
- package/dist/team/task-tool.d.ts +47 -0
- package/dist/team/task-tool.js +118 -0
- package/dist/team/types.d.ts +172 -0
- package/dist/team/types.js +13 -0
- package/dist/tools/index.d.ts +48 -2
- package/dist/tools/index.js +26 -1
- package/dist/ui/App.d.ts +22 -1
- package/dist/ui/App.js +144 -18
- package/dist/ui/StatusBar.d.ts +21 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TeamPanel.d.ts +57 -0
- package/dist/ui/TeamPanel.js +134 -0
- package/dist/ui/Transcript.js +10 -0
- package/dist/ui/entries/TeamCard.d.ts +31 -0
- package/dist/ui/entries/TeamCard.js +39 -0
- package/dist/ui/glyphs.d.ts +10 -0
- package/dist/ui/glyphs.js +6 -0
- package/dist/ui/layout/AppShell.d.ts +11 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/overlays/HelpOverlay.js +13 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
- package/dist/ui/overlays/SettingsScreen.js +38 -1
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +1 -1
- package/dist/ui/use-startup-notices.js +2 -1
- 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
|
+
}
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/ui/glyphs.d.ts
CHANGED
|
@@ -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,8 @@ 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
|
+
['/max-tokens [n|auto]', 'Output token cap; no argument reports the effective one'],
|
|
29
|
+
['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
|
|
28
30
|
['/tools', 'List active tools'],
|
|
29
31
|
['/clear', 'Clear the visible transcript'],
|
|
30
32
|
['/reset', 'New conversation'],
|
|
@@ -53,6 +55,14 @@ const PLAN_ROWS = [
|
|
|
53
55
|
['submit_plan', 'The agent submits a plan: a approve, r revise, Esc dismiss'],
|
|
54
56
|
['--plan / --no-plan', 'Start a session in a given mode (also ARAGON_PLAN=1)'],
|
|
55
57
|
];
|
|
58
|
+
const TEAM_ROWS = [
|
|
59
|
+
['task', 'The tool the agent uses to run 2-5 subagents in parallel'],
|
|
60
|
+
['/team', 'Status: on/off, max subagents, and the live roster'],
|
|
61
|
+
['/team on | off', 'Switch team mode for this session (and save it)'],
|
|
62
|
+
['/team max <n>', 'Fan-out width, 1-10 (5 by default)'],
|
|
63
|
+
['--team / --no-team', 'Start a session with team mode on or off (also ARAGON_TEAM=0)'],
|
|
64
|
+
['team_send / team_wait', 'How subagents message each other; 6 each, one every 15s'],
|
|
65
|
+
];
|
|
56
66
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
57
67
|
export function helpRows(theme, caps) {
|
|
58
68
|
const glyphs = pickGlyphs(caps);
|
|
@@ -68,6 +78,9 @@ export function helpRows(theme, caps) {
|
|
|
68
78
|
rows.push(section('Plan mode'));
|
|
69
79
|
for (const [c, d] of PLAN_ROWS)
|
|
70
80
|
rows.push(pair(c, d));
|
|
81
|
+
rows.push(section('Team subagents'));
|
|
82
|
+
for (const [c, d] of TEAM_ROWS)
|
|
83
|
+
rows.push(pair(c, d));
|
|
71
84
|
rows.push(section('Skills'));
|
|
72
85
|
for (const [c, d] of SKILL_ROWS)
|
|
73
86
|
rows.push(pair(c, d));
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* otherwise.
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import type
|
|
11
|
+
import { type ThinkingLevel } from '@aragon-agent/core';
|
|
12
12
|
import { type LogLevelName } from '../../logging/levels.js';
|
|
13
13
|
import type { Theme } from '../theme.js';
|
|
14
14
|
import type { TermCapabilities } from '../capabilities.js';
|
|
@@ -40,5 +40,16 @@ interface SettingsScreenProps {
|
|
|
40
40
|
caps: TermCapabilities;
|
|
41
41
|
onSave: (values: SettingsValues) => void;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The cap this setting will ACTUALLY produce against the selected model.
|
|
45
|
+
*
|
|
46
|
+
* Exported for the tests, and computed on every render rather than in an effect:
|
|
47
|
+
* `resolveOutputTokens` is pure, allocation-light and does no I/O — the ceiling
|
|
48
|
+
* lookup is a `Map.get` plus at most seven regex tests.
|
|
49
|
+
*
|
|
50
|
+
* Without this row the settings screen can only show what the user typed, and
|
|
51
|
+
* "I set 64000 but answers still stop early" has no answer anywhere in the UI.
|
|
52
|
+
*/
|
|
53
|
+
export declare function effectiveCapLine(values: Pick<SettingsValues, 'provider' | 'model' | 'maxTokens'>): string;
|
|
43
54
|
export declare function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, }: SettingsScreenProps): React.ReactElement;
|
|
44
55
|
export {};
|
|
@@ -10,7 +10,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
*/
|
|
11
11
|
import { useState } from 'react';
|
|
12
12
|
import { Text, useInput } from 'ink';
|
|
13
|
-
import {
|
|
13
|
+
import { DEFAULT_MAX_OUTPUT_TOKENS, resolveOutputTokens, } from '@aragon-agent/core';
|
|
14
|
+
import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret, parseMaxTokensInput, } from '../../config/schema.js';
|
|
14
15
|
import { LOG_LEVEL_NAMES } from '../../logging/levels.js';
|
|
15
16
|
import { pickGlyphs } from '../glyphs.js';
|
|
16
17
|
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
@@ -23,6 +24,34 @@ const FIELDS = [
|
|
|
23
24
|
{ key: 'apiKey', label: 'API key', kind: 'secret' },
|
|
24
25
|
{ key: 'logLevel', label: 'Log level', kind: 'enum', options: LOG_LEVEL_NAMES },
|
|
25
26
|
];
|
|
27
|
+
/**
|
|
28
|
+
* The cap this setting will ACTUALLY produce against the selected model.
|
|
29
|
+
*
|
|
30
|
+
* Exported for the tests, and computed on every render rather than in an effect:
|
|
31
|
+
* `resolveOutputTokens` is pure, allocation-light and does no I/O — the ceiling
|
|
32
|
+
* lookup is a `Map.get` plus at most seven regex tests.
|
|
33
|
+
*
|
|
34
|
+
* Without this row the settings screen can only show what the user typed, and
|
|
35
|
+
* "I set 64000 but answers still stop early" has no answer anywhere in the UI.
|
|
36
|
+
*/
|
|
37
|
+
export function effectiveCapLine(values) {
|
|
38
|
+
const parsed = parseMaxTokensInput(values.maxTokens);
|
|
39
|
+
if (parsed.kind === 'invalid')
|
|
40
|
+
return 'Effective: - (not a number; use a value or "auto")';
|
|
41
|
+
const requested = parsed.kind === 'value' ? parsed.value : undefined;
|
|
42
|
+
const resolution = resolveOutputTokens({
|
|
43
|
+
providerId: values.provider,
|
|
44
|
+
modelId: values.model,
|
|
45
|
+
...(requested !== undefined ? { requested } : {}),
|
|
46
|
+
});
|
|
47
|
+
if (resolution.clampedBy === 'ceiling') {
|
|
48
|
+
const ambition = requested ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
49
|
+
return `Effective: ${resolution.value} (clamped from ${ambition} by the ${values.model} ceiling)`;
|
|
50
|
+
}
|
|
51
|
+
if (requested === undefined)
|
|
52
|
+
return `Effective: ${resolution.value} (auto)`;
|
|
53
|
+
return `Effective: ${resolution.value}`;
|
|
54
|
+
}
|
|
26
55
|
export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, }) {
|
|
27
56
|
const [values, setValues] = useState(initial);
|
|
28
57
|
const [index, setIndex] = useState(0);
|
|
@@ -99,6 +128,10 @@ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset,
|
|
|
99
128
|
}
|
|
100
129
|
if (f.kind === 'enum')
|
|
101
130
|
return raw;
|
|
131
|
+
// An empty cap field is AUTO, not "unset". `(none)` would read as a missing
|
|
132
|
+
// setting, which is the one thing this field can never be.
|
|
133
|
+
if (f.key === 'maxTokens' && raw.length === 0)
|
|
134
|
+
return 'auto';
|
|
102
135
|
return raw.length > 0 ? raw : selected ? '(type to set)' : '(none)';
|
|
103
136
|
};
|
|
104
137
|
// One element per row, each `wrap="truncate"` — the contract for OverlayFrame's
|
|
@@ -108,6 +141,10 @@ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset,
|
|
|
108
141
|
const selected = i === index;
|
|
109
142
|
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: selected ? theme.accent : theme.muted, children: selected ? `${glyphs.caret} ` : ' ' }), _jsx(Text, { color: selected ? theme.assistant : theme.muted, children: (f.key === 'apiKey' ? `API key (${values.provider})` : f.label).padEnd(18) }), _jsx(Text, { color: selected ? theme.primary : theme.muted, children: displayValue(f, selected) }), f.kind === 'enum' && selected && (_jsxs(Text, { color: theme.muted, children: [' (', glyphs.arrowLeft, "/", glyphs.arrowRight, ')'] }))] }, f.key));
|
|
110
143
|
});
|
|
144
|
+
// One element, `wrap="truncate"`, pushed like every other row — OverlayFrame's
|
|
145
|
+
// controlled mode slices by ELEMENT, so a row that wraps desynchronises the
|
|
146
|
+
// position indicator.
|
|
147
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: effectiveCapLine(values) }, "__effective"));
|
|
111
148
|
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: "Enter saves. Keys persist to the config file (0600)." }, "__save"));
|
|
112
149
|
return (_jsx(OverlayFrame, { title: "Settings", hint: `${glyphs.arrowUp}${glyphs.arrowDown} field ${glyphs.midDot} ${glyphs.arrowLeft}${glyphs.arrowRight} change ${glyphs.midDot} Enter save ${glyphs.midDot} Esc close`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
|
|
113
150
|
}
|
|
@@ -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
|
-
/**
|
|
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 (
|
|
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.
|
|
3
|
+
"version": "0.5.4",
|
|
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.
|
|
54
|
+
"@aragon-agent/core": "^0.2.4",
|
|
55
55
|
"chalk": "^5.6.2",
|
|
56
56
|
"cli-highlight": "^2.0.0",
|
|
57
57
|
"commander": "^12.1.0",
|