@aragon-agent/cli 0.6.45 → 0.6.47

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 (71) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +104 -6
  3. package/dist/agent/coalesce.d.ts +4 -0
  4. package/dist/agent/coalesce.js +13 -0
  5. package/dist/agent/controller.d.ts +21 -0
  6. package/dist/agent/controller.js +56 -3
  7. package/dist/agent/invocation-card.d.ts +77 -0
  8. package/dist/agent/invocation-card.js +169 -0
  9. package/dist/agent/prompt-options.d.ts +6 -0
  10. package/dist/agent/queued-messages.d.ts +3 -0
  11. package/dist/agent/queued-messages.js +4 -1
  12. package/dist/agent/reducer.d.ts +25 -0
  13. package/dist/agent/reducer.js +43 -5
  14. package/dist/agent/user-attention.d.ts +17 -0
  15. package/dist/agent/user-attention.js +32 -0
  16. package/dist/cli.js +2 -3
  17. package/dist/commands/builtins.js +12 -0
  18. package/dist/commands/governance-command.js +13 -1
  19. package/dist/commands/index-build.d.ts +8 -0
  20. package/dist/commands/index-build.js +11 -0
  21. package/dist/commands/registry.d.ts +23 -0
  22. package/dist/commands/registry.js +20 -7
  23. package/dist/commands/skills.js +36 -2
  24. package/dist/commands/subagent-command.js +3 -1
  25. package/dist/commands/team-command.d.ts +6 -0
  26. package/dist/commands/team-command.js +40 -3
  27. package/dist/config/load.js +4 -25
  28. package/dist/config/schema.d.ts +4 -4
  29. package/dist/config/schema.js +7 -1
  30. package/dist/proc/limits.d.ts +27 -13
  31. package/dist/proc/limits.js +27 -13
  32. package/dist/proc/progress.d.ts +7 -0
  33. package/dist/proc/progress.js +35 -7
  34. package/dist/proc/prompt.js +7 -4
  35. package/dist/proc/supervisor.js +7 -1
  36. package/dist/proc/types.d.ts +6 -0
  37. package/dist/proc/wait-plan.d.ts +39 -0
  38. package/dist/proc/wait-plan.js +77 -0
  39. package/dist/proc/wait-view.d.ts +65 -0
  40. package/dist/proc/wait-view.js +66 -0
  41. package/dist/proc/wait.d.ts +36 -3
  42. package/dist/proc/wait.js +67 -4
  43. package/dist/session/checkpoint.js +2 -0
  44. package/dist/session/persist.js +29 -2
  45. package/dist/session/validate-session.js +7 -2
  46. package/dist/team/forced-mode.d.ts +19 -0
  47. package/dist/team/forced-mode.js +42 -20
  48. package/dist/tools/job-tools.d.ts +13 -2
  49. package/dist/tools/job-tools.js +156 -33
  50. package/dist/ui/ActivityLine.js +5 -2
  51. package/dist/ui/App.js +59 -6
  52. package/dist/ui/StatusBar.d.ts +1 -0
  53. package/dist/ui/Transcript.js +11 -1
  54. package/dist/ui/entries/InvocationCard.d.ts +20 -0
  55. package/dist/ui/entries/InvocationCard.js +32 -0
  56. package/dist/ui/entries/ToolCard.js +11 -2
  57. package/dist/ui/entries/WaitCard.d.ts +37 -0
  58. package/dist/ui/entries/WaitCard.js +92 -0
  59. package/dist/ui/interaction-copy.d.ts +2 -0
  60. package/dist/ui/interaction-copy.js +3 -0
  61. package/dist/ui/layout/invocation-card.d.ts +41 -0
  62. package/dist/ui/layout/invocation-card.js +91 -0
  63. package/dist/ui/layout/status-layout.d.ts +1 -0
  64. package/dist/ui/layout/status-layout.js +10 -1
  65. package/dist/ui/layout/virtual-window.js +18 -2
  66. package/dist/ui/layout/wait-bar.d.ts +34 -0
  67. package/dist/ui/layout/wait-bar.js +48 -0
  68. package/dist/ui/run-status-row.d.ts +4 -2
  69. package/dist/ui/scrollback/live-preview.js +12 -0
  70. package/dist/ui/transcript-text.js +4 -1
  71. package/package.json +2 -2
@@ -0,0 +1,92 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * WaitCard - the five-row card a running `job_wait` is drawn as
4
+ * (extended-job-wait §3.8.2).
5
+ *
6
+ * ASCII ONLY: every non-ASCII cell is a `pickGlyphs` result and every sentence the
7
+ * user did not get from the job itself is in `interactionCopy`.
8
+ *
9
+ * FIVE ROWS, IN EVERY STATE (I-10). A branch may change what a row SAYS, never how
10
+ * many rows there are: `estimateEntryRows` charges exactly `separation + 5`, and a
11
+ * card that is one row short makes the viewport jump while one row long makes the
12
+ * height estimate an under-count - the direction `virtual-window.ts` names unsafe.
13
+ * Every row is `wrap="truncate"` for the same reason.
14
+ *
15
+ * ONE CLOCK. Every number is a subtraction between an epoch stamped by
16
+ * `buildWaitView` and the `nowSec` the transcript already ticks at 1 Hz; there is
17
+ * no timer in this file, and the spinner is the app's shared factory, which this
18
+ * card REPLACES `ToolCard`'s badge with rather than adding to.
19
+ */
20
+ import React from 'react';
21
+ import { Box, Text, useStdout } from 'ink';
22
+ import { pickGlyphs } from '../glyphs.js';
23
+ import { liveSpinner } from '../ActivityLine.js';
24
+ import { railBorderProps } from '../layout/Gutter.js';
25
+ import { interactionCopy } from '../interaction-copy.js';
26
+ import { WAIT_BAR_MIN_COLS, barCellsFor, formatWaitClock, percentOf, renderBar, } from '../layout/wait-bar.js';
27
+ /** Empty `<Text>` measures zero rows in Ink, so a blank row is drawn as a space. */
28
+ function filled(text) {
29
+ return text.length > 0 ? text : ' ';
30
+ }
31
+ function WaitCardImpl(props) {
32
+ const { view, reducedMotion, theme, caps } = props;
33
+ const glyphs = pickGlyphs(caps);
34
+ const { stdout } = useStdout();
35
+ const cols = stdout?.columns ?? 80;
36
+ // Before the first tick the clock reads the instant the wait began.
37
+ const nowMs = Math.max(view.waitStartedAt, props.nowSec !== undefined ? props.nowSec * 1000 : view.waitStartedAt);
38
+ const waited = nowMs - view.waitStartedAt;
39
+ const remaining = Math.max(0, view.upToMs - waited);
40
+ const upTo = formatWaitClock(view.upToMs);
41
+ const dot = ` ${glyphs.midDot} `;
42
+ // ROW 1 - what, and the clock.
43
+ const spinner = liveSpinner(reducedMotion ?? false, caps);
44
+ const title = `Waiting for ${view.jobLabel}`;
45
+ const clock = `${formatWaitClock(waited)} of ${upTo}`;
46
+ // ROW 2 - the bar. THE JOB'S OWN PROGRESS WINS; with none, the bar shows the wait
47
+ // window itself and the words say so (D-10), so the two meanings never share a
48
+ // bar silently.
49
+ const jobFraction = view.progress?.fraction ?? null;
50
+ const windowFraction = view.upToMs > 0 ? waited / view.upToMs : 0;
51
+ const barFraction = jobFraction ?? windowFraction;
52
+ const showBar = cols >= WAIT_BAR_MIN_COLS;
53
+ const bar = showBar
54
+ ? `${renderBar(barFraction, barCellsFor(cols), glyphs)} `
55
+ : '';
56
+ let barText;
57
+ if (jobFraction !== null && view.progress) {
58
+ barText =
59
+ view.progress.kind === 'percent'
60
+ ? view.progress.text
61
+ : `${percentOf(jobFraction)}% ${view.progress.text}`;
62
+ }
63
+ else {
64
+ barText = `${percentOf(windowFraction)}% of this ${upTo} wait`;
65
+ if (view.progress)
66
+ barText += ` ${view.progress.text}`;
67
+ }
68
+ // ROW 3 - the two clocks.
69
+ let clocks;
70
+ if (view.jobLabel.endsWith(' jobs')) {
71
+ const rule = view.mode === 'any' ? 'ANY finishes' : 'ALL finish';
72
+ clocks = `${view.liveJobs} ${view.liveJobs === 1 ? 'job' : 'jobs'} live${dot}this wait ends when ${rule}`;
73
+ }
74
+ else {
75
+ clocks =
76
+ `job ${view.status} ${formatWaitClock(Math.max(0, nowMs - view.jobStartedAt))}${dot}` +
77
+ `this wait ends in ${formatWaitClock(remaining)}, or sooner if the job finishes`;
78
+ }
79
+ // ROW 4 - what the job last said, or a calm note that silence is not failure (D-11).
80
+ const quietMs = Math.max(0, nowMs - view.lastOutputAt);
81
+ const quiet = view.lastRow !== undefined && quietMs > view.stallSeconds * 1000;
82
+ const lastRowText = quiet
83
+ ? `quiet for ${formatWaitClock(quietMs)} - the job is still alive; this is not a failure`
84
+ : (view.lastRow ?? 'no output yet');
85
+ const ago = view.lastRow !== undefined && !quiet ? `${formatWaitClock(quietMs)} ago` : '';
86
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [spinner ? _jsxs(Text, { color: theme.toolRunning, children: [spinner, " "] }) : (_jsxs(Text, { color: theme.toolRunning, children: [glyphs.spinnerStill, " "] })), _jsx(Text, { color: theme.accent, bold: true, children: title }), _jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: view.command.length > 0 ? ` ${view.command}` : ' ' }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { color: theme.muted, children: [" ", clock] }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.toolRunning, children: bar }), _jsx(Text, { color: theme.primary, children: filled(barText) })] }), _jsx(Text, { color: theme.muted, wrap: "truncate", children: filled(clocks) }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: _jsx(Text, { color: quiet ? theme.noticeWarn : theme.muted, wrap: "truncate", children: filled(lastRowText) }) }), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: theme.muted, children: ago.length > 0 ? ` ${ago}` : ' ' }) })] }), _jsxs(Text, { color: theme.muted, wrap: "truncate", children: [interactionCopy.waitHintSend, dot, interactionCopy.waitHintStop] })] })] }));
87
+ }
88
+ /**
89
+ * `React.memo` with the DEFAULT comparator: the reducer writes a fresh `view`
90
+ * object once a second, which is exactly the one change that must re-render.
91
+ */
92
+ export const WaitCard = React.memo(WaitCardImpl);
@@ -77,6 +77,8 @@ export declare const interactionCopy: {
77
77
  readonly shiftNewline: "Shift+Enter newline needs terminal support";
78
78
  readonly mode: "Shift+Tab switch mode";
79
79
  readonly update: "/update";
80
+ readonly waitHintSend: "Send a message to end the wait early";
81
+ readonly waitHintStop: "Esc x2 stops the whole run";
80
82
  readonly idlePlaceholder: "Ask a question or describe a task...";
81
83
  readonly runningPlaceholder: "Add a follow-up...";
82
84
  };
@@ -34,5 +34,8 @@ export const interactionCopy = {
34
34
  governPausePending: 'pause pending: /gov pause cancel',
35
35
  help: '/help', shiftNewline: 'Shift+Enter newline needs terminal support',
36
36
  mode: 'Shift+Tab switch mode', update: '/update',
37
+ // The wait card's hint row (extended-job-wait): a user staring at a 20-minute
38
+ // wait needs to know they are not trapped in it.
39
+ waitHintSend: 'Send a message to end the wait early', waitHintStop: 'Esc x2 stops the whole run',
37
40
  idlePlaceholder: 'Ask a question or describe a task...', runningPlaceholder: 'Add a follow-up...',
38
41
  };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Invocation card layout (tui-invocation-cards section 3.7): what a `user` entry that
3
+ * carries an `invocation` says, as pure data. No Ink, no React. ASCII ONLY.
4
+ *
5
+ * THE CARD'S HEIGHT IS A FUNCTION OF ITS DATA AND THE USABLE WIDTH (I-4).
6
+ * `InvocationCard.tsx` renders exactly the rows `invocationCardRows` counts, and both
7
+ * read `INVOCATION_LIMITS` plus the one `invocationInputText` below - an estimate that
8
+ * disagrees with the painter is permanent, because an over-estimated entry is never
9
+ * mounted and so never measured (see `virtual-window.ts`).
10
+ */
11
+ import type { InvocationCard } from '../../agent/invocation-card.js';
12
+ import type { Glyphs } from '../glyphs.js';
13
+ /** Colour slots the component maps onto the theme; kept local so this file needs no `Theme`. */
14
+ export type InvocationTone = 'label' | 'name' | 'muted';
15
+ export interface InvocationSegment {
16
+ text: string;
17
+ tone: InvocationTone;
18
+ bold: boolean;
19
+ }
20
+ /**
21
+ * The header row, already fitted to `usable` columns (RV-10). Narrow terminals drop
22
+ * facts from the right WHOLE before anything else is cut; only when the identity alone
23
+ * is too wide is the name shortened. Ink's `wrap="truncate"` stays on the row as a
24
+ * backstop, but it is never what produces the visible ellipsis.
25
+ */
26
+ export declare function invocationHeaderSegments(card: InvocationCard, glyphs: Glyphs, usable: number): InvocationSegment[];
27
+ /** The text painted for `input`: tabs expanded per line, the same rule `UserEntry` follows. */
28
+ export declare function invocationInputText(card: InvocationCard): string;
29
+ /** Singular/plural for the hidden-characters tail. */
30
+ export declare function invocationHiddenText(card: InvocationCard, glyphs: Glyphs): string;
31
+ /**
32
+ * Rows the card occupies at `usable` columns. `wrappedRows` is injected by the caller
33
+ * (`virtual-window.ts` owns the one wrapping rule) so this file stays import-cycle free.
34
+ */
35
+ export declare function invocationCardRows(card: InvocationCard, usable: number, wrappedRows: (text: string, usable: number) => number): number;
36
+ /**
37
+ * The lines the exit replay / export prints UNDER the command line (section 2.4):
38
+ * a bracketed fact line, and the disclosure note when there is one. Plain ASCII
39
+ * separators - the replay is pasted into issues and must survive any terminal.
40
+ */
41
+ export declare function invocationCardLines(card: InvocationCard): string[];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Invocation card layout (tui-invocation-cards section 3.7): what a `user` entry that
3
+ * carries an `invocation` says, as pure data. No Ink, no React. ASCII ONLY.
4
+ *
5
+ * THE CARD'S HEIGHT IS A FUNCTION OF ITS DATA AND THE USABLE WIDTH (I-4).
6
+ * `InvocationCard.tsx` renders exactly the rows `invocationCardRows` counts, and both
7
+ * read `INVOCATION_LIMITS` plus the one `invocationInputText` below - an estimate that
8
+ * disagrees with the painter is permanent, because an over-estimated entry is never
9
+ * mounted and so never measured (see `virtual-window.ts`).
10
+ */
11
+ import { expandTabs } from '../display-tab.js';
12
+ import { cellWidth, fitCells } from './cells.js';
13
+ /** Two spaces between the identity (`skill . pdf-forms`) and the facts. */
14
+ const FACT_GAP = ' ';
15
+ function seg(text, tone, bold = false) {
16
+ return { text, tone, bold };
17
+ }
18
+ function segmentsWidth(segments) {
19
+ return segments.reduce((sum, part) => sum + cellWidth(part.text), 0);
20
+ }
21
+ function identitySegments(card, glyphs, name) {
22
+ const parts = [seg(card.variant, 'label', true)];
23
+ if (name !== '')
24
+ parts.push(seg(` ${glyphs.midDot} `, 'muted'), seg(name, 'name', true));
25
+ return parts;
26
+ }
27
+ function factSegments(facts, glyphs) {
28
+ if (facts.length === 0)
29
+ return [];
30
+ const parts = [seg(FACT_GAP, 'muted')];
31
+ facts.forEach((fact, index) => {
32
+ if (index > 0)
33
+ parts.push(seg(` ${glyphs.midDot} `, 'muted'));
34
+ parts.push(seg(fact, 'muted'));
35
+ });
36
+ return parts;
37
+ }
38
+ /**
39
+ * The header row, already fitted to `usable` columns (RV-10). Narrow terminals drop
40
+ * facts from the right WHOLE before anything else is cut; only when the identity alone
41
+ * is too wide is the name shortened. Ink's `wrap="truncate"` stays on the row as a
42
+ * backstop, but it is never what produces the visible ellipsis.
43
+ */
44
+ export function invocationHeaderSegments(card, glyphs, usable) {
45
+ const width = Math.max(1, Math.floor(usable));
46
+ for (let kept = card.facts.length; kept >= 0; kept -= 1) {
47
+ const candidate = [
48
+ ...identitySegments(card, glyphs, card.name),
49
+ ...factSegments(card.facts.slice(0, kept), glyphs),
50
+ ];
51
+ if (segmentsWidth(candidate) <= width)
52
+ return candidate;
53
+ }
54
+ const head = identitySegments(card, glyphs, '');
55
+ const room = width - segmentsWidth(head) - cellWidth(` ${glyphs.midDot} `);
56
+ if (card.name !== '' && room > 1) {
57
+ return identitySegments(card, glyphs, fitCells(card.name, room, glyphs.ellipsis));
58
+ }
59
+ return [seg(fitCells(card.variant, width, glyphs.ellipsis), 'label', true)];
60
+ }
61
+ /** The text painted for `input`: tabs expanded per line, the same rule `UserEntry` follows. */
62
+ export function invocationInputText(card) {
63
+ return card.input.split('\n').map((line) => expandTabs(line)).join('\n');
64
+ }
65
+ /** Singular/plural for the hidden-characters tail. */
66
+ export function invocationHiddenText(card, glyphs) {
67
+ const unit = card.inputHidden === 1 ? 'char' : 'chars';
68
+ return `${glyphs.ellipsis} +${card.inputHidden} ${unit} not shown`;
69
+ }
70
+ /**
71
+ * Rows the card occupies at `usable` columns. `wrappedRows` is injected by the caller
72
+ * (`virtual-window.ts` owns the one wrapping rule) so this file stays import-cycle free.
73
+ */
74
+ export function invocationCardRows(card, usable, wrappedRows) {
75
+ const inputRows = card.input === '' ? 0 : wrappedRows(invocationInputText(card), usable);
76
+ return 1 + inputRows + (card.inputHidden > 0 ? 1 : 0) + (card.note ? 1 : 0);
77
+ }
78
+ const REPLAY_INDENT = ' ';
79
+ /**
80
+ * The lines the exit replay / export prints UNDER the command line (section 2.4):
81
+ * a bracketed fact line, and the disclosure note when there is one. Plain ASCII
82
+ * separators - the replay is pasted into issues and must survive any terminal.
83
+ */
84
+ export function invocationCardLines(card) {
85
+ const identity = card.name === '' ? card.variant : `${card.variant} ${card.name}`;
86
+ const detail = card.facts.length === 0 ? identity : `${identity} ${card.facts.join(' - ')}`;
87
+ const lines = [`${REPLAY_INDENT}[${detail}]`];
88
+ if (card.note)
89
+ lines.push(`${REPLAY_INDENT}${card.note}`);
90
+ return lines;
91
+ }
@@ -26,6 +26,7 @@ export interface StatusLayoutInput {
26
26
  wait?: {
27
27
  jobId: string;
28
28
  upToSeconds: number;
29
+ elapsedSeconds?: number;
29
30
  };
30
31
  runOutcome?: 'none' | 'ended' | 'interrupted' | 'failed';
31
32
  spinner?: string;
@@ -31,7 +31,16 @@ export function phaseLabels(input) {
31
31
  // quiet terminal actually has. Both forms carry it - the short rung is
32
32
  // truncated by width, not reworded.
33
33
  if (input.wait) {
34
- const label = `Waiting on ${cleanStatusText(input.wait.jobId)} ` +
34
+ const id = cleanStatusText(input.wait.jobId);
35
+ // WITH A CLOCK (extended-job-wait): `Waiting on t3 4m12s/10m00s`, and a shorter
36
+ // rung that keeps the id and the elapsed time. Without one the original
37
+ // sentence is byte-identical, which the older assertions rely on.
38
+ if (input.wait.elapsedSeconds !== undefined) {
39
+ const elapsed = shortDuration(Math.max(0, input.wait.elapsedSeconds) * 1000);
40
+ const upTo = shortDuration(Math.max(0, input.wait.upToSeconds) * 1000);
41
+ return [`Wait ${id} ${elapsed}`, `Waiting on ${id} ${elapsed}/${upTo}`];
42
+ }
43
+ const label = `Waiting on ${id} ` +
35
44
  `(up to ${Math.max(0, Math.round(input.wait.upToSeconds))}s)`;
36
45
  return [label, label];
37
46
  }
@@ -34,12 +34,14 @@
34
34
  */
35
35
  import stringWidth from 'string-width';
36
36
  import { governanceCardRows } from './governance-card.js';
37
+ import { WAIT_CARD_ROWS } from './wait-bar.js';
37
38
  import { FAST_LIMITS } from '../../fast/limits.js';
38
39
  import { COMPACTION_LIMITS } from '../../compaction/limits.js';
39
40
  import { GUTTER_WIDTH } from './Gutter.js';
40
41
  import { expandTabs } from '../display-tab.js';
41
42
  import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
42
43
  import { isLiveTeamCard } from '../team-status.js';
44
+ import { invocationCardRows } from './invocation-card.js';
43
45
  export const VIRTUAL_LIMITS = {
44
46
  /**
45
47
  * Entries kept mounted on each side of the visible band.
@@ -68,8 +70,12 @@ export const VIRTUAL_LIMITS = {
68
70
  */
69
71
  export function entryRevision(entry) {
70
72
  switch (entry.kind) {
73
+ // A card's shape is fixed at creation; the suffix only tells "card" from "plain" (and
74
+ // the three row-bearing parts), which is what makes a `queued -> user` rewrite re-measure.
71
75
  case 'user':
72
- return 'u';
76
+ return entry.invocation
77
+ ? `i${entry.invocation.input.length}.${entry.invocation.inputHidden > 0 ? 1 : 0}.${entry.invocation.note ? 1 : 0}`
78
+ : 'u';
73
79
  case 'notice':
74
80
  return 'n';
75
81
  // `thinkingMs` is O(1) and it CHANGES THE ROW: sealing the clock turns
@@ -91,7 +97,7 @@ export function entryRevision(entry) {
91
97
  // field makes it seven, and a term that is not O(1) would put the cost this
92
98
  // module exists to remove back on every frame.
93
99
  case 'tool':
94
- return `t${entry.status}.${entry.argsRaw.length}.${entry.preview?.length ?? 0}.${entry.durationMs ?? -1}.${entry.patch?.lineCount ?? -1}.${entry.liveSeq ?? -1}`;
100
+ return `t${entry.status}.${entry.argsRaw.length}.${entry.preview?.length ?? 0}.${entry.durationMs ?? -1}.${entry.patch?.lineCount ?? -1}.${entry.liveSeq ?? -1}.w${entry.waitSeq ?? -1}`;
95
101
  case 'team':
96
102
  return `m${entry.active ? 1 : 0}.${entry.runs.length}.${entry.runs
97
103
  .map((r) => r.phase[0])
@@ -259,6 +265,11 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
259
265
  const separation = density === 'comfortable' ? 1 : 0;
260
266
  switch (entry.kind) {
261
267
  case 'user':
268
+ // A CARD HAS ITS OWN, BOUNDED HEIGHT (tui-invocation-cards I-4): the same
269
+ // `invocationCardRows` the painter is built from, never the command line's wrapped rows.
270
+ if (entry.invocation) {
271
+ return separation + invocationCardRows(entry.invocation, usable, wrappedRows);
272
+ }
262
273
  // MIRRORS `UserEntry`'s RENDER CAP (+1 for the `... +N more lines` tail).
263
274
  // THESE TWO NUMBERS ARE ONE NUMBER (I-13). Capping the renderer alone
264
275
  // would leave this function believing a 5 000-line message is 5 000 rows
@@ -310,6 +321,11 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
310
321
  // string). A `!settled` block alone would drop the second into
311
322
  // `wrappedRows(entry.preview, usable)` with `undefined`.
312
323
  if (!settled) {
324
+ // A RUNNING `job_wait` WITH A VIEW IS THE FIVE-ROW WAIT CARD, EXACTLY
325
+ // (extended-job-wait I-10): constant height is what lets this be an
326
+ // equality rather than an upper bound. Keep in step with `WAIT_CARD_ROWS`.
327
+ if (entry.name === 'job_wait' && entry.wait)
328
+ return separation + WAIT_CARD_ROWS;
313
329
  const live = entry.live?.length ?? 0;
314
330
  if (live === 0)
315
331
  return separation + 1;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pure pieces of the wait card (extended-job-wait §3.8.2).
3
+ *
4
+ * ASCII ONLY: `src/ui/**` is inside the glyph scanner's scope, so every
5
+ * non-ASCII cell arrives as an argument from `pickGlyphs`.
6
+ */
7
+ /**
8
+ * Rows the wait card occupies, header included, in every state (I-10).
9
+ * Here rather than beside the component so `virtual-window.ts` can import it
10
+ * without pulling React into a pure layout module.
11
+ */
12
+ export declare const WAIT_CARD_ROWS = 5;
13
+ /** The two glyphs a bar is made of; a subset of the `pickGlyphs` result. */
14
+ export interface BarGlyphs {
15
+ gaugeFull: string;
16
+ gaugeEmpty: string;
17
+ }
18
+ /** Narrowest terminal that still gets a bar; below it the row is text only. */
19
+ export declare const WAIT_BAR_MIN_COLS = 40;
20
+ /** Bar width for a terminal `cols` wide: about a third of the row, 8..28 cells. */
21
+ export declare function barCellsFor(cols: number): number;
22
+ /**
23
+ * A `cells`-wide bar for `fraction` in 0..1.
24
+ *
25
+ * AN UNFINISHED BAR IS NEVER FULL (I-8): anything strictly between 0 and 1 shows
26
+ * at least one filled and at least one empty cell, so a 99.9% job does not read
27
+ * as done. `NaN`, negatives and non-finite values are an empty bar rather than a
28
+ * throw - this runs inside a render.
29
+ */
30
+ export declare function renderBar(fraction: number, cells: number, glyphs: BarGlyphs): string;
31
+ /** `4m12s`, `10m00s`, `45s` - the one clock format of the card and the status bar. */
32
+ export declare function formatWaitClock(ms: number): string;
33
+ /** Whole percent for a 0..1 fraction, clamped. */
34
+ export declare function percentOf(fraction: number): number;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Pure pieces of the wait card (extended-job-wait §3.8.2).
3
+ *
4
+ * ASCII ONLY: `src/ui/**` is inside the glyph scanner's scope, so every
5
+ * non-ASCII cell arrives as an argument from `pickGlyphs`.
6
+ */
7
+ import { shortDuration } from './status-layout.js';
8
+ /**
9
+ * Rows the wait card occupies, header included, in every state (I-10).
10
+ * Here rather than beside the component so `virtual-window.ts` can import it
11
+ * without pulling React into a pure layout module.
12
+ */
13
+ export const WAIT_CARD_ROWS = 5;
14
+ /** Narrowest terminal that still gets a bar; below it the row is text only. */
15
+ export const WAIT_BAR_MIN_COLS = 40;
16
+ /** Bar width for a terminal `cols` wide: about a third of the row, 8..28 cells. */
17
+ export function barCellsFor(cols) {
18
+ if (!Number.isFinite(cols))
19
+ return 8;
20
+ return Math.max(8, Math.min(28, Math.floor((cols - 4) * 0.35)));
21
+ }
22
+ /**
23
+ * A `cells`-wide bar for `fraction` in 0..1.
24
+ *
25
+ * AN UNFINISHED BAR IS NEVER FULL (I-8): anything strictly between 0 and 1 shows
26
+ * at least one filled and at least one empty cell, so a 99.9% job does not read
27
+ * as done. `NaN`, negatives and non-finite values are an empty bar rather than a
28
+ * throw - this runs inside a render.
29
+ */
30
+ export function renderBar(fraction, cells, glyphs) {
31
+ const width = Math.max(1, Math.floor(cells));
32
+ if (!Number.isFinite(fraction) || fraction <= 0)
33
+ return glyphs.gaugeEmpty.repeat(width);
34
+ if (fraction >= 1)
35
+ return glyphs.gaugeFull.repeat(width);
36
+ const filled = Math.min(width - 1, Math.max(1, Math.round(fraction * width)));
37
+ return glyphs.gaugeFull.repeat(filled) + glyphs.gaugeEmpty.repeat(width - filled);
38
+ }
39
+ /** `4m12s`, `10m00s`, `45s` - the one clock format of the card and the status bar. */
40
+ export function formatWaitClock(ms) {
41
+ return shortDuration(ms);
42
+ }
43
+ /** Whole percent for a 0..1 fraction, clamped. */
44
+ export function percentOf(fraction) {
45
+ if (!Number.isFinite(fraction))
46
+ return 0;
47
+ return Math.max(0, Math.min(100, Math.floor(fraction * 100)));
48
+ }
@@ -22,12 +22,14 @@ export interface RunActivity {
22
22
  *
23
23
  * `Running job_wait` is a TOOL NAME; `Waiting on t3 (up to 30s)` is a STATE, and
24
24
  * the state is what answers the user's two questions - what is it waiting for,
25
- * and how long may that take. No countdown: a ticking number would need its own
26
- * re-render, and the sentence is already complete.
25
+ * and how long may that take. `elapsedSeconds`, when present (extended-job-wait),
26
+ * turns the sentence into a running clock - the 1 Hz transcript tick already
27
+ * causes that render, so no timer is added.
27
28
  */
28
29
  wait?: {
29
30
  jobId: string;
30
31
  upToSeconds: number;
32
+ elapsedSeconds?: number;
31
33
  };
32
34
  }
33
35
  export interface RunRowPlanInput {
@@ -22,6 +22,7 @@ import { wrapToRows } from '../layout/wrap-rows.js';
22
22
  import { isExcluded } from './settled-boundary.js';
23
23
  import { isLiveTeamCard, outcomePhrase, rosterCounts } from '../team-status.js';
24
24
  import { clipLine } from './writer-shapes.js';
25
+ import { formatWaitClock } from '../layout/wait-bar.js';
25
26
  const MAX_TEAM_RUN_ROWS = 3;
26
27
  const TOOL_LIVE_ROWS = 6;
27
28
  function firstLine(text) {
@@ -49,6 +50,17 @@ function assistantRows(entry, cols, rows, thinkingVisible) {
49
50
  return out;
50
51
  }
51
52
  function toolRows(entry, cols) {
53
+ // A RUNNING WAIT IS ONE LINE HERE, not a miniature of the five-row card
54
+ // (extended-job-wait RV-3). `job_wait` never streams, so `live` is empty and the
55
+ // generic row would say only `> job_wait`; the clock comes from the view's own
56
+ // epoch stamps (`beatAt` is the view's last beat), keeping this function pure.
57
+ if (entry.name === 'job_wait' && entry.status === 'running' && entry.wait) {
58
+ const { wait } = entry;
59
+ const waited = Math.max(0, wait.beatAt - wait.waitStartedAt);
60
+ return [
61
+ fit(`> Waiting for ${wait.jobLabel} ${formatWaitClock(waited)} of ${formatWaitClock(wait.upToMs)}`, cols),
62
+ ];
63
+ }
52
64
  const head = entry.label && entry.label.length > 0 ? entry.label : entry.name;
53
65
  const out = [fit(`${entry.status === 'running' ? '>' : '.'} ${head}`, cols)];
54
66
  const live = (entry.live ?? []).slice(-TOOL_LIVE_ROWS);
@@ -3,6 +3,7 @@ import { formatRetryLine } from '../agent/retry-view.js';
3
3
  import { childStatus, outcomePhrase, rosterCounts, statusMarkerCell } from './team-status.js';
4
4
  import { expandTabs } from './display-tab.js';
5
5
  import { governanceCardLines } from './layout/governance-card.js';
6
+ import { invocationCardLines } from './layout/invocation-card.js';
6
7
  const DEFAULT_MAX_ENTRIES = 200;
7
8
  const CONTINUATION_INDENT = ' ';
8
9
  const TOOL_INDENT = ' ';
@@ -87,7 +88,9 @@ function renderEntry(entry, glyphs) {
87
88
  case 'queued':
88
89
  return block('Queued but never sent:', entry.text);
89
90
  case 'user':
90
- return block(glyphs.user, entry.text);
91
+ return entry.invocation
92
+ ? [...block(glyphs.user, entry.text), ...invocationCardLines(entry.invocation)]
93
+ : block(glyphs.user, entry.text);
91
94
  case 'assistant': {
92
95
  const suffix = entry.aborted ? ' (aborted)' : '';
93
96
  return block(glyphs.assistant, `${entry.text}${suffix}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.45",
3
+ "version": "0.6.47",
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",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@aragon-agent/core": "^0.2.57",
59
+ "@aragon-agent/core": "^0.2.59",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",