@aragon-agent/cli 0.6.35 → 0.6.36
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 +21 -0
- package/README.md +3173 -3071
- package/dist/agent/agent-mode.d.ts +20 -4
- package/dist/agent/agent-mode.js +20 -4
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +64 -4
- package/dist/agent/headless.js +31 -0
- package/dist/agent/reducer.d.ts +13 -2
- package/dist/agent/reducer.js +1 -0
- package/dist/cli.js +12 -0
- package/dist/commands/agents-command.d.ts +15 -0
- package/dist/commands/agents-command.js +27 -0
- package/dist/commands/builtins.js +131 -4
- package/dist/commands/perf.d.ts +11 -0
- package/dist/commands/perf.js +2 -1
- package/dist/commands/registry.d.ts +14 -0
- package/dist/config/app-paths.d.ts +15 -0
- package/dist/config/app-paths.js +33 -2
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +28 -2
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +8 -1
- package/dist/config/schema.d.ts +68 -0
- package/dist/config/schema.js +39 -0
- package/dist/config/store.js +12 -1
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +25 -0
- package/dist/exec/events.js +4 -0
- package/dist/exec/index.js +16 -1
- package/dist/exec/runner.js +10 -0
- package/dist/proc/limits.d.ts +16 -0
- package/dist/proc/limits.js +16 -0
- package/dist/proc/supervisor.d.ts +20 -1
- package/dist/proc/supervisor.js +42 -14
- package/dist/prompts/command.d.ts +34 -0
- package/dist/prompts/command.js +237 -0
- package/dist/prompts/copy.d.ts +89 -0
- package/dist/prompts/copy.js +131 -0
- package/dist/prompts/fill.d.ts +51 -0
- package/dist/prompts/fill.js +46 -0
- package/dist/prompts/limits.d.ts +53 -0
- package/dist/prompts/limits.js +53 -0
- package/dist/prompts/normalize.d.ts +53 -0
- package/dist/prompts/normalize.js +190 -0
- package/dist/prompts/resolve.d.ts +31 -0
- package/dist/prompts/resolve.js +60 -0
- package/dist/prompts/search.d.ts +58 -0
- package/dist/prompts/search.js +146 -0
- package/dist/prompts/store.d.ts +86 -0
- package/dist/prompts/store.js +453 -0
- package/dist/prompts/types.d.ts +204 -0
- package/dist/prompts/types.js +13 -0
- package/dist/session/checkpoint.d.ts +8 -1
- package/dist/session/checkpoint.js +6 -0
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +13 -1
- package/dist/session/startup-session.d.ts +7 -2
- package/dist/session/startup-session.js +5 -2
- package/dist/session/store.js +3 -0
- package/dist/session/validate-session.js +64 -1
- package/dist/team/limits.d.ts +25 -0
- package/dist/team/limits.js +26 -0
- package/dist/team/runtime.js +27 -2
- package/dist/team/settled.d.ts +57 -0
- package/dist/team/settled.js +87 -0
- package/dist/team/subagent.js +48 -2
- package/dist/team/timeline.d.ts +74 -0
- package/dist/team/timeline.js +101 -0
- package/dist/team/types.d.ts +24 -1
- package/dist/todo/acceptance.d.ts +120 -0
- package/dist/todo/acceptance.js +399 -0
- package/dist/todo/follow-through.d.ts +48 -23
- package/dist/todo/follow-through.js +110 -44
- package/dist/todo/limits.d.ts +62 -1
- package/dist/todo/limits.js +62 -1
- package/dist/todo/panel-rows.d.ts +29 -39
- package/dist/todo/panel-rows.js +37 -83
- package/dist/todo/plan-sections.d.ts +60 -0
- package/dist/todo/plan-sections.js +108 -0
- package/dist/todo/prompt.d.ts +17 -12
- package/dist/todo/prompt.js +59 -40
- package/dist/todo/row-window.d.ts +51 -0
- package/dist/todo/row-window.js +107 -0
- package/dist/todo/store.d.ts +139 -31
- package/dist/todo/store.js +271 -73
- package/dist/todo/todo-tool.d.ts +33 -8
- package/dist/todo/todo-tool.js +186 -30
- package/dist/todo/types.d.ts +58 -0
- package/dist/ui/App.js +310 -46
- package/dist/ui/Composer.d.ts +6 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +2 -2
- package/dist/ui/PromptInput.d.ts +21 -1
- package/dist/ui/PromptInput.js +21 -1
- package/dist/ui/ScrollbackShell.d.ts +19 -19
- package/dist/ui/ScrollbackShell.js +8 -1
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/TeamPanel.d.ts +26 -44
- package/dist/ui/TeamPanel.js +46 -135
- package/dist/ui/TodoPanel.d.ts +17 -14
- package/dist/ui/TodoPanel.js +146 -38
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/composer-input.d.ts +10 -0
- package/dist/ui/composer-input.js +57 -24
- package/dist/ui/entries/TodoCard.d.ts +26 -11
- package/dist/ui/entries/TodoCard.js +56 -15
- package/dist/ui/glyphs.d.ts +19 -0
- package/dist/ui/glyphs.js +4 -0
- package/dist/ui/layout/agents-board.d.ts +102 -0
- package/dist/ui/layout/agents-board.js +389 -0
- package/dist/ui/layout/cells.d.ts +61 -0
- package/dist/ui/layout/cells.js +156 -0
- package/dist/ui/layout/header-hints.d.ts +13 -3
- package/dist/ui/layout/header-hints.js +48 -14
- package/dist/ui/layout/status-detail-layout.js +9 -0
- package/dist/ui/layout/status-layout.d.ts +13 -0
- package/dist/ui/layout/team-row.d.ts +139 -0
- package/dist/ui/layout/team-row.js +246 -0
- package/dist/ui/layout/todo-layout.d.ts +38 -0
- package/dist/ui/layout/todo-layout.js +75 -0
- package/dist/ui/layout/virtual-window.js +23 -3
- package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
- package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
- package/dist/ui/overlays/HelpOverlay.js +17 -2
- package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
- package/dist/ui/overlays/PromptEditorPage.js +227 -0
- package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
- package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
- package/dist/ui/overlays/PromptPicker.d.ts +36 -0
- package/dist/ui/overlays/PromptPicker.js +129 -0
- package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
- package/dist/ui/overlays/PromptTextArea.js +141 -0
- package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
- package/dist/ui/overlays/prompt-library-state.js +335 -0
- package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
- package/dist/ui/overlays/prompt-picker-state.js +98 -0
- package/dist/ui/scrollback/band.js +14 -1
- package/dist/ui/team-status.d.ts +25 -0
- package/dist/ui/team-status.js +31 -3
- package/dist/ui/use-startup-notices.d.ts +35 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.js +9 -1
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agents board's row model — pure projection, no Ink (tui-subagent-panel §4.4 / §7).
|
|
3
|
+
*
|
|
4
|
+
* WHY A PURE MODULE AND NOT JSX. The board's whole value is that it shows EVERY fact
|
|
5
|
+
* about every child, and the two things that can go wrong with that are both
|
|
6
|
+
* properties of a list: a row wider than the overlay (which silently desynchronises
|
|
7
|
+
* `OverlayFrame`'s position indicator) and a row count that grows without a bound.
|
|
8
|
+
* Both are assertable here, against plain strings, without a terminal - which is the
|
|
9
|
+
* same reason `layout/team-panel.ts` and `layout/budget.ts` are separate modules.
|
|
10
|
+
*
|
|
11
|
+
* LIVE-ONLY RULE (§4.4.2, RV-P0-2). The board has two sources with DIFFERENT fields:
|
|
12
|
+
* a live `TeamSnapshot` (has `messageCount`, `startedAt`, the activity prose and the
|
|
13
|
+
* timeline) and a settled `kind: 'team'` entry (has `durationMs` and the runs, and
|
|
14
|
+
* nothing else - `agent/reducer.ts`'s entry member carries no `messageCount` and no
|
|
15
|
+
* `startedAt`). A section whose source is missing is OMITTED, never filled with a
|
|
16
|
+
* zero: `mail 0` would be a lie, and an empty section title tells the reader nothing.
|
|
17
|
+
* The one section that survives both sources is `result` (`summary` / `error` is
|
|
18
|
+
* never stripped), which is precisely the fact a user opens the board to find.
|
|
19
|
+
*
|
|
20
|
+
* ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan, so every symbol
|
|
21
|
+
* (the ellipsis, the separator, the cursor) arrives through `glyphs`.
|
|
22
|
+
*/
|
|
23
|
+
import type { Entry } from '../../agent/reducer.js';
|
|
24
|
+
import type { SubagentRun, TeamSnapshot } from '../../team/types.js';
|
|
25
|
+
import type { Glyphs } from '../glyphs.js';
|
|
26
|
+
export type BoardRowKind = 'summary' | 'blank' | 'section' | 'child' | 'field' | 'event' | 'hint';
|
|
27
|
+
/** Which palette slot a row draws with. Mapped to `Theme` one level up, in the component. */
|
|
28
|
+
export type BoardTone = 'accent' | 'primary' | 'muted' | 'warn' | 'error' | 'done' | 'hint';
|
|
29
|
+
export interface BoardRow {
|
|
30
|
+
/** React key: stable and unique (`kind:subject:index`). */
|
|
31
|
+
key: string;
|
|
32
|
+
kind: BoardRowKind;
|
|
33
|
+
/** One already-folded line, safe to hand to `<Text wrap="truncate">`. */
|
|
34
|
+
text: string;
|
|
35
|
+
tone: BoardTone;
|
|
36
|
+
selected?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface BoardSource {
|
|
39
|
+
dispatchId: string;
|
|
40
|
+
active: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* When the dispatch started. **A settled `team` entry has no such field**
|
|
43
|
+
* (`agent/reducer.ts`'s entry member), so it is optional: with no `startedAt` the
|
|
44
|
+
* summary shows `durationMs` alone rather than inventing an absolute instant.
|
|
45
|
+
*/
|
|
46
|
+
startedAt?: number;
|
|
47
|
+
endedAt?: number;
|
|
48
|
+
requested: number;
|
|
49
|
+
runs: readonly SubagentRun[];
|
|
50
|
+
/**
|
|
51
|
+
* Team-mail count. Live snapshots carry it; a settled entry does **not** - absent
|
|
52
|
+
* means the clause is omitted rather than rendered as `mail 0` (RV-P0-2).
|
|
53
|
+
*/
|
|
54
|
+
messageCount?: number;
|
|
55
|
+
/** A settled entry's own total (`teamEnd` writes it). */
|
|
56
|
+
durationMs?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The board's single source shape, resolved in ONE place.
|
|
60
|
+
*
|
|
61
|
+
* PRIORITY: live snapshot > (the component's frozen copy) > the most recent settled
|
|
62
|
+
* entry. This function answers the first and the third; the frozen copy belongs to
|
|
63
|
+
* the component because it only exists while a dispatch is being read (§4.4.5).
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveBoardSource(input: {
|
|
66
|
+
team: TeamSnapshot | null;
|
|
67
|
+
entries: readonly Entry[];
|
|
68
|
+
}): BoardSource | null;
|
|
69
|
+
/**
|
|
70
|
+
* The child the board is showing, resolved from its LABEL every frame (D-8).
|
|
71
|
+
*
|
|
72
|
+
* Labels are the only stable handle: `panel-rows.ts` re-sorts the roster by phase, so
|
|
73
|
+
* an index would silently jump to a different child the moment one of them settles.
|
|
74
|
+
* Fallback order is "the first running child, else the first child", which is the
|
|
75
|
+
* one a reader opening the board mid-dispatch wants.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveSelection(runs: readonly SubagentRun[], label: string | null): SubagentRun | null;
|
|
78
|
+
export interface AgentBoardInput {
|
|
79
|
+
source: BoardSource;
|
|
80
|
+
/** Overlay content width. */
|
|
81
|
+
cols: number;
|
|
82
|
+
now: number;
|
|
83
|
+
selectedLabel: string | null;
|
|
84
|
+
glyphs: Glyphs;
|
|
85
|
+
native: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The board's rows, in the order a reader wants them: what the dispatch did, then who
|
|
89
|
+
* is in it, then everything about the selected child, then its history.
|
|
90
|
+
*
|
|
91
|
+
* Bounded by `TEAM_LIMITS.boardMaxRows`, and the truncation is ANNOUNCED rather than
|
|
92
|
+
* silent - a list that stops mid-fact with no notice reads as a rendering bug.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildAgentBoardRows(input: AgentBoardInput): BoardRow[];
|
|
95
|
+
/**
|
|
96
|
+
* The facts, in a FIXED order, and with the zero-valued ones ABSENT.
|
|
97
|
+
*
|
|
98
|
+
* `supervisor` / `retries` / `compactions` / `truncated` only appear when they carry a
|
|
99
|
+
* fact; a row reading `supervisor 0` teaches a reader nothing and pushes the section
|
|
100
|
+
* a row down for every child that was never touched.
|
|
101
|
+
*/
|
|
102
|
+
export declare function factPairs(run: SubagentRun, glyphs: Glyphs): [string, string][];
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agents board's row model — pure projection, no Ink (tui-subagent-panel §4.4 / §7).
|
|
3
|
+
*
|
|
4
|
+
* WHY A PURE MODULE AND NOT JSX. The board's whole value is that it shows EVERY fact
|
|
5
|
+
* about every child, and the two things that can go wrong with that are both
|
|
6
|
+
* properties of a list: a row wider than the overlay (which silently desynchronises
|
|
7
|
+
* `OverlayFrame`'s position indicator) and a row count that grows without a bound.
|
|
8
|
+
* Both are assertable here, against plain strings, without a terminal - which is the
|
|
9
|
+
* same reason `layout/team-panel.ts` and `layout/budget.ts` are separate modules.
|
|
10
|
+
*
|
|
11
|
+
* LIVE-ONLY RULE (§4.4.2, RV-P0-2). The board has two sources with DIFFERENT fields:
|
|
12
|
+
* a live `TeamSnapshot` (has `messageCount`, `startedAt`, the activity prose and the
|
|
13
|
+
* timeline) and a settled `kind: 'team'` entry (has `durationMs` and the runs, and
|
|
14
|
+
* nothing else - `agent/reducer.ts`'s entry member carries no `messageCount` and no
|
|
15
|
+
* `startedAt`). A section whose source is missing is OMITTED, never filled with a
|
|
16
|
+
* zero: `mail 0` would be a lie, and an empty section title tells the reader nothing.
|
|
17
|
+
* The one section that survives both sources is `result` (`summary` / `error` is
|
|
18
|
+
* never stripped), which is precisely the fact a user opens the board to find.
|
|
19
|
+
*
|
|
20
|
+
* ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan, so every symbol
|
|
21
|
+
* (the ellipsis, the separator, the cursor) arrives through `glyphs`.
|
|
22
|
+
*/
|
|
23
|
+
import { TEAM_LIMITS } from '../../team/limits.js';
|
|
24
|
+
import { childStatus, lastTeamEntry, rosterCounts, rosterHeadline, statusMarkerCell, } from '../team-status.js';
|
|
25
|
+
import { formatDuration, formatTokens } from '../../agent/usage.js';
|
|
26
|
+
import { sanitizeActivity } from '../../team/activity.js';
|
|
27
|
+
import { activityWithBadge, teamRowGeometry } from './team-row.js';
|
|
28
|
+
import { alignRightCells, fitCells, padCells, wrapCells } from './cells.js';
|
|
29
|
+
/**
|
|
30
|
+
* The board's single source shape, resolved in ONE place.
|
|
31
|
+
*
|
|
32
|
+
* PRIORITY: live snapshot > (the component's frozen copy) > the most recent settled
|
|
33
|
+
* entry. This function answers the first and the third; the frozen copy belongs to
|
|
34
|
+
* the component because it only exists while a dispatch is being read (§4.4.5).
|
|
35
|
+
*/
|
|
36
|
+
export function resolveBoardSource(input) {
|
|
37
|
+
const { team } = input;
|
|
38
|
+
if (team !== null) {
|
|
39
|
+
return {
|
|
40
|
+
dispatchId: team.dispatchId,
|
|
41
|
+
active: team.active,
|
|
42
|
+
startedAt: team.startedAt,
|
|
43
|
+
requested: team.requested,
|
|
44
|
+
runs: team.runs,
|
|
45
|
+
messageCount: team.messageCount,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const entry = lastTeamEntry(input.entries);
|
|
49
|
+
if (entry === null)
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
dispatchId: entry.dispatchId,
|
|
53
|
+
active: entry.active === true,
|
|
54
|
+
requested: entry.requested,
|
|
55
|
+
runs: entry.runs,
|
|
56
|
+
...(entry.durationMs !== undefined ? { durationMs: entry.durationMs } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The child the board is showing, resolved from its LABEL every frame (D-8).
|
|
61
|
+
*
|
|
62
|
+
* Labels are the only stable handle: `panel-rows.ts` re-sorts the roster by phase, so
|
|
63
|
+
* an index would silently jump to a different child the moment one of them settles.
|
|
64
|
+
* Fallback order is "the first running child, else the first child", which is the
|
|
65
|
+
* one a reader opening the board mid-dispatch wants.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveSelection(runs, label) {
|
|
68
|
+
const named = label === null ? undefined : runs.find((run) => run.label === label);
|
|
69
|
+
if (named !== undefined)
|
|
70
|
+
return named;
|
|
71
|
+
return runs.find((run) => childStatus(run) === 'running') ?? runs[0] ?? null;
|
|
72
|
+
}
|
|
73
|
+
/** Fixed label column of a `field` row, and the value column of its wide form. */
|
|
74
|
+
const FIELD_LABEL_CELLS = 11;
|
|
75
|
+
const FIELD_WIDE_VALUE_CELLS = 23;
|
|
76
|
+
/** Rows of prose a `now` / `result` section may spend. */
|
|
77
|
+
const NOW_MAX_ROWS = 6;
|
|
78
|
+
const RESULT_MAX_ROWS = 12;
|
|
79
|
+
/** Board width at which the field rows go 2-up, and the narrowest width that pads. */
|
|
80
|
+
const BOARD_WIDE_COLS = 110;
|
|
81
|
+
const BOARD_INLINE_COLS = 40;
|
|
82
|
+
/**
|
|
83
|
+
* The board's rows, in the order a reader wants them: what the dispatch did, then who
|
|
84
|
+
* is in it, then everything about the selected child, then its history.
|
|
85
|
+
*
|
|
86
|
+
* Bounded by `TEAM_LIMITS.boardMaxRows`, and the truncation is ANNOUNCED rather than
|
|
87
|
+
* silent - a list that stops mid-fact with no notice reads as a rendering bug.
|
|
88
|
+
*/
|
|
89
|
+
export function buildAgentBoardRows(input) {
|
|
90
|
+
const { source, glyphs } = input;
|
|
91
|
+
const cols = normalizeCols(input.cols);
|
|
92
|
+
const rows = [];
|
|
93
|
+
const selected = resolveSelection(source.runs, input.selectedLabel);
|
|
94
|
+
appendSummary(rows, input, selected);
|
|
95
|
+
appendChildren(rows, input, selected, cols);
|
|
96
|
+
if (selected !== null) {
|
|
97
|
+
appendFacts(rows, input, selected, cols);
|
|
98
|
+
appendProse(rows, input, selected, cols);
|
|
99
|
+
appendTimeline(rows, input, selected, cols);
|
|
100
|
+
}
|
|
101
|
+
if (rows.length > TEAM_LIMITS.boardMaxRows) {
|
|
102
|
+
const kept = rows.slice(0, TEAM_LIMITS.boardMaxRows);
|
|
103
|
+
const dropped = rows.length - TEAM_LIMITS.boardMaxRows;
|
|
104
|
+
kept[kept.length - 1] = {
|
|
105
|
+
key: 'truncated',
|
|
106
|
+
kind: 'hint',
|
|
107
|
+
text: fitCells(`+${dropped} more rows`, cols, glyphs.ellipsis),
|
|
108
|
+
tone: 'hint',
|
|
109
|
+
};
|
|
110
|
+
return kept;
|
|
111
|
+
}
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
function normalizeCols(value) {
|
|
115
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
116
|
+
}
|
|
117
|
+
/** A section title, then nothing else: callers add the body. */
|
|
118
|
+
function pushSection(rows, title, cols, glyphs) {
|
|
119
|
+
rows.push({
|
|
120
|
+
key: `section:${title}`,
|
|
121
|
+
kind: 'section',
|
|
122
|
+
text: fitCells(title, cols, glyphs.ellipsis),
|
|
123
|
+
tone: 'accent',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function pushBlank(rows, key) {
|
|
127
|
+
rows.push({ key: `blank:${key}`, kind: 'blank', text: '', tone: 'muted' });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Two summary lines, each a left clause and a right-anchored figure.
|
|
131
|
+
*
|
|
132
|
+
* The right figure is `alignRightCells`-ed rather than flex-ended because the row is a
|
|
133
|
+
* STRING here: the board hands `OverlayFrame` one element per row, so a row has to be
|
|
134
|
+
* a single measured line or the position indicator desynchronises.
|
|
135
|
+
*/
|
|
136
|
+
function summaryRow(rows, key, left, right, cols, glyphs, tone) {
|
|
137
|
+
if (cols === 0)
|
|
138
|
+
return;
|
|
139
|
+
const rightCells = Math.min(right.length, Math.max(0, cols - 1));
|
|
140
|
+
const leftCells = Math.max(0, cols - rightCells - 1);
|
|
141
|
+
const body = `${padCells(fitCells(left, leftCells, glyphs.ellipsis), leftCells)} ${right.slice(0, rightCells)}`;
|
|
142
|
+
rows.push({
|
|
143
|
+
key,
|
|
144
|
+
kind: 'summary',
|
|
145
|
+
text: padCells(fitCells(body, cols, glyphs.ellipsis), cols),
|
|
146
|
+
tone,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function appendSummary(rows, input, selected) {
|
|
150
|
+
const { source, glyphs } = input;
|
|
151
|
+
const cols = normalizeCols(input.cols);
|
|
152
|
+
const counts = rosterCounts(source.runs);
|
|
153
|
+
// A settled entry has no `startedAt`, so only `durationMs` is honest there; a live
|
|
154
|
+
// one may have neither (a dispatch that has not been stamped yet).
|
|
155
|
+
const totalMs = source.active && source.startedAt !== undefined
|
|
156
|
+
? Math.max(0, input.now - source.startedAt)
|
|
157
|
+
: source.durationMs;
|
|
158
|
+
const clock = totalMs === undefined ? '' : formatDuration(totalMs);
|
|
159
|
+
summaryRow(rows, 'summary:counts', rosterHeadline(counts, glyphs, cols), clock, cols, glyphs, 'primary');
|
|
160
|
+
const figures = [];
|
|
161
|
+
// THE LIVE-ONLY CLAUSE (I-15): no `messageCount` means no `mail` clause at all.
|
|
162
|
+
if (source.messageCount !== undefined)
|
|
163
|
+
figures.push(`mail ${source.messageCount}`);
|
|
164
|
+
const toolCalls = source.runs.reduce((sum, run) => sum + run.toolCalls, 0);
|
|
165
|
+
figures.push(`${toolCalls} tool calls`);
|
|
166
|
+
const usage = source.runs.reduce((acc, run) => ({
|
|
167
|
+
inputTokens: acc.inputTokens + run.usage.inputTokens,
|
|
168
|
+
outputTokens: acc.outputTokens + run.usage.outputTokens,
|
|
169
|
+
}), { inputTokens: 0, outputTokens: 0 });
|
|
170
|
+
figures.push(`${formatTokens(usage.inputTokens)} in / ${formatTokens(usage.outputTokens)} out`);
|
|
171
|
+
const children = `${source.runs.length} children`;
|
|
172
|
+
if (cols >= BOARD_INLINE_COLS) {
|
|
173
|
+
summaryRow(rows, 'summary:figures', figures.join(` ${glyphs.midDot} `), children, cols, glyphs, 'muted');
|
|
174
|
+
}
|
|
175
|
+
pushBlank(rows, 'summary');
|
|
176
|
+
if (selected === null)
|
|
177
|
+
pushSection(rows, 'no children', cols, glyphs);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Every child, not the panel's `panelMaxRows` five: the board is the place a reader
|
|
181
|
+
* goes to see the child the panel folded away behind `+N more`.
|
|
182
|
+
*
|
|
183
|
+
* EACH ROW REUSES THE PANEL'S GEOMETRY one cell narrower and prefixes a cursor, so the
|
|
184
|
+
* columns line up with the panel exactly - "the same table at a second zoom level"
|
|
185
|
+
* (§1) rather than a second layout to keep in step.
|
|
186
|
+
*/
|
|
187
|
+
function appendChildren(rows, input, selected, cols) {
|
|
188
|
+
const { glyphs, source } = input;
|
|
189
|
+
pushSection(rows, 'children', cols, glyphs);
|
|
190
|
+
const inner = Math.max(0, cols - 1);
|
|
191
|
+
const geometry = teamRowGeometry(inner, glyphs);
|
|
192
|
+
for (const run of source.runs) {
|
|
193
|
+
const isSelected = selected !== null && run.label === selected.label;
|
|
194
|
+
const marker = statusMarkerCell(childStatus(run), glyphs);
|
|
195
|
+
const label = run.label.padEnd(8).slice(0, 8);
|
|
196
|
+
const descriptionCells = Math.max(0, inner - geometry.marker - geometry.label - geometry.gap - geometry.activity - geometry.clock);
|
|
197
|
+
const description = fitCells(run.description.replace(/\s+/g, ' '), descriptionCells, glyphs.ellipsis);
|
|
198
|
+
// THE MARKER TIER IS PASSED THROUGH (R-10): the ASCII tier's marker cell is 6 columns,
|
|
199
|
+
// so a budget computed against the Unicode tier's 3 would promise two columns this row
|
|
200
|
+
// does not have. `fitCells` is the second gate; the first one has to be told the truth
|
|
201
|
+
// or the text is ellipsised where it did fit.
|
|
202
|
+
const activity = fitCells(activityWithBadge(run, inner, input.now, geometry.marker), geometry.activity, glyphs.ellipsis);
|
|
203
|
+
const clock = run.startedAt === undefined
|
|
204
|
+
? ''
|
|
205
|
+
: formatDuration(Math.max(0, (run.endedAt ?? input.now) - run.startedAt));
|
|
206
|
+
const text = [
|
|
207
|
+
isSelected ? glyphs.cursor : ' ',
|
|
208
|
+
padCells(`${marker} `, geometry.marker),
|
|
209
|
+
padCells(label, geometry.label),
|
|
210
|
+
padCells(description, descriptionCells),
|
|
211
|
+
padCells(' '.repeat(geometry.gap), geometry.gap),
|
|
212
|
+
padCells(activity, geometry.activity),
|
|
213
|
+
alignRightCells(clock, geometry.clock, glyphs.ellipsis),
|
|
214
|
+
].join('');
|
|
215
|
+
rows.push({
|
|
216
|
+
key: `child:${run.label}`,
|
|
217
|
+
kind: 'child',
|
|
218
|
+
text: padCells(fitCells(text, cols, glyphs.ellipsis), cols),
|
|
219
|
+
tone: isSelected ? 'primary' : 'muted',
|
|
220
|
+
...(isSelected ? { selected: true } : {}),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
pushBlank(rows, 'children');
|
|
224
|
+
}
|
|
225
|
+
/** The selected child's fixed-order fact list (§4.4.2's table, top to bottom). */
|
|
226
|
+
function appendFacts(rows, input, run, cols) {
|
|
227
|
+
const { glyphs } = input;
|
|
228
|
+
pushSection(rows, `${run.label} ${glyphs.midDot} ${run.description}`.trim(), cols, glyphs);
|
|
229
|
+
const facts = factPairs(run, glyphs);
|
|
230
|
+
// Two facts per row when there is room, one otherwise; the pairing is done here
|
|
231
|
+
// rather than in the component so the row COUNT is assertable.
|
|
232
|
+
const step = cols >= BOARD_WIDE_COLS ? 2 : 1;
|
|
233
|
+
for (let index = 0; index < facts.length; index += step) {
|
|
234
|
+
const first = facts[index];
|
|
235
|
+
if (first === undefined)
|
|
236
|
+
continue;
|
|
237
|
+
const second = step === 2 ? facts[index + 1] : undefined;
|
|
238
|
+
rows.push({
|
|
239
|
+
key: `field:${first[0]}:${index}`,
|
|
240
|
+
kind: 'field',
|
|
241
|
+
text: second === undefined
|
|
242
|
+
? fieldCells(first, cols, glyphs)
|
|
243
|
+
: fieldPairCells(first, second, cols, glyphs),
|
|
244
|
+
tone: 'primary',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
pushBlank(rows, 'facts');
|
|
248
|
+
}
|
|
249
|
+
/** The width one field occupies in the 2-up form: its label plus its value column. */
|
|
250
|
+
const FIELD_PAIR_CELLS = FIELD_LABEL_CELLS + FIELD_WIDE_VALUE_CELLS;
|
|
251
|
+
/** One `label value` field, padded so two of them stand in columns. */
|
|
252
|
+
function fieldCells(pair, cols, glyphs) {
|
|
253
|
+
const width = Math.max(0, cols);
|
|
254
|
+
return [
|
|
255
|
+
padCells(fitCells(pair[0], FIELD_LABEL_CELLS - 1, glyphs.ellipsis), FIELD_LABEL_CELLS - 1),
|
|
256
|
+
' ',
|
|
257
|
+
fitCells(pair[1], Math.max(0, width - FIELD_LABEL_CELLS), glyphs.ellipsis),
|
|
258
|
+
].join('');
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Two fields side by side, each with its own label and value column.
|
|
262
|
+
*
|
|
263
|
+
* BUILT FROM THE LEFT FIELD'S WIDTH, not from slicing a full-width row: a slice counts
|
|
264
|
+
* code units while a row is measured in cells, so a wide character in the first value
|
|
265
|
+
* could be cut in half and both columns would drift. `fieldCells` measures, so one space
|
|
266
|
+
* of gutter and a 34-cell left field are what fix the right field's offset.
|
|
267
|
+
*/
|
|
268
|
+
function fieldPairCells(first, second, cols, glyphs) {
|
|
269
|
+
const right = Math.max(0, cols - FIELD_PAIR_CELLS - 1);
|
|
270
|
+
return `${fieldCells(first, FIELD_PAIR_CELLS, glyphs)} ${fieldCells(second, right, glyphs)}`;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The facts, in a FIXED order, and with the zero-valued ones ABSENT.
|
|
274
|
+
*
|
|
275
|
+
* `supervisor` / `retries` / `compactions` / `truncated` only appear when they carry a
|
|
276
|
+
* fact; a row reading `supervisor 0` teaches a reader nothing and pushes the section
|
|
277
|
+
* a row down for every child that was never touched.
|
|
278
|
+
*/
|
|
279
|
+
export function factPairs(run, glyphs) {
|
|
280
|
+
const status = childStatus(run);
|
|
281
|
+
const state = status === 'running' ? `${status} ${glyphs.midDot} ${run.phase}` : status;
|
|
282
|
+
const pairs = [
|
|
283
|
+
['state', state],
|
|
284
|
+
['turns', String(run.turns)],
|
|
285
|
+
['tools', String(run.toolCalls)],
|
|
286
|
+
['tokens', `${formatTokens(run.usage.inputTokens)} / ${formatTokens(run.usage.outputTokens)}`],
|
|
287
|
+
];
|
|
288
|
+
if (run.filesTouched.length > 0) {
|
|
289
|
+
const head = run.filesTouched.slice(0, 3).join(', ');
|
|
290
|
+
const more = run.filesTouched.length > 3 ? ` (+${run.filesTouched.length - 3})` : '';
|
|
291
|
+
pairs.push(['files', `${run.filesTouched.length} (${head}${more})`]);
|
|
292
|
+
}
|
|
293
|
+
pairs.push(['mail', `${run.messagesSent} sent ${glyphs.midDot} ${run.blockedWaits ?? 0} blocked`]);
|
|
294
|
+
if ((run.interventions ?? 0) > 0 || (run.replacements ?? 0) > 0) {
|
|
295
|
+
const looks = `${run.interventions ?? 0} looks`;
|
|
296
|
+
const replaced = (run.replacements ?? 0) > 0 ? ` ${glyphs.midDot} ${run.replacements} replaced` : '';
|
|
297
|
+
const last = run.lastIntervention === undefined
|
|
298
|
+
? ''
|
|
299
|
+
: ` ${glyphs.midDot} ${run.lastIntervention.action}`;
|
|
300
|
+
pairs.push(['supervisor', `${looks}${replaced}${last}`]);
|
|
301
|
+
}
|
|
302
|
+
if (run.retry !== undefined) {
|
|
303
|
+
pairs.push(['retries', `retrying ${run.retry.attempt}/${run.retry.maxRetries}`]);
|
|
304
|
+
}
|
|
305
|
+
else if ((run.retries ?? 0) > 0) {
|
|
306
|
+
pairs.push(['retries', `${run.retries} cold start`]);
|
|
307
|
+
}
|
|
308
|
+
if ((run.compactions ?? 0) > 0)
|
|
309
|
+
pairs.push(['compactions', String(run.compactions)]);
|
|
310
|
+
if (run.truncated === true)
|
|
311
|
+
pairs.push(['truncated', 'stopped at turn cap']);
|
|
312
|
+
return pairs;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* `now` and `result`: the two prose sections, both folded by DISPLAY WIDTH.
|
|
316
|
+
*
|
|
317
|
+
* LIVE-ONLY, and honestly so: `now` needs `run.activity`, which is stripped on the way
|
|
318
|
+
* out of the dispatch, so a settled board says one line instead of drawing an empty
|
|
319
|
+
* section (I-15). `result` survives both sources and is the section a reader opens the
|
|
320
|
+
* board for when a child failed.
|
|
321
|
+
*/
|
|
322
|
+
function appendProse(rows, input, run, cols) {
|
|
323
|
+
const { glyphs } = input;
|
|
324
|
+
pushSection(rows, 'now', cols, glyphs);
|
|
325
|
+
const now = run.activity === undefined
|
|
326
|
+
? ''
|
|
327
|
+
: sanitizeActivity(run.activity, TEAM_LIMITS.timelineTextChars);
|
|
328
|
+
if (now.length === 0) {
|
|
329
|
+
pushUnavailable(rows, 'now', cols, glyphs);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const wrapped = wrapCells(now, Math.max(0, cols - 2), NOW_MAX_ROWS, glyphs.ellipsis);
|
|
333
|
+
wrapped.lines.forEach((line, index) => {
|
|
334
|
+
rows.push({ key: `now:${index}`, kind: 'event', text: fitCells(` ${line}`, cols, glyphs.ellipsis), tone: 'muted' });
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
pushSection(rows, 'result', cols, glyphs);
|
|
338
|
+
const result = run.error ?? run.summary ?? '';
|
|
339
|
+
if (result.length === 0) {
|
|
340
|
+
pushUnavailable(rows, 'result', cols, glyphs);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const wrapped = wrapCells(result, Math.max(0, cols - 2), RESULT_MAX_ROWS, glyphs.ellipsis);
|
|
344
|
+
wrapped.lines.forEach((line, index) => {
|
|
345
|
+
rows.push({
|
|
346
|
+
key: `result:${index}`,
|
|
347
|
+
kind: 'event',
|
|
348
|
+
text: fitCells(` ${line}`, cols, glyphs.ellipsis),
|
|
349
|
+
tone: run.error !== undefined ? 'error' : 'primary',
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The one honest sentence that replaces a section whose source is gone (§4.4.2 rule 2).
|
|
355
|
+
*
|
|
356
|
+
* QUOTE-SAFE AND PATH-FREE: it names the reason ("live only") rather than apologising,
|
|
357
|
+
* because the fact that matters is that the reader is not being shown a stale copy.
|
|
358
|
+
*/
|
|
359
|
+
function pushUnavailable(rows, section, cols, glyphs) {
|
|
360
|
+
rows.push({
|
|
361
|
+
key: `unavailable:${section}`,
|
|
362
|
+
kind: 'hint',
|
|
363
|
+
text: fitCells(' live only - not available after the dispatch ended', cols, glyphs.ellipsis),
|
|
364
|
+
tone: 'hint',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/** The 24-event ring, oldest first, each prefixed with its offset from the start. */
|
|
368
|
+
function appendTimeline(rows, input, run, cols) {
|
|
369
|
+
const { glyphs } = input;
|
|
370
|
+
pushSection(rows, 'timeline', cols, glyphs);
|
|
371
|
+
const timeline = run.timeline ?? [];
|
|
372
|
+
if (timeline.length === 0) {
|
|
373
|
+
pushUnavailable(rows, 'timeline', cols, glyphs);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const start = run.startedAt ?? timeline[0]?.at ?? 0;
|
|
377
|
+
timeline.forEach((entry, index) => {
|
|
378
|
+
const offset = `+${formatDuration(Math.max(0, entry.at - start))}`;
|
|
379
|
+
const mark = entry.ok === undefined ? '' : entry.ok ? ' ok' : ' failed';
|
|
380
|
+
const took = entry.ms === undefined ? '' : ` ${formatDuration(entry.ms)}`;
|
|
381
|
+
const text = ` ${padCells(offset, 8)} ${entry.text}${took}${mark}`;
|
|
382
|
+
rows.push({
|
|
383
|
+
key: `event:${index}`,
|
|
384
|
+
kind: 'event',
|
|
385
|
+
text: fitCells(text, cols, glyphs.ellipsis),
|
|
386
|
+
tone: entry.ok === false ? 'warn' : 'muted',
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-width cell arithmetic (tui-subagent-panel §5 #1).
|
|
3
|
+
*
|
|
4
|
+
* EVERY FUNCTION HERE IS PURE AND TOTAL: no Ink, no clock, no terminal, and no
|
|
5
|
+
* input that throws. The panel row and the Agents board both need to place text
|
|
6
|
+
* in a FIXED number of terminal columns, and `padEnd` cannot do that: it counts
|
|
7
|
+
* code units while a terminal counts cells, so a CJK description padded with
|
|
8
|
+
* `padEnd` overflows its box by one column per wide character and pushes the
|
|
9
|
+
* next cell off the row. That is exactly the defect this round removes from the
|
|
10
|
+
* roster, one layer down.
|
|
11
|
+
*
|
|
12
|
+
* ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan, so the
|
|
13
|
+
* ellipsis and any other symbol a caller wants must be passed IN (the board
|
|
14
|
+
* hands over `glyphs.ellipsis`), never spelled here.
|
|
15
|
+
*/
|
|
16
|
+
/** Display columns a string occupies, or 0 for a non-string (defensive at the edge only). */
|
|
17
|
+
export declare function cellWidth(text: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Cut `text` to at most `cells` display columns, marking the cut with `ellipsis`.
|
|
20
|
+
*
|
|
21
|
+
* THE ELLIPSIS IS PART OF THE BUDGET, not an extra column: a caller that asks for
|
|
22
|
+
* 8 cells gets a string that MEASURES 8, which is what makes a fixed-width cell
|
|
23
|
+
* a cell rather than a hope. When the budget cannot hold the ellipsis plus one
|
|
24
|
+
* character, the string is hard-cut instead - a marker wider than the cell it
|
|
25
|
+
* marks is the one outcome worse than no marker.
|
|
26
|
+
*
|
|
27
|
+
* Wide characters are never split: a surrogate pair or a double-width glyph that
|
|
28
|
+
* does not fit is dropped whole, so the result can measure one column less than
|
|
29
|
+
* asked but never one more.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fitCells(text: string, cells: number, ellipsis?: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Pad `text` to exactly `cells` columns (or leave it alone when it already
|
|
34
|
+
* measures at least that).
|
|
35
|
+
*
|
|
36
|
+
* NOT `padEnd`: see this file's header. A padded cell is what keeps the next
|
|
37
|
+
* column at a stable offset, which is the whole point of the panel's table.
|
|
38
|
+
*/
|
|
39
|
+
export declare function padCells(text: string, cells: number): string;
|
|
40
|
+
/** `fitCells` then `padCells`: the result ALWAYS measures exactly `cells`. */
|
|
41
|
+
export declare function alignRightCells(text: string, cells: number, ellipsis?: string): string;
|
|
42
|
+
/** What one wrapped line is worth: the text, and whether lines were dropped. */
|
|
43
|
+
export interface WrappedCells {
|
|
44
|
+
lines: string[];
|
|
45
|
+
/** Lines that did not fit under `maxRows`; 0 when everything fitted. */
|
|
46
|
+
dropped: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fold `text` into at most `maxRows` lines of at most `cells` columns each.
|
|
50
|
+
*
|
|
51
|
+
* BREAKS AT SPACES, HARD-CUTS ONLY WHEN A SINGLE TOKEN CANNOT FIT: a word that is
|
|
52
|
+
* wider than the column is the one case where an honest over-long token beats a
|
|
53
|
+
* line of nothing, so it is cut rather than pushed to a line that still cannot
|
|
54
|
+
* hold it. The last allowed line, when text remains, carries the `(+N lines)`
|
|
55
|
+
* tail so the reader learns that the section was truncated instead of believing
|
|
56
|
+
* the prose ended mid-sentence.
|
|
57
|
+
*
|
|
58
|
+
* Pure and total: `cells <= 0` or `maxRows <= 0` yields no lines, and the caller
|
|
59
|
+
* decides what an empty section means (the board omits it).
|
|
60
|
+
*/
|
|
61
|
+
export declare function wrapCells(text: string, cells: number, maxRows: number, ellipsis?: string): WrappedCells;
|