@aragon-agent/cli 0.6.14 → 0.6.16

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 (131) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +295 -238
  3. package/dist/agent/controller.d.ts +37 -1
  4. package/dist/agent/controller.js +195 -45
  5. package/dist/agent/plan-prompt.d.ts +1 -1
  6. package/dist/agent/plan-prompt.js +4 -3
  7. package/dist/agent/project-guidance-prompt.d.ts +4 -0
  8. package/dist/agent/project-guidance-prompt.js +104 -0
  9. package/dist/agent/reducer.d.ts +2 -0
  10. package/dist/agent/reducer.js +4 -0
  11. package/dist/agent/system-prompt.js +4 -0
  12. package/dist/cli.js +49 -2
  13. package/dist/commands/builtins.js +26 -28
  14. package/dist/compaction/child.d.ts +9 -1
  15. package/dist/compaction/child.js +39 -13
  16. package/dist/compaction/command.js +20 -10
  17. package/dist/compaction/compactor.d.ts +33 -204
  18. package/dist/compaction/compactor.js +311 -726
  19. package/dist/compaction/context-command.js +14 -3
  20. package/dist/compaction/digest.d.ts +15 -51
  21. package/dist/compaction/digest.js +113 -148
  22. package/dist/compaction/limits.d.ts +19 -142
  23. package/dist/compaction/limits.js +20 -147
  24. package/dist/compaction/memory-identity.d.ts +16 -0
  25. package/dist/compaction/memory-identity.js +63 -0
  26. package/dist/compaction/memory-input.d.ts +25 -0
  27. package/dist/compaction/memory-input.js +106 -0
  28. package/dist/compaction/memory-render.d.ts +4 -0
  29. package/dist/compaction/memory-render.js +47 -0
  30. package/dist/compaction/memory-validation.d.ts +21 -0
  31. package/dist/compaction/memory-validation.js +268 -0
  32. package/dist/compaction/memory.d.ts +79 -0
  33. package/dist/compaction/memory.js +118 -0
  34. package/dist/compaction/meter.d.ts +14 -10
  35. package/dist/compaction/meter.js +53 -19
  36. package/dist/compaction/operation.d.ts +18 -0
  37. package/dist/compaction/operation.js +68 -0
  38. package/dist/compaction/pressure.d.ts +5 -20
  39. package/dist/compaction/pressure.js +13 -23
  40. package/dist/compaction/summarize-call.d.ts +3 -16
  41. package/dist/compaction/summarize-call.js +20 -20
  42. package/dist/compaction/summary-prompt.d.ts +29 -66
  43. package/dist/compaction/summary-prompt.js +132 -178
  44. package/dist/compaction/types.d.ts +27 -4
  45. package/dist/compaction/wiring.d.ts +10 -25
  46. package/dist/compaction/wiring.js +56 -66
  47. package/dist/config/app-paths.d.ts +10 -0
  48. package/dist/config/app-paths.js +12 -0
  49. package/dist/config/env.d.ts +2 -1
  50. package/dist/config/env.js +13 -1
  51. package/dist/config/load.d.ts +9 -0
  52. package/dist/config/load.js +14 -0
  53. package/dist/config/model-windows.d.ts +91 -0
  54. package/dist/config/model-windows.js +181 -0
  55. package/dist/config/schema.d.ts +18 -1
  56. package/dist/config/schema.js +2 -1
  57. package/dist/exec/events.d.ts +8 -0
  58. package/dist/exec/index.js +13 -6
  59. package/dist/exec/runner.d.ts +8 -0
  60. package/dist/exec/runner.js +26 -2
  61. package/dist/input/csiu-keys.d.ts +66 -0
  62. package/dist/input/csiu-keys.js +170 -0
  63. package/dist/input/keyboard-enhancement.d.ts +42 -0
  64. package/dist/input/keyboard-enhancement.js +65 -0
  65. package/dist/input/stdin-filter.d.ts +9 -0
  66. package/dist/input/stdin-filter.js +45 -9
  67. package/dist/input/win32-input-mode.d.ts +61 -0
  68. package/dist/input/win32-input-mode.js +251 -0
  69. package/dist/session/persist.d.ts +3 -0
  70. package/dist/session/persist.js +9 -0
  71. package/dist/session/store.js +2 -0
  72. package/dist/session/validate-session.js +34 -0
  73. package/dist/team/subagent.js +8 -0
  74. package/dist/ui/App.js +218 -116
  75. package/dist/ui/BottomStatusRow.d.ts +5 -10
  76. package/dist/ui/BottomStatusRow.js +7 -11
  77. package/dist/ui/Composer.d.ts +4 -2
  78. package/dist/ui/Composer.js +2 -2
  79. package/dist/ui/Header.d.ts +6 -2
  80. package/dist/ui/Header.js +7 -6
  81. package/dist/ui/PromptInput.d.ts +3 -1
  82. package/dist/ui/PromptInput.js +32 -41
  83. package/dist/ui/StatusBar.d.ts +6 -0
  84. package/dist/ui/StatusBar.js +8 -7
  85. package/dist/ui/Transcript.js +1 -1
  86. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  87. package/dist/ui/entries/CompactionCard.js +7 -5
  88. package/dist/ui/frame-differ.js +27 -0
  89. package/dist/ui/frame-observer.js +18 -5
  90. package/dist/ui/interaction-copy.d.ts +73 -73
  91. package/dist/ui/interaction-copy.js +30 -38
  92. package/dist/ui/interaction-hints.d.ts +8 -1
  93. package/dist/ui/interaction-hints.js +56 -69
  94. package/dist/ui/layout/AppShell.d.ts +6 -2
  95. package/dist/ui/layout/AppShell.js +2 -2
  96. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  97. package/dist/ui/layout/OverlayFrame.js +23 -14
  98. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  99. package/dist/ui/layout/ScrollViewport.js +10 -2
  100. package/dist/ui/layout/budget.d.ts +25 -26
  101. package/dist/ui/layout/budget.js +29 -22
  102. package/dist/ui/layout/document-layout.d.ts +2 -2
  103. package/dist/ui/layout/document-layout.js +2 -3
  104. package/dist/ui/layout/queue-layout.js +4 -4
  105. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  106. package/dist/ui/layout/status-detail-layout.js +56 -0
  107. package/dist/ui/layout/status-layout.d.ts +19 -2
  108. package/dist/ui/layout/status-layout.js +95 -84
  109. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  110. package/dist/ui/layout/wrap-rows.js +21 -7
  111. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  112. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  113. package/dist/ui/overlays/HelpOverlay.js +9 -2
  114. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  115. package/dist/ui/overlays/ModelPicker.js +44 -10
  116. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  117. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  118. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  119. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  120. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  121. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  122. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  123. package/dist/ui/overlays/QueueOverlay.js +2 -2
  124. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  125. package/dist/ui/overlays/SettingsScreen.js +10 -3
  126. package/dist/ui/run-metrics.d.ts +23 -0
  127. package/dist/ui/run-metrics.js +27 -0
  128. package/dist/ui/status-feedback.d.ts +33 -0
  129. package/dist/ui/status-feedback.js +36 -0
  130. package/dist/ui/stdout-frame-writer.js +28 -0
  131. package/package.json +2 -2
@@ -1,29 +1,28 @@
1
- /** Fullscreen reserves three fixed rows; composer height is a footer layout input. */
2
- /** Below this many rows the composer's hint line is dropped to buy back a row. */
3
1
  export declare const HINT_MIN_ROWS = 20;
4
- export interface ChromeBudget {
5
- /** Brand bar. Constant 1 in full-screen — this is what makes A-3 hold. */
6
- header: number;
7
- toast: number;
8
- /**
9
- * Round border (2) + the DRAFT's own rows, plus the hint row when it fits.
10
- *
11
- * The draft term used to be the constant 1 that the `3` / `4` below encode.
12
- * That constant is what made a pasted 200-line draft draw the transcript 197
13
- * rows shorter than every consumer of `viewportRows` believed -- the trap
14
- * `BottomStatusRow.tsx:5-27` already documents at 1/200th the magnitude.
15
- */
16
- composer: number;
17
- status: number;
2
+ export interface FrameBudgetInput {
3
+ rows?: number;
4
+ cols?: number;
5
+ draftRows: number;
6
+ popupRows: number;
7
+ statusExpanded: boolean;
18
8
  }
19
- /**
20
- * The composer's draft rows, clamped to what this terminal height allows.
21
- *
22
- * Exported so the reporter and the budget clamp with ONE function: I-8 requires
23
- * the rendered row count and the number handed to `viewportRows` to be the same
24
- * number, and two clamps are how they stop being.
25
- */
9
+ export interface FrameBudget {
10
+ rows: number;
11
+ cols: number;
12
+ inactive: boolean;
13
+ frameRows: number;
14
+ headerRows: number;
15
+ statusRows: 0 | 1 | 2;
16
+ composerRows: number;
17
+ composerSlotRows: number;
18
+ popupRows: number;
19
+ popupMaxHeight: number;
20
+ viewportRows: number;
21
+ composerCols: number;
22
+ }
23
+ /** Clamp the displayed draft, independently of any transcript or panel content. */
26
24
  export declare function clampDraftRows(rows: number, draftRows: number): number;
27
- export declare function chromeBudget(rows: number, draftRows?: number): ChromeBudget;
28
- /** Shared document height; the legacy draft argument remains source-compatible. */
29
- export declare function viewportRows(rows: number, _draftRows?: number): number;
25
+ /** Total menu capacity, including its borders; reserve at least one message row. */
26
+ export declare function popupCapacity(rows: number, draftRows: number, statusExpanded: boolean): number;
27
+ /** Normalize terminal dimensions once and conserve every visible frame row. */
28
+ export declare function buildFrameBudget(input: FrameBudgetInput): FrameBudget;
@@ -1,30 +1,37 @@
1
- /** Fullscreen reserves three fixed rows; composer height is a footer layout input. */
1
+ /** One height budget for the transcript, fixed editor and status slots. */
2
2
  import { draftMaxRows } from '../composer-limits.js';
3
- import { frameHeight, MIN_FULLSCREEN_ROWS } from './frame.js';
4
- /** Below this many rows the composer's hint line is dropped to buy back a row. */
3
+ import { MIN_FULLSCREEN_ROWS, MIN_FULLSCREEN_COLS } from './frame.js';
5
4
  export const HINT_MIN_ROWS = 20;
6
- /**
7
- * The composer's draft rows, clamped to what this terminal height allows.
8
- *
9
- * Exported so the reporter and the budget clamp with ONE function: I-8 requires
10
- * the rendered row count and the number handed to `viewportRows` to be the same
11
- * number, and two clamps are how they stop being.
12
- */
5
+ const dimension = (value, fallback) => value !== undefined && Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback;
6
+ /** Clamp the displayed draft, independently of any transcript or panel content. */
13
7
  export function clampDraftRows(rows, draftRows) {
14
8
  const wanted = Number.isFinite(draftRows) ? Math.floor(draftRows) : 1;
15
9
  return Math.max(1, Math.min(wanted, draftMaxRows(rows)));
16
10
  }
17
- export function chromeBudget(rows, draftRows = 1) {
18
- return {
19
- header: 1,
20
- toast: 1,
21
- composer: 2 + clampDraftRows(rows, draftRows),
22
- status: 1,
23
- };
11
+ /** Total menu capacity, including its borders; reserve at least one message row. */
12
+ export function popupCapacity(rows, draftRows, statusExpanded) {
13
+ return Math.max(0, rows - 1 - 1 - (statusExpanded ? 2 : 1)
14
+ - 2 - clampDraftRows(rows, draftRows) - 1);
24
15
  }
25
- /** Shared document height; the legacy draft argument remains source-compatible. */
26
- export function viewportRows(rows, _draftRows = 1) {
27
- if (!Number.isFinite(rows) || rows < MIN_FULLSCREEN_ROWS)
28
- return 0;
29
- return Math.max(0, frameHeight(rows) - 3);
16
+ /** Normalize terminal dimensions once and conserve every visible frame row. */
17
+ export function buildFrameBudget(input) {
18
+ const rows = dimension(input.rows, 24);
19
+ const cols = dimension(input.cols, 80);
20
+ const frameRows = rows - 1;
21
+ const composerCols = Math.max(0, cols - 1);
22
+ const inactive = rows < MIN_FULLSCREEN_ROWS || cols < MIN_FULLSCREEN_COLS;
23
+ if (inactive)
24
+ return { rows, cols, inactive, frameRows, composerCols, headerRows: 0,
25
+ statusRows: 0, composerRows: 0, composerSlotRows: 0, popupRows: 0,
26
+ popupMaxHeight: 0, viewportRows: 0 };
27
+ const statusRows = input.statusExpanded ? 2 : 1;
28
+ const composerRows = 2 + clampDraftRows(rows, input.draftRows);
29
+ const popupMaxHeight = popupCapacity(rows, input.draftRows, input.statusExpanded);
30
+ const requested = Number.isFinite(input.popupRows) ? Math.floor(input.popupRows) : 0;
31
+ const candidate = Math.max(0, Math.min(requested, popupMaxHeight));
32
+ const popupRows = candidate >= 3 ? candidate : 0;
33
+ const composerSlotRows = composerRows + popupRows;
34
+ return { rows, cols, inactive, frameRows, composerCols, headerRows: 1, statusRows,
35
+ composerRows, composerSlotRows, popupRows, popupMaxHeight,
36
+ viewportRows: frameRows - 1 - statusRows - composerSlotRows };
30
37
  }
@@ -1,5 +1,5 @@
1
1
  export interface DocumentLayoutInput {
2
- rows: number;
2
+ viewportRows: number;
3
3
  bodyRows: number;
4
4
  footerRows: number;
5
5
  }
@@ -9,5 +9,5 @@ export interface DocumentLayout {
9
9
  contentRows: number;
10
10
  trailingContentRows: number;
11
11
  }
12
- /** Allocate the shared document, filling short sessions before the editor. */
12
+ /** Allocate the shared document, filling short sessions before the panels. */
13
13
  export declare function buildDocumentLayout(input: DocumentLayoutInput): DocumentLayout;
@@ -1,8 +1,7 @@
1
- import { frameHeight } from './frame.js';
2
- /** Allocate the shared document, filling short sessions before the editor. */
1
+ /** Allocate the shared document, filling short sessions before the panels. */
3
2
  export function buildDocumentLayout(input) {
4
3
  const count = (n) => Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
5
- const viewportRows = count(input.rows) > 0 ? Math.max(0, frameHeight(count(input.rows)) - 3) : 0;
4
+ const viewportRows = count(input.viewportRows);
6
5
  const body = count(input.bodyRows);
7
6
  const footer = count(input.footerRows);
8
7
  const paddingRows = Math.max(0, viewportRows - body - footer);
@@ -10,13 +10,13 @@ export function buildQueueLayout(input) {
10
10
  const items = input.pending.slice(0, shown).map((item, index) => {
11
11
  const prefix = `${index + 1}. `;
12
12
  const lines = item.text.split(/\r\n|[\r\n]/).length;
13
- const suffix = lines > 1 ? ` (+${lines - 1}\u884c)` : '';
13
+ const suffix = lines > 1 ? ` (+${lines - 1} lines)` : '';
14
14
  return { queueId: item.queueId, label: prefix + truncateSummary(queueSummary(item.text), Math.max(0, input.columns - stringWidth(prefix + suffix)), '...') + suffix };
15
15
  });
16
16
  const hiddenCount = count - items.length;
17
- const paused = input.paused ? ' \u5df2\u6682\u505c' : '';
18
- const hidden = hiddenCount ? ` \u53e6${hiddenCount}\u6761` : '';
19
- const title = `Queue: \u5f85\u5904\u7406 ${count}${paused}${hidden} /queue`;
17
+ const paused = input.paused ? ' paused' : '';
18
+ const hidden = hiddenCount ? ` +${hiddenCount} more` : '';
19
+ const title = `Queue: ${count} pending${paused}${hidden} /queue`;
20
20
  if (limit === 1)
21
21
  return { rows: 1,
22
22
  title: truncateSummary(`Queue ${count} /queue ${items[0].label}`, input.columns, '...'),
@@ -0,0 +1,9 @@
1
+ import { type ActionHintInput } from '../interaction-hints.js';
2
+ import { type StatusLinePlan, type StatusPrimaryInput } from './status-layout.js';
3
+ export interface StatusDetailInput {
4
+ status: StatusPrimaryInput;
5
+ hints: ActionHintInput;
6
+ model: string;
7
+ provider: string;
8
+ }
9
+ export declare function planStatusDetail(input: StatusDetailInput): StatusLinePlan;
@@ -0,0 +1,56 @@
1
+ import { formatCost, formatTokens, promptTokensOf } from '../../agent/usage.js';
2
+ import { actionCandidates, clipStatusText, escapeClause } from '../interaction-hints.js';
3
+ import { statusField, statusLinePlan, validContext } from './status-layout.js';
4
+ export function planStatusDetail(input) {
5
+ const { status, hints } = input;
6
+ const budget = Math.max(0, Math.floor(status.columns) - 1);
7
+ const escape = escapeClause({ ...hints, escapeAction: status.escapeAction });
8
+ const reserved = [...(escape ? [statusField('escape', escape, 'warning')] : []),
9
+ statusField('toggle', hints.overlay ? 'Details on' : '^G less')];
10
+ const fields = [];
11
+ const add = (candidate) => {
12
+ if (statusLinePlan([...fields, candidate, ...reserved]).cells <= budget)
13
+ fields.push(candidate);
14
+ };
15
+ if (status.feedback) {
16
+ const remaining = budget - statusLinePlan(reserved).cells - (reserved.length ? 3 : 0);
17
+ const text = clipStatusText(status.feedback.text, Math.max(0, remaining));
18
+ if (text)
19
+ add(statusField('feedback', text, status.feedback.level === 'error' ? 'error'
20
+ : status.feedback.level === 'warn' ? 'warning' : 'normal'));
21
+ }
22
+ for (const [index, action] of actionCandidates(hints).entries())
23
+ add(statusField('action-' + index, action));
24
+ if (status.pendingCount > 0)
25
+ add(statusField('queue', 'Q' + status.pendingCount));
26
+ if (status.servicesActive?.live)
27
+ add(statusField('services', 'Svc' + status.servicesActive.live));
28
+ if (status.mode || status.pendingMode)
29
+ add(statusField('mode', status.pendingMode
30
+ ? (status.mode ?? 'build') + '>' + status.pendingMode : status.mode));
31
+ if (validContext(status.context)) {
32
+ const approximate = status.context.source === 'estimate' || status.context.deltaTokens > 0 ? '~' : '';
33
+ add(statusField('context', 'Context ' + approximate + formatTokens(status.context.occupied)
34
+ + '/' + formatTokens(status.context.window) + ' tokens'));
35
+ }
36
+ else
37
+ add(statusField('context', 'Context ? /context'));
38
+ if (status.speedKnown)
39
+ add(statusField('average', 'Avg: main output / run time'));
40
+ add(statusField('usage', 'Session input ' + formatTokens(promptTokensOf(status.usageTotal))
41
+ + ' output ' + formatTokens(status.usageTotal.outputTokens)));
42
+ if (Number.isFinite(status.usageTotal.costUsd))
43
+ add(statusField('cost', 'Cost ' + formatCost(status.usageTotal.costUsd) + ' est'));
44
+ if (status.teamActive)
45
+ add(statusField('team', 'Team ' + status.teamActive.running + '/' + status.teamActive.total));
46
+ if (status.todoActive)
47
+ add(statusField('todo', 'Todo ' + status.todoActive.done + '/' + status.todoActive.total));
48
+ add(statusField('model', input.provider + ':' + input.model));
49
+ if (status.fastActive)
50
+ add(statusField('fast', 'Fast' + (status.fastActive.inFlight ? ' active' : '')));
51
+ if (status.ecoRung)
52
+ add(statusField('eco', 'Eco'));
53
+ if (hints.updateAvailable)
54
+ add(statusField('update', '/update'));
55
+ return statusLinePlan([...fields, ...reserved]);
56
+ }
@@ -1,5 +1,6 @@
1
1
  import type { UsageTotal } from '../../agent/reducer.js';
2
2
  import type { ContextUsageSnapshot } from '../../compaction/types.js';
3
+ import type { EscapeAction, StatusFeedback } from '../status-feedback.js';
3
4
  export type RunPhase = 'idle' | 'starting' | 'waiting' | 'thinking' | 'generating' | 'preparing-tool' | 'tool';
4
5
  export interface StatusLayoutInput {
5
6
  columns: number;
@@ -43,13 +44,29 @@ export interface StatusLayoutInput {
43
44
  };
44
45
  scrolledLines?: number;
45
46
  }
47
+ export interface StatusPrimaryInput extends StatusLayoutInput {
48
+ feedback?: StatusFeedback;
49
+ escapeAction?: EscapeAction;
50
+ speedKnown: boolean;
51
+ interruptPhase?: 'ready' | 'armed' | 'stopping';
52
+ }
46
53
  export interface StatusField {
47
54
  id: string;
48
55
  text: string;
49
56
  cells: number;
50
57
  tone: 'normal' | 'muted' | 'warning' | 'error';
51
58
  }
59
+ export interface StatusLinePlan {
60
+ fields: readonly StatusField[];
61
+ separator: string;
62
+ separatorCells: number;
63
+ cells: number;
64
+ }
52
65
  export declare function cleanStatusText(text: string): string;
66
+ export declare function statusField(id: string, text: string, tone?: StatusField['tone']): StatusField;
67
+ export declare function statusLinePlan(fields: readonly StatusField[], separator?: string): StatusLinePlan;
53
68
  export declare function phaseLabels(input: Pick<StatusLayoutInput, 'phase' | 'activeTool' | 'runOutcome' | 'waitingForConfirmation' | 'stopping' | 'compacting' | 'retryActive'>): readonly [string, string];
54
- /** Allocate complete fields before rendering; only the redraw carrier lives outside this budget. */
55
- export declare function planStatusFields(input: StatusLayoutInput): readonly StatusField[];
69
+ export declare function validContext(context: ContextUsageSnapshot): boolean;
70
+ export declare function shortDuration(ms: number): string;
71
+ /** All five fields survive each width rung; only the redraw carrier is outside this plan. */
72
+ export declare function planPrimaryStatusFields(input: StatusPrimaryInput): StatusLinePlan;
@@ -1,11 +1,20 @@
1
1
  import stringWidth from 'string-width';
2
2
  import stripAnsi from 'strip-ansi';
3
- import { formatCost, formatDuration, formatTokens, promptTokensOf } from '../../agent/usage.js';
3
+ import { formatDuration } from '../../agent/usage.js';
4
4
  import { interactionCopy as copy } from '../interaction-copy.js';
5
5
  export function cleanStatusText(text) {
6
6
  // eslint-disable-next-line no-control-regex
7
7
  return stripAnsi(text).replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, ' ').trim();
8
8
  }
9
+ export function statusField(id, text, tone = 'muted') {
10
+ const clean = cleanStatusText(text);
11
+ return { id, text: clean, cells: stringWidth(clean), tone };
12
+ }
13
+ export function statusLinePlan(fields, separator = ' | ') {
14
+ const separatorCells = stringWidth(separator);
15
+ return { fields, separator, separatorCells,
16
+ cells: fields.reduce((sum, item) => sum + item.cells, 0) + Math.max(0, fields.length - 1) * separatorCells };
17
+ }
9
18
  export function phaseLabels(input) {
10
19
  if (input.waitingForConfirmation)
11
20
  return copy.confirming;
@@ -14,97 +23,99 @@ export function phaseLabels(input) {
14
23
  if (input.compacting)
15
24
  return copy.compacting;
16
25
  if (input.retryActive)
17
- return [copy.retrying[0], `${copy.retrying[1]} ${Math.max(0, Math.ceil(input.retryActive.secondsLeft))}s`];
18
- if (input.activeTool) {
19
- const name = input.activeTool.name;
20
- if (name === 'read_file')
21
- return copy.readFile;
22
- if (name === 'write_file' || name === 'edit_file')
23
- return copy.writeFile;
24
- if (name === 'bash')
25
- return copy.command;
26
- return [copy.tool[0], `${copy.tool[1]} ${cleanStatusText(name)}`];
27
- }
26
+ return copy.retrying;
27
+ if (input.activeTool)
28
+ return [copy.tool[0], copy.tool[1] + ' ' + cleanStatusText(input.activeTool.name)];
28
29
  if (input.phase === 'idle' && input.runOutcome && input.runOutcome !== 'none')
29
30
  return copy[input.runOutcome];
30
31
  if (input.phase === 'preparing-tool')
31
32
  return copy.preparingTool;
32
33
  return copy[input.phase];
33
34
  }
34
- const field = (id, text, tone = 'muted') => ({ id, text, cells: stringWidth(text), tone });
35
- const width = (fields) => fields.reduce((sum, item) => sum + item.cells, 0) + Math.max(0, fields.length - 1) * 3;
36
- const scientific = (value) => value.toExponential(1).replace(/\.0e/, 'e').replace('e+', 'e');
37
- const compactNumber = (value) => value >= 1e9 ? scientific(value) : formatTokens(value).replace(/\.0([kM])$/, '$1');
38
- function contextText(context, detail = false, exponent = false) {
39
- if (![context.occupied, context.window, context.deltaTokens, context.pct].every(Number.isFinite)
40
- || context.occupied < 0 || context.window <= 0)
41
- return copy.contextUnknown;
42
- const number = exponent ? scientific : detail ? formatTokens : compactNumber;
43
- const approximate = context.source === 'estimate' || context.deltaTokens > 0 ? '~' : '';
44
- const limit = context.windowKnown ? number(context.window) : '?';
45
- return `${copy.context} ${approximate}${number(context.occupied)}/${limit} ${detail ? 'tokens' : 'tok'}`;
35
+ export function validContext(context) {
36
+ return context.windowKnown && context.window > 0 &&
37
+ [context.occupied, context.window, context.pct, context.deltaTokens].every(n => Number.isFinite(n) && n >= 0);
46
38
  }
47
- function secondaryFields(input) {
48
- const list = [];
49
- if ((input.mode && input.mode !== 'build') || input.pendingMode)
50
- list.push(field('mode', input.pendingMode ? `${input.mode ?? 'build'}>${input.pendingMode}` : input.mode, 'normal'));
51
- if (input.servicesActive?.live)
52
- list.push(field('services', `${copy.services} ${input.servicesActive.live}`, 'warning'));
53
- if (Number.isFinite(input.usageTotal.costUsd))
54
- list.push(field('cost', `${copy.estimatedCost} ${formatCost(input.usageTotal.costUsd).slice(1)}`));
55
- if (input.phase !== 'idle') {
56
- if (input.tokPerSec > 0 && Number.isFinite(input.tokPerSec))
57
- list.push(field('speed', `${copy.speed} ${input.tokPerSec} tok/s`));
58
- if (Number.isFinite(input.elapsedMs))
59
- list.push(field('elapsed', `${copy.elapsed} ${formatDuration(input.elapsedMs)}`));
60
- }
61
- if (input.thinkingLevel !== 'off')
62
- list.push(field('thinking', `think:${cleanStatusText(input.thinkingLevel)}`));
63
- list.push(field('usage', `${copy.sessionInput} ${compactNumber(promptTokensOf(input.usageTotal))} ${copy.output} ${compactNumber(input.usageTotal.outputTokens)} tok`));
64
- if (input.context.windowKnown && Number.isFinite(input.context.pct))
65
- list.push(field('percentage', `${input.context.source === 'estimate' || input.context.deltaTokens > 0 || !input.context.windowKnown ? '~' : ''}${Math.round(input.context.pct)}%`));
66
- if (input.ecoRung)
67
- list.push(field('eco', 'eco'));
68
- if (input.fastActive)
69
- list.push(field('fast', `fast${input.fastActive.inFlight ? '*' : ''}`));
70
- if (input.teamActive)
71
- list.push(field('team', `agents ${input.teamActive.running}/${input.teamActive.total}`));
72
- if (input.todoActive)
73
- list.push(field('todo', `todo ${input.todoActive.done}/${input.todoActive.total}`));
74
- if (input.scrolledLines)
75
- list.push(field('scroll', `^${input.scrolledLines}`, 'warning'));
76
- return list;
39
+ function contextPercent(context) {
40
+ if (!validContext(context))
41
+ return '?';
42
+ const mark = context.source === 'estimate' || context.deltaTokens > 0 ? '~' : '';
43
+ return mark + (context.pct > 999 ? '>999' : Math.round(context.pct)) + '%';
77
44
  }
78
- /** Allocate complete fields before rendering; only the redraw carrier lives outside this budget. */
79
- export function planStatusFields(input) {
80
- const budget = Math.max(0, Math.floor(input.columns) - 1);
81
- const [short, long] = phaseLabels(input);
82
- const prefix = input.spinner ? `${input.spinner} ` : '';
83
- const phaseTone = input.runOutcome === 'failed' ? 'error' : 'normal';
84
- const fields = [field('phase', prefix + short, phaseTone)];
85
- if (input.pendingCount > 0)
86
- fields.push(field('queue', `${copy.queue} ${input.pendingCount}`, 'warning'));
87
- fields.push(field('context', contextText(input.context)));
88
- const contextIndex = fields.length - 1;
89
- if (width(fields) > budget)
90
- fields[contextIndex] = field('context', contextText(input.context, false, true));
91
- if (width(fields) > budget)
92
- fields[contextIndex] = field('context', '/context');
93
- if (width(fields) > budget && input.pendingCount > 0)
94
- fields[1] = field('queue', '/queue', 'warning');
95
- if (width(fields) > budget)
96
- return fields.filter((_f, index) => width(fields.slice(0, index + 1)) <= budget);
97
- for (const candidate of secondaryFields(input)) {
98
- if (width([...fields, candidate]) <= budget)
99
- fields.push(candidate);
45
+ export function shortDuration(ms) {
46
+ if (!Number.isFinite(ms) || ms <= 0)
47
+ return '0s';
48
+ const seconds = Math.floor(ms / 1000);
49
+ if (seconds < 60)
50
+ return seconds + 's';
51
+ if (seconds < 3600)
52
+ return Math.floor(seconds / 60) + 'm' + String(seconds % 60).padStart(2, '0') + 's';
53
+ if (seconds < 86400)
54
+ return Math.floor(seconds / 3600) + 'h' + String(Math.floor(seconds / 60) % 60).padStart(2, '0') + 'm';
55
+ const days = Math.floor(seconds / 86400);
56
+ return days > 999 ? '>999d' : days + 'd';
57
+ }
58
+ function speedNumber(input) {
59
+ if (!input.speedKnown || input.phase === 'idle' || input.elapsedMs <= 500
60
+ || !Number.isFinite(input.tokPerSec) || input.tokPerSec < 0)
61
+ return '--';
62
+ const value = input.tokPerSec;
63
+ if (value > 99e6)
64
+ return '>99M';
65
+ if (value >= 999950)
66
+ return (value / 1e6).toFixed(1) + 'M';
67
+ if (value > 999)
68
+ return (value / 1000).toFixed(1) + 'k';
69
+ return String(Math.round(value));
70
+ }
71
+ function feedbackLabel(input, form) {
72
+ const feedback = input.feedback;
73
+ if (!feedback)
74
+ return undefined;
75
+ const error = feedback.kind === 'copy-error' || feedback.kind === 'copy-cleanup';
76
+ if (error && input.escapeAction === 'force-stop') {
77
+ return form === 'full' ? 'Copy err / Esc force' : form === 'tiny' ? 'E/F' : 'Err/EscF';
100
78
  }
101
- const upgrades = [field('phase', prefix + long, phaseTone),
102
- field('queue', `${copy.pending} ${input.pendingCount}`, 'warning'),
103
- field('context', contextText(input.context, true))];
104
- for (const upgrade of upgrades) {
105
- const index = fields.findIndex((f) => f.id === upgrade.id);
106
- if (index >= 0 && width(fields) - fields[index].cells + upgrade.cells <= budget)
107
- fields[index] = upgrade;
79
+ if (error && input.escapeAction === 'confirm-stop' && input.interruptPhase === 'armed') {
80
+ return form === 'full' ? 'Copy err / Esc stop' : form === 'tiny' ? 'E/S' : 'Err/EscS';
108
81
  }
109
- return fields;
82
+ const labels = {
83
+ confirm: ['Awaiting confirmation', 'Ask', 'Ask'], stopping: ['Esc stop', 'EscS', 'EscS'],
84
+ 'force-stop': ['Esc force', 'EscF', 'EscF'], exit: ['^C exit', '^Cexit', '^C'],
85
+ copying: ['Copying', 'Copy', 'Copy'], copied: ['Copied', 'Copied', 'Copy'],
86
+ 'copy-sent': ['Copy sent', 'Sent', 'Sent'], 'copy-cleanup': ['Copy cleanup', 'Err', 'Err'],
87
+ 'copy-error': ['Copy err', 'Err', 'Err'], notice: ['Notice', 'Notice', 'Note'],
88
+ };
89
+ return labels[feedback.kind][form === 'full' ? 0 : form === 'short' ? 1 : 2];
90
+ }
91
+ const thinkingCodes = { off: 'O', minimal: 'N', low: 'L', medium: 'M', high: 'H', xhigh: 'X' };
92
+ /** All five fields survive each width rung; only the redraw carrier is outside this plan. */
93
+ export function planPrimaryStatusFields(input) {
94
+ const budget = Math.max(0, (Number.isFinite(input.columns) ? Math.floor(input.columns) : 80) - 1);
95
+ const labels = phaseLabels(input);
96
+ const glyphText = cleanStatusText(input.spinner ?? '-');
97
+ const glyph = stringWidth(glyphText) === 1 ? glyphText : '-';
98
+ const code = thinkingCodes[input.thinkingLevel] ?? '?';
99
+ const pct = contextPercent(input.context);
100
+ const speed = speedNumber(input);
101
+ const tone = input.feedback?.level === 'error' || input.runOutcome === 'failed'
102
+ ? 'error' : input.feedback?.level === 'warn' || input.waitingForConfirmation || input.stopping ? 'warning' : 'normal';
103
+ const build = (rung) => {
104
+ const full = rung === 0;
105
+ const label = feedbackLabel(input, full ? 'full' : rung >= 4 ? 'tiny' : 'short')
106
+ ?? (full ? labels[1] : rung >= 4 ? labels[0].slice(0, 4) : labels[0]);
107
+ return statusLinePlan([
108
+ statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label, tone),
109
+ statusField('context', (full ? 'Context ' : 'C') + pct),
110
+ statusField('thinking', full ? 'Think ' + (thinkingCodes[input.thinkingLevel] ? input.thinkingLevel : '?')
111
+ : (rung >= 2 ? '' : 'Th:') + code),
112
+ statusField('speed', speed + (full ? ' tok/s' : rung >= 3 ? '/s' : 't/s')),
113
+ statusField('elapsed', full && Number.isFinite(input.elapsedMs) && input.elapsedMs > 0
114
+ ? formatDuration(input.elapsedMs) : shortDuration(input.elapsedMs)),
115
+ ], full ? ' | ' : ' ');
116
+ };
117
+ let plan = build(0);
118
+ for (let rung = 1; plan.cells > budget && rung <= 5; rung++)
119
+ plan = build(rung);
120
+ return plan;
110
121
  }
@@ -18,6 +18,8 @@
18
18
  *
19
19
  * Prefers spaces; hard-breaks any single token longer than `width` (a URL or a
20
20
  * long path, which must not be dropped and cannot be wrapped politely).
21
+ * Width is measured in terminal cells and hard breaks preserve graphemes. A
22
+ * single grapheme wider than the limit stays intact on its own row.
21
23
  * Existing newlines are honoured as paragraph breaks. Returns `[]` for empty
22
24
  * input so callers can splice the result without an emptiness branch.
23
25
  */
@@ -1,3 +1,4 @@
1
+ import { graphemes, graphemeWidth } from '../editor-navigation.js';
1
2
  /**
2
3
  * `wrapToRows` — the prose wrapper for `OverlayFrame`'s controlled (mode A)
3
4
  * content (plan-mode §6.6).
@@ -18,6 +19,8 @@
18
19
  *
19
20
  * Prefers spaces; hard-breaks any single token longer than `width` (a URL or a
20
21
  * long path, which must not be dropped and cannot be wrapped politely).
22
+ * Width is measured in terminal cells and hard breaks preserve graphemes. A
23
+ * single grapheme wider than the limit stays intact on its own row.
21
24
  * Existing newlines are honoured as paragraph breaks. Returns `[]` for empty
22
25
  * input so callers can splice the result without an emptiness branch.
23
26
  */
@@ -40,32 +43,43 @@ export function wrapToRows(text, width) {
40
43
  function wrapParagraph(paragraph, limit) {
41
44
  const rows = [];
42
45
  let current = '';
46
+ let currentWidth = 0;
43
47
  for (const word of paragraph.split(/\s+/).filter((w) => w.length > 0)) {
44
- if (word.length > limit) {
48
+ const parts = graphemes(word);
49
+ const wordWidth = parts.reduce((sum, part) => sum + graphemeWidth(part.text), 0);
50
+ if (wordWidth > limit) {
45
51
  // Flush what we have, then chop the over-long token into full-width
46
52
  // pieces. Its tail becomes the new `current` so the next word can still
47
53
  // join it.
48
54
  if (current.length > 0) {
49
55
  rows.push(current);
50
56
  current = '';
57
+ currentWidth = 0;
51
58
  }
52
- let rest = word;
53
- while (rest.length > limit) {
54
- rows.push(rest.slice(0, limit));
55
- rest = rest.slice(limit);
59
+ for (const part of parts) {
60
+ const cells = graphemeWidth(part.text);
61
+ if (current && currentWidth + cells > limit) {
62
+ rows.push(current);
63
+ current = '';
64
+ currentWidth = 0;
65
+ }
66
+ current += part.text;
67
+ currentWidth += cells;
56
68
  }
57
- current = rest;
58
69
  continue;
59
70
  }
60
71
  if (current.length === 0) {
61
72
  current = word;
73
+ currentWidth = wordWidth;
62
74
  }
63
- else if (current.length + 1 + word.length <= limit) {
75
+ else if (currentWidth + 1 + wordWidth <= limit) {
64
76
  current = `${current} ${word}`;
77
+ currentWidth += 1 + wordWidth;
65
78
  }
66
79
  else {
67
80
  rows.push(current);
68
81
  current = word;
82
+ currentWidth = wordWidth;
69
83
  }
70
84
  }
71
85
  if (current.length > 0)
@@ -12,6 +12,7 @@ export interface ConfirmState {
12
12
  resolve: (approved: boolean) => void;
13
13
  }
14
14
  interface ConfirmDialogProps {
15
+ isActive?: boolean;
15
16
  state: ConfirmState;
16
17
  maxRows: number;
17
18
  cols: number;
@@ -26,5 +27,5 @@ interface ConfirmDialogProps {
26
27
  * title and border match the other three overlays; the existing `useInput` is
27
28
  * untouched.
28
29
  */
29
- export declare function ConfirmDialog({ state, maxRows, cols, theme, caps, onClose, }: ConfirmDialogProps): React.ReactElement;
30
+ export declare function ConfirmDialog({ state, isActive, maxRows, cols, theme, caps, onClose, }: ConfirmDialogProps): React.ReactElement;
30
31
  export {};
@@ -1,6 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Confirm dialog — Yes/No gate for `confirmTools` mode. Resolves the pending
4
+ * tool-execution promise (spec §3.6). `y` = approve; `n`/Enter/Esc = reject
5
+ * (Enter follows the safe default shown by the capital N in the "(y/N)" prompt,
6
+ * since this gates mutating tools like write_file/edit_file/bash).
7
+ */
8
+ import { useState } from 'react';
2
9
  import { Text, useInput } from 'ink';
3
10
  import { pickGlyphs } from '../glyphs.js';
11
+ import { wrapToRows } from '../layout/wrap-rows.js';
4
12
  import { OverlayFrame } from '../layout/OverlayFrame.js';
5
13
  /**
6
14
  * Self-managed (mode B) and deliberately without a position indicator: five
@@ -9,9 +17,20 @@ import { OverlayFrame } from '../layout/OverlayFrame.js';
9
17
  * title and border match the other three overlays; the existing `useInput` is
10
18
  * untouched.
11
19
  */
12
- export function ConfirmDialog({ state, maxRows, cols, theme, caps, onClose, }) {
20
+ export function ConfirmDialog({ state, isActive = true, maxRows, cols, theme, caps, onClose, }) {
13
21
  const glyphs = pickGlyphs(caps);
22
+ const [offset, setOffset] = useState(0);
14
23
  useInput((input, key) => {
24
+ if (!isActive)
25
+ return;
26
+ if (key.pageUp || key.upArrow) {
27
+ setOffset(n => Math.max(0, n - 1));
28
+ return;
29
+ }
30
+ if (key.pageDown || key.downArrow) {
31
+ setOffset(n => n + 1);
32
+ return;
33
+ }
15
34
  if (input === 'y' || input === 'Y') {
16
35
  state.resolve(true);
17
36
  onClose();
@@ -21,5 +40,5 @@ export function ConfirmDialog({ state, maxRows, cols, theme, caps, onClose, }) {
21
40
  onClose();
22
41
  }
23
42
  });
24
- return (_jsxs(OverlayFrame, { title: "Confirm action", hint: `y approve ${glyphs.midDot} n / Enter / Esc reject`, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: [_jsx(Text, { color: theme.assistant, children: state.summary }), _jsx(Text, { color: theme.muted, children: "Proceed? (y/N)" })] }));
43
+ return (_jsx(OverlayFrame, { title: "Confirm action", hint: "y approve | n/Enter/Esc reject | PgDn", maxRows: maxRows, cols: cols, theme: theme, caps: caps, scrollOffset: offset, onScrollClamp: setOffset, rows: wrapToRows(state.summary, Math.max(1, cols - 4)).map((line, index) => _jsx(Text, { wrap: "truncate", color: theme.assistant, children: line || ' ' }, index)) }));
25
44
  }