@aragon-agent/cli 0.6.11 → 0.6.13

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 (115) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +97 -20
  3. package/dist/agent/controller.d.ts +38 -3
  4. package/dist/agent/controller.js +151 -60
  5. package/dist/agent/model-profile-settings.d.ts +31 -0
  6. package/dist/agent/model-profile-settings.js +199 -0
  7. package/dist/agent/queued-messages.d.ts +20 -0
  8. package/dist/agent/queued-messages.js +46 -0
  9. package/dist/agent/reducer.d.ts +21 -2
  10. package/dist/agent/reducer.js +65 -37
  11. package/dist/cli.js +15 -1
  12. package/dist/commands/builtins.js +83 -42
  13. package/dist/commands/queue-command.d.ts +3 -0
  14. package/dist/commands/queue-command.js +5 -0
  15. package/dist/commands/registry.d.ts +3 -0
  16. package/dist/compaction/child.d.ts +4 -2
  17. package/dist/compaction/child.js +4 -1
  18. package/dist/compaction/compactor.d.ts +7 -2
  19. package/dist/compaction/compactor.js +13 -11
  20. package/dist/compaction/context-command.js +5 -0
  21. package/dist/compaction/wiring.d.ts +4 -3
  22. package/dist/compaction/wiring.js +6 -1
  23. package/dist/config/cli-commands.d.ts +2 -0
  24. package/dist/config/cli-commands.js +16 -4
  25. package/dist/config/load.d.ts +2 -1
  26. package/dist/config/load.js +15 -8
  27. package/dist/config/model-profile-resolution.d.ts +20 -0
  28. package/dist/config/model-profile-resolution.js +81 -0
  29. package/dist/config/model-profile-store.d.ts +67 -0
  30. package/dist/config/model-profile-store.js +153 -0
  31. package/dist/config/model-profiles.d.ts +44 -0
  32. package/dist/config/model-profiles.js +121 -0
  33. package/dist/config/schema.d.ts +9 -0
  34. package/dist/config/store.d.ts +11 -1
  35. package/dist/config/store.js +43 -27
  36. package/dist/fast/resolve.d.ts +2 -1
  37. package/dist/fast/resolve.js +6 -4
  38. package/dist/fast/wiring.d.ts +5 -3
  39. package/dist/fast/wiring.js +8 -4
  40. package/dist/input/enter-sequences.d.ts +11 -10
  41. package/dist/input/enter-sequences.js +29 -27
  42. package/dist/input/stdin-filter.js +93 -28
  43. package/dist/logging/install.d.ts +3 -0
  44. package/dist/logging/install.js +4 -2
  45. package/dist/logging/secret-registry.d.ts +2 -0
  46. package/dist/logging/secret-registry.js +18 -0
  47. package/dist/session/persist.d.ts +6 -7
  48. package/dist/session/persist.js +6 -9
  49. package/dist/session/store.d.ts +4 -1
  50. package/dist/session/validate-session.d.ts +3 -0
  51. package/dist/session/validate-session.js +207 -0
  52. package/dist/team/runtime.d.ts +2 -1
  53. package/dist/team/subagent.d.ts +3 -1
  54. package/dist/team/subagent.js +3 -1
  55. package/dist/ui/ActivityLine.d.ts +6 -70
  56. package/dist/ui/ActivityLine.js +12 -34
  57. package/dist/ui/App.d.ts +1 -0
  58. package/dist/ui/App.js +161 -226
  59. package/dist/ui/BottomStatusRow.d.ts +6 -86
  60. package/dist/ui/BottomStatusRow.js +11 -26
  61. package/dist/ui/Composer.d.ts +18 -84
  62. package/dist/ui/Composer.js +9 -100
  63. package/dist/ui/PromptInput.d.ts +6 -2
  64. package/dist/ui/PromptInput.js +91 -105
  65. package/dist/ui/QueuePanel.d.ts +8 -0
  66. package/dist/ui/QueuePanel.js +9 -0
  67. package/dist/ui/QueueStatusRow.d.ts +24 -0
  68. package/dist/ui/QueueStatusRow.js +51 -0
  69. package/dist/ui/StatusBar.d.ts +15 -149
  70. package/dist/ui/StatusBar.js +27 -112
  71. package/dist/ui/clipboard-task.d.ts +29 -0
  72. package/dist/ui/clipboard-task.js +66 -0
  73. package/dist/ui/clipboard.d.ts +29 -50
  74. package/dist/ui/clipboard.js +141 -75
  75. package/dist/ui/composer-input.d.ts +28 -0
  76. package/dist/ui/composer-input.js +46 -0
  77. package/dist/ui/composer-limits.d.ts +3 -4
  78. package/dist/ui/composer-limits.js +4 -9
  79. package/dist/ui/composer-rows.d.ts +1 -1
  80. package/dist/ui/composer-rows.js +24 -137
  81. package/dist/ui/editor-navigation.d.ts +45 -0
  82. package/dist/ui/editor-navigation.js +85 -0
  83. package/dist/ui/editor-reducer.d.ts +9 -0
  84. package/dist/ui/editor-reducer.js +31 -15
  85. package/dist/ui/enter-frames.d.ts +15 -31
  86. package/dist/ui/enter-frames.js +14 -33
  87. package/dist/ui/interaction-copy.d.ts +77 -0
  88. package/dist/ui/interaction-copy.js +40 -0
  89. package/dist/ui/interaction-hints.d.ts +18 -0
  90. package/dist/ui/interaction-hints.js +92 -0
  91. package/dist/ui/layout/budget.js +1 -1
  92. package/dist/ui/layout/queue-layout.d.ts +19 -0
  93. package/dist/ui/layout/queue-layout.js +26 -0
  94. package/dist/ui/layout/status-layout.d.ts +55 -0
  95. package/dist/ui/layout/status-layout.js +109 -0
  96. package/dist/ui/layout/todo-layout.d.ts +1 -0
  97. package/dist/ui/layout/todo-layout.js +1 -1
  98. package/dist/ui/overlays/HelpOverlay.js +11 -3
  99. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  100. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  101. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  102. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  103. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  104. package/dist/ui/overlays/QueueOverlay.js +79 -0
  105. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  106. package/dist/ui/overlays/SettingsScreen.js +347 -3
  107. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  108. package/dist/ui/overlays/model-profile-state.js +119 -0
  109. package/dist/ui/run-status-row.d.ts +7 -19
  110. package/dist/ui/run-status-row.js +2 -18
  111. package/dist/ui/selection/selection-controller.d.ts +2 -2
  112. package/dist/ui/selection/selection-controller.js +58 -20
  113. package/dist/ui/transcript-text.js +20 -12
  114. package/dist/ui/use-wheel-routing.js +2 -1
  115. package/package.json +2 -2
@@ -28,11 +28,14 @@ import { recognize } from '../input/keymap.js';
28
28
  import { AutocompletePopup } from './AutocompletePopup.js';
29
29
  import { buildAutocompleteLayout } from './layout/autocomplete.js';
30
30
  import { editorReducer, INITIAL_EDITOR_STATE, } from './editor-reducer.js';
31
- import { hasPasteFrame, splitPasteFrames } from './paste-frames.js';
32
- import { hasEnterFrame, mergeWithPasteRuns, splitEnterFrames, } from './enter-frames.js';
31
+ import { hasPasteFrame } from './paste-frames.js';
32
+ import { hasEnterFrame, splitEnterFrames, } from './enter-frames.js';
33
+ import { planComposerInput } from './composer-input.js';
33
34
  import { allocatePasteId, expandPastes, expandRangeOverTokens } from './paste-tokens.js';
34
35
  import { draftMaxRows } from './composer-limits.js';
35
36
  import { layoutComposer, splitRowAtColumn } from './composer-rows.js';
37
+ import { moveVisualCursor, snapGrapheme, stepGrapheme } from './editor-navigation.js';
38
+ import { interactionCopy } from './interaction-copy.js';
36
39
  import { PASTE_DRAFT_MAX_BYTES, PASTE_MAX_BLOCKS, formatPasteSize, } from '../input/limits.js';
37
40
  // ---------------------------------------------------------------------------
38
41
  // The scroll chip (§4.2)
@@ -132,6 +135,8 @@ export function draftLimitRefusal(segments, editor) {
132
135
  // ---------------------------------------------------------------------------
133
136
  const isSpace = (ch) => !!ch && /\s/.test(ch);
134
137
  function lineStart(buffer, cursor) {
138
+ if (cursor <= 0)
139
+ return 0;
135
140
  const nl = buffer.lastIndexOf('\n', cursor - 1);
136
141
  return nl === -1 ? 0 : nl + 1;
137
142
  }
@@ -165,23 +170,23 @@ function nextWord(buffer, cursor) {
165
170
  * label the user then sends INSTEAD of the text it stands for.
166
171
  */
167
172
  function killRange(buffer, from, to) {
168
- const range = expandRangeOverTokens(buffer, from, to);
173
+ const range = expandRangeOverTokens(buffer, snapGrapheme(buffer, from), snapGrapheme(buffer, to, true));
169
174
  return { buffer: buffer.slice(0, range.from) + buffer.slice(range.to), cursor: range.from };
170
175
  }
171
176
  /** Apply an edit intent as a pure buffer/cursor transform. */
172
177
  export function applyEdit(buffer, cursor, intent) {
178
+ cursor = snapGrapheme(buffer, cursor);
173
179
  switch (intent) {
174
- // Cursor MOVEMENT is deliberately not token-aware (D-8): arrows, Home/End
175
- // and word jumps step by character, because re-deriving `moveVertical` around
176
- // a synthetic unit buys a correctness the user cannot notice.
180
+ // Navigation may enter a token label; insertion and range deletion retain
181
+ // the existing token snapping and expansion contracts.
177
182
  case 'home':
178
183
  return { buffer, cursor: lineStart(buffer, cursor) };
179
184
  case 'end':
180
185
  return { buffer, cursor: lineEnd(buffer, cursor) };
181
186
  case 'wordLeft':
182
- return { buffer, cursor: prevWord(buffer, cursor) };
187
+ return { buffer, cursor: snapGrapheme(buffer, prevWord(buffer, cursor)) };
183
188
  case 'wordRight':
184
- return { buffer, cursor: nextWord(buffer, cursor) };
189
+ return { buffer, cursor: snapGrapheme(buffer, nextWord(buffer, cursor), true) };
185
190
  case 'deleteWordBack':
186
191
  return killRange(buffer, prevWord(buffer, cursor), cursor);
187
192
  case 'killToStart':
@@ -196,19 +201,8 @@ export function applyEdit(buffer, cursor, intent) {
196
201
  * prompt-history recall.
197
202
  */
198
203
  export function moveVertical(buffer, cursor, dir) {
199
- const lines = buffer.split('\n');
200
- if (lines.length < 2)
201
- return null;
202
- const before = buffer.slice(0, cursor);
203
- const idx = before.split('\n').length - 1;
204
- const col = before.length - (before.lastIndexOf('\n') + 1);
205
- const target = dir === 'up' ? idx - 1 : idx + 1;
206
- if (target < 0 || target >= lines.length)
207
- return null;
208
- let start = 0;
209
- for (let i = 0; i < target; i += 1)
210
- start += lines[i].length + 1;
211
- return { cursor: start + Math.min(col, lines[target].length) };
204
+ const moved = moveVisualCursor({ buffer, cursor, direction: dir, cols: Number.MAX_SAFE_INTEGER });
205
+ return moved ? { cursor: moved.cursor } : null;
212
206
  }
213
207
  /**
214
208
  * Slash-command suggestions when the buffer is a bare `/word` (no space yet).
@@ -249,7 +243,7 @@ function isControlSeq(input) {
249
243
  // ---------------------------------------------------------------------------
250
244
  // Component
251
245
  // ---------------------------------------------------------------------------
252
- export function PromptInput({ cols: colsProp, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, onNotice, scrolledLines = 0, }) {
246
+ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated = false, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, onNotice, scrolledLines = 0, }) {
253
247
  const { stdout } = useStdout();
254
248
  const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
255
249
  const { buffer, cursor, historyIndex, dismissed, sel } = editor;
@@ -279,6 +273,17 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
279
273
  onPopupRowsChange?.(popupLayout.rowCount);
280
274
  }, [onPopupRowsChange, popupLayout.rowCount]);
281
275
  useLayoutEffect(() => () => onPopupRowsChange?.(0), [onPopupRowsChange]);
276
+ const completion = popupVisible ? popupKind ?? 'none' : 'none';
277
+ const completionCallback = useRef(onCompletionContextChange);
278
+ completionCallback.current = onCompletionContextChange;
279
+ const lastCompletion = useRef();
280
+ useLayoutEffect(() => {
281
+ if (!onCompletionContextChange || lastCompletion.current === completion)
282
+ return;
283
+ lastCompletion.current = completion;
284
+ onCompletionContextChange?.(completion);
285
+ }, [onCompletionContextChange, completion]);
286
+ useLayoutEffect(() => () => completionCallback.current?.('none'), []);
282
287
  // --- Debounced `@file` scan. ---------------------------------------------
283
288
  useEffect(() => {
284
289
  if (fileQuery === null || fileQuery.length < 1) {
@@ -346,6 +351,8 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
346
351
  const overflows = wide.hiddenAbove > 0 || wide.hiddenBelow > 0;
347
352
  const usableCols = overflows ? Math.max(8, baseCols - OVERFLOW_CELLS - 1) : baseCols;
348
353
  const layout = overflows ? measure(usableCols) : wide;
354
+ const preferredColumnWidth = useRef();
355
+ useLayoutEffect(() => { preferredColumnWidth.current = undefined; }, [usableCols]);
349
356
  const draftRowCount = Math.max(1, layout.rows.length);
350
357
  const caretResetKey = useMemo(() => ({}), [buffer, cursor, isActive, usableCols]);
351
358
  // --- Draft presence and height, reported only on a transition. -----------
@@ -371,18 +378,42 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
371
378
  onInteraction?.();
372
379
  dispatch({ type: 'insert', text });
373
380
  };
374
- /**
375
- * Expand every token back to its payload, then send (D-7 / D-9).
376
- *
377
- * `onSubmit`'s signature is unchanged, so `Composer`, `App.handleSubmit`,
378
- * `runSlashInput` and `submitMessage` are all untouched — and a slash command
379
- * therefore sees the EXPANDED text, which is what `/skill foo <pasted body>`
380
- * needs. `clear` releases every payload in the same dispatch.
381
- */
382
- const submit = (text) => {
381
+ const resolveSubmit = (state) => {
382
+ const suggestions = state.dismissed ? null : slashSuggestions(state.buffer, commands);
383
+ const selectedIndex = Math.min(state.sel, (suggestions?.length ?? 0) - 1);
384
+ const suggestionLayout = buildAutocompleteLayout({
385
+ itemCount: isActive ? suggestions?.length ?? 0 : 0,
386
+ selected: selectedIndex, maxRows: popupMaxRows, maxHeight: popupMaxHeight,
387
+ });
388
+ const selected = suggestionLayout.rowCount > 0 ? suggestions?.[selectedIndex] : undefined;
389
+ const text = selected?.label ?? expandPastes(state.buffer, state.pastes);
390
+ return text.trim().length > 0 ? text : null;
391
+ };
392
+ const transact = (intents) => {
393
+ const plan = planComposerInput({
394
+ editor, intents, resolveSubmit, checkPasteLimit: draftLimitRefusal,
395
+ });
396
+ if (plan.refusal) {
397
+ onNotice?.('warn', plan.refusal);
398
+ return;
399
+ }
383
400
  onInteraction?.();
384
- onSubmit(expandPastes(text, editor.pastes));
385
- dispatch({ type: 'clear' });
401
+ let result = { accepted: true };
402
+ if (plan.submission !== undefined) {
403
+ try {
404
+ result = onSubmit(plan.submission);
405
+ }
406
+ catch (error) {
407
+ result = { accepted: false, reason: error instanceof Error ? error.message : String(error) };
408
+ }
409
+ }
410
+ dispatch({ type: 'adopt', state: result.accepted ? plan.nextEditor : plan.rejectedEditor });
411
+ if (!result.accepted) {
412
+ onNotice?.('error', result.reason ?? 'Message was not accepted; input is kept in the draft.');
413
+ }
414
+ else if (plan.notice) {
415
+ onNotice?.('warn', plan.notice);
416
+ }
386
417
  };
387
418
  const recallUp = () => {
388
419
  if (history.length === 0)
@@ -421,9 +452,12 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
421
452
  };
422
453
  const verticalOrHistory = (dir) => {
423
454
  onInteraction?.();
424
- const moved = moveVertical(buffer, cursor, dir);
455
+ const moved = moveVisualCursor({ buffer, cursor, cols: usableCols, direction: dir,
456
+ preferredVisualColumn: preferredColumnWidth.current === usableCols
457
+ ? editor.preferredVisualColumn : undefined });
425
458
  if (moved) {
426
- dispatch({ type: 'moveCursor', cursor: moved.cursor });
459
+ preferredColumnWidth.current = usableCols;
460
+ dispatch({ type: 'moveCursor', ...moved });
427
461
  return;
428
462
  }
429
463
  // At an edge: recall history only from an empty buffer or while already
@@ -447,6 +481,16 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
447
481
  return;
448
482
  if (key.pageUp || key.pageDown || (key.shift && (key.upArrow || key.downArrow)))
449
483
  return;
484
+ if ((key.return || input === '\r') && (key.meta || key.shift)) {
485
+ insert('\n');
486
+ return;
487
+ }
488
+ if (hasPasteFrame(input) || hasEnterFrame(input) || /[\r\n]/.test(input)) {
489
+ const intents = splitEnterFrames(input).map((frame) => frame.kind === 'paste' ? { ...frame, id: allocatePasteId() } : frame);
490
+ if (intents.length > 0)
491
+ transact(intents);
492
+ return;
493
+ }
450
494
  // Popup navigation owns Up/Down/Tab/→/Enter/Esc while it is open.
451
495
  if (popupVisible) {
452
496
  if (key.upArrow) {
@@ -464,9 +508,7 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
464
508
  return;
465
509
  }
466
510
  if (key.return && popupKind === 'slash') {
467
- const label = popupItems[clampedSel]?.label;
468
- if (label)
469
- submit(label);
511
+ transact([{ kind: 'submit' }]);
470
512
  return;
471
513
  }
472
514
  if (key.escape) {
@@ -481,11 +523,15 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
481
523
  return;
482
524
  }
483
525
  // Line-editing intents (Home/End/word/kill) recognized from raw keys.
484
- const intent = recognize(input, key);
526
+ const intent = key.meta && (key.backspace || key.delete)
527
+ ? 'deleteWordBack' : recognize(input, key);
485
528
  if (intent) {
486
529
  onInteraction?.();
487
530
  const next = applyEdit(buffer, cursor, intent);
488
- dispatch({ type: 'replace', buffer: next.buffer, cursor: next.cursor });
531
+ const moving = intent === 'home' || intent === 'end'
532
+ || intent === 'wordLeft' || intent === 'wordRight';
533
+ dispatch(moving ? { type: 'moveCursor', cursor: next.cursor }
534
+ : { type: 'replace', buffer: next.buffer, cursor: next.cursor });
489
535
  return;
490
536
  }
491
537
  if (key.ctrl)
@@ -497,7 +543,7 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
497
543
  }
498
544
  if (buffer.trim().length === 0)
499
545
  return;
500
- submit(buffer);
546
+ transact([{ kind: 'submit' }]);
501
547
  return;
502
548
  }
503
549
  if (key.backspace || key.delete) {
@@ -506,19 +552,17 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
506
552
  // unconditional dispatch and a no-op at the left edge returns the same
507
553
  // state object — which React bails out on, exactly as the old
508
554
  // `if (cursor > 0)` skipped the `setState`s.
509
- dispatch({ type: 'backspace' });
555
+ dispatch({ type: key.delete && deleteDisambiguated ? 'delete' : 'backspace' });
510
556
  return;
511
557
  }
512
558
  if (key.leftArrow) {
513
559
  onInteraction?.();
514
- const step = cursor >= 2 && (buffer.codePointAt(cursor - 2) ?? 0) > 0xffff ? 2 : 1;
515
- dispatch({ type: 'moveCursor', cursor: Math.max(0, cursor - step) });
560
+ dispatch({ type: 'moveCursor', cursor: stepGrapheme(buffer, cursor, 'left') });
516
561
  return;
517
562
  }
518
563
  if (key.rightArrow) {
519
564
  onInteraction?.();
520
- const step = (buffer.codePointAt(cursor) ?? 0) > 0xffff ? 2 : 1;
521
- dispatch({ type: 'moveCursor', cursor: Math.min(buffer.length, cursor + step) });
565
+ dispatch({ type: 'moveCursor', cursor: stepGrapheme(buffer, cursor, 'right') });
522
566
  return;
523
567
  }
524
568
  // `!key.shift` is load-bearing: Shift+↑/↓ is the viewport's line-scroll
@@ -537,71 +581,13 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
537
581
  onHelp();
538
582
  return;
539
583
  }
540
- // A PASTE (section 5.4). It sits AFTER every key branch so a paste can
541
- // never be shadowed by a key test, and BEFORE `isControlSeq` because a
542
- // framed string starts with NUL and would otherwise be dropped.
543
- //
544
- // The text runs inside `segments` are already sanitised (I-14): Ink drains
545
- // its whole buffer in one `read()`, so this same string can carry the `\r`
546
- // of a user who pasted and pressed Enter inside the burst window.
547
- //
548
- // NO NEWLINE INTENT IN THE CHUNK (tui-shift-enter-copy-queue 3.5):
549
- // that same one-`read()` coalescing means a paste frame can share an
550
- // `input` with an enter frame or a bare Ctrl+J, and those chunks
551
- // belong to the newline branch below, whose `mergeWithPasteRuns`
552
- // interleaves both families in byte order. This branch's
553
- // `splitPasteFrames` would strip the enter frame's NUL and leak its
554
- // letter as text. A multi-line payload (a `\n` of its own) routes
555
- // there too and comes back byte-identical inside its paste segment.
556
- if (hasPasteFrame(input) && !hasEnterFrame(input) && !input.includes('\n')) {
557
- const runs = splitPasteFrames(input);
558
- const segments = runs.map((run) => run.kind === 'paste' ? { kind: 'paste', text: run.text, id: allocatePasteId() } : run);
559
- if (segments.length === 0)
560
- return;
561
- const refusal = draftLimitRefusal(segments, editor);
562
- if (refusal) {
563
- // A refusal dispatches NOTHING, so the draft is byte-for-byte
564
- // unchanged (AC-9) — and it is never silent (D-11).
565
- onNotice?.('warn', refusal);
566
- return;
567
- }
568
- onInteraction?.();
569
- dispatch({ type: 'input', segments });
570
- return;
571
- }
572
- // A NEWLINE INTENT, from either source (tui-shift-enter-copy-queue
573
- // 3.5): the NUL frame the filter wrote for Shift+Enter / Alt+Enter /
574
- // CSI-u Enter, or a bare `\n` (Ctrl+J), which Ink delivers WITHOUT
575
- // `key.return`. Sits beside the paste branch -- after every key test so
576
- // it can never shadow one, before `isControlSeq` because both sources
577
- // begin with a control byte and would otherwise be dropped. ONE
578
- // dispatch carries the whole ordered chunk, paste runs included when
579
- // Ink coalesced the filter's two writes into one input string.
580
- if (hasEnterFrame(input) || input.includes('\n')) {
581
- const runs = mergeWithPasteRuns(input, splitEnterFrames(input));
582
- const segments = runs.map((run) => run.kind === 'paste' ? { kind: 'paste', text: run.text, id: allocatePasteId() } : run);
583
- if (segments.length === 0)
584
- return;
585
- const refusal = draftLimitRefusal(segments, editor);
586
- if (refusal) {
587
- onNotice?.('warn', refusal);
588
- return;
589
- }
590
- onInteraction?.();
591
- dispatch({ type: 'input', segments });
592
- return;
593
- }
594
584
  // Printable input — drop any unrecognized control/escape byte.
595
585
  if (input && !key.tab && !isControlSeq(input))
596
586
  insert(input);
597
587
  }, { isActive });
598
588
  const marker = running ? glyphs.steer : glyphs.caret;
599
589
  const markerColor = running ? theme.toolRunning : theme.primary;
600
- const placeholder = running
601
- ? `Type to queue a message, Esc twice to interrupt${glyphs.ellipsis}`
602
- : agentMode === 'plan'
603
- ? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
604
- : `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
590
+ const placeholder = running ? interactionCopy.runningPlaceholder : interactionCopy.idlePlaceholder;
605
591
  const overflow = overflowChip(layout.hiddenAbove, layout.hiddenBelow, glyphs);
606
592
  const tokenColor = theme.hintFg ?? theme.muted;
607
593
  const renderSegments = (segments, keyPrefix) => segments.map((segment, i) => (_jsx(Text, { color: segment.kind === 'token' ? tokenColor : undefined, children: segment.text }, `${keyPrefix}${i}`)));
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { QueueLayout } from './layout/queue-layout.js';
3
+ import type { Theme } from './theme.js';
4
+ /** Persistent receipt-backed footer, with no timer or separate queue state. */
5
+ export declare const QueuePanel: React.NamedExoticComponent<{
6
+ layout: QueueLayout;
7
+ theme: Theme;
8
+ }>;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ /** Persistent receipt-backed footer, with no timer or separate queue state. */
5
+ export const QueuePanel = React.memo(function QueuePanel({ layout, theme }) {
6
+ if (!layout.rows)
7
+ return null;
8
+ return _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: layout.rows, children: [_jsx(Text, { color: theme.accent, wrap: "truncate", children: layout.title }), layout.rows > 1 && layout.items.map((item) => _jsx(Text, { color: theme.muted, wrap: "truncate", children: item.label }, item.queueId))] });
9
+ });
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import type { PendingSteering } from '../agent/queued-messages.js';
3
+ import type { TermCapabilities } from './capabilities.js';
4
+ import type { Theme } from './theme.js';
5
+ export interface QueueStatusRowProps {
6
+ pending: readonly PendingSteering[];
7
+ paused: boolean;
8
+ columns: number;
9
+ compact: boolean;
10
+ theme: Theme;
11
+ caps: TermCapabilities;
12
+ }
13
+ /** Return the first nonempty logical line without terminal control characters. */
14
+ export declare function queueSummary(text: string): string;
15
+ /** Truncate at a whole grapheme, reserving the ellipsis in the same cell budget. */
16
+ export declare function truncateSummary(text: string, columns: number, ellipsis: string): string;
17
+ /** Format a queue using only the columns assigned by the status bar. */
18
+ export declare function formatQueueStatus(props: Omit<QueueStatusRowProps, 'theme'>): {
19
+ prefix: string;
20
+ body: string;
21
+ suffix: string;
22
+ };
23
+ /** Fixed single-line pending receipt feedback; no timeout or spinner. */
24
+ export declare function QueueStatusRow(props: QueueStatusRowProps): React.ReactElement;
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import stringWidth from 'string-width';
4
+ import stripAnsi from 'strip-ansi';
5
+ import { pickGlyphs } from './glyphs.js';
6
+ const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
7
+ /** Return the first nonempty logical line without terminal control characters. */
8
+ export function queueSummary(text) {
9
+ return stripAnsi(text).split(/\r\n|[\r\n]/)
10
+ .map((line) => line.replace(/\t/g, ' ').replace(/[\x00-\x1f\x7f-\x9f]/g, '').trim())
11
+ .find(Boolean) ?? '(empty)';
12
+ }
13
+ /** Truncate at a whole grapheme, reserving the ellipsis in the same cell budget. */
14
+ export function truncateSummary(text, columns, ellipsis) {
15
+ if (stringWidth(text) <= columns)
16
+ return text;
17
+ if (columns < stringWidth(ellipsis))
18
+ return '';
19
+ let out = '';
20
+ let used = stringWidth(ellipsis);
21
+ for (const { segment } of segmenter.segment(text)) {
22
+ const width = stringWidth(segment);
23
+ if (used + width > columns)
24
+ break;
25
+ used += width;
26
+ out += segment;
27
+ }
28
+ return out + ellipsis;
29
+ }
30
+ /** Format a queue using only the columns assigned by the status bar. */
31
+ export function formatQueueStatus(props) {
32
+ if (!props.pending.length)
33
+ return { prefix: '', body: '', suffix: '' };
34
+ const prefix = props.paused
35
+ ? (props.compact ? 'Queue(p): ' : 'Queue (paused): ') : 'Queue: ';
36
+ const remaining = props.pending.length - 1;
37
+ const suffix = remaining === 0 ? '' : props.compact
38
+ ? `(+${remaining > 99 ? '99+' : remaining})` : `(+${remaining} more)`;
39
+ const tail = suffix ? ` ${suffix}` : '';
40
+ const budget = Math.max(0, props.columns - stringWidth(prefix + tail));
41
+ return {
42
+ prefix,
43
+ body: truncateSummary(queueSummary(props.pending[0].text), budget, pickGlyphs(props.caps).ellipsis),
44
+ suffix: tail,
45
+ };
46
+ }
47
+ /** Fixed single-line pending receipt feedback; no timeout or spinner. */
48
+ export function QueueStatusRow(props) {
49
+ const parts = formatQueueStatus(props);
50
+ return _jsxs(Text, { children: [_jsx(Text, { color: props.theme.accent, children: parts.prefix }), _jsxs(Text, { color: props.theme.muted, children: [parts.body, parts.suffix] })] });
51
+ }
@@ -1,40 +1,27 @@
1
- /**
2
- * Status bar (spec §4.8). Width-aware, borderless, EXACTLY one row: a left
3
- * cluster (run status, model, thinking level) and a right cluster (context
4
- * gauge, occupancy pair, session total, cost, tokens/sec + elapsed while
5
- * running, off-bottom `↑N`). Clusters drop out gracefully on narrow terminals.
6
- *
7
- * THE RIGHT CLUSTER'S WIDTH LADDER (context-usage-gauge-accuracy §3.6):
8
- *
9
- * >= 96 [####....] 43% 86.0k/200.0k total 1.2M^ 48.0k v $3.21 12 tok/s 1m02s
10
- * 72-95 [####....] 43% 86.0k/200.0k $3.21
11
- * 60-71 [####....] 43% $3.21
12
- * < 60 43% $3.21
13
- *
14
- * Dropping the round border here buys back 3 rows of a 24-row terminal — an
15
- * eighth of the screen was being spent framing a single line of text.
16
- */
1
+ /** A single row of complete, measured status fields. */
17
2
  import React from 'react';
18
3
  import type { Theme } from './theme.js';
19
4
  import type { TermCapabilities } from './capabilities.js';
20
5
  import type { UsageTotal } from '../agent/reducer.js';
21
6
  import type { ContextUsageSnapshot } from '../compaction/types.js';
22
- import { type AgentMode } from '../agent/agent-mode.js';
23
- interface StatusBarProps {
7
+ import type { AgentMode } from '../agent/agent-mode.js';
8
+ import type { PendingSteering } from '../agent/queued-messages.js';
9
+ import { type RunPhase } from './layout/status-layout.js';
10
+ export interface StatusBarProps {
11
+ runPhase?: RunPhase;
12
+ activeTool?: {
13
+ name: string;
14
+ toolCallId: string;
15
+ } | null;
16
+ runOutcome?: "none" | "ended" | "interrupted" | "failed";
17
+ reducedMotion?: boolean;
18
+ waitingForConfirmation?: boolean;
19
+ stopping?: boolean;
20
+ pendingSteering?: readonly PendingSteering[];
24
21
  interruptHint?: string;
25
22
  model: string;
26
23
  provider: string;
27
24
  usageTotal: UsageTotal;
28
- /**
29
- * Context occupancy, as one object (context-usage-gauge-accuracy §4.3).
30
- *
31
- * IT REPLACED FOUR PARALLEL PROPS - `contextTokens`, `contextWindow`,
32
- * `contextWindowKnown`, `contextEstimated`. Four props describing one reading
33
- * are four chances for a caller to update three of them, which is the props-
34
- * layer form of the "many writers" defect this feature removed from the
35
- * reducer. `usageTotal` stays separate because it genuinely IS another
36
- * quantity: session spend, not occupancy.
37
- */
38
25
  context: ContextUsageSnapshot;
39
26
  status: 'idle' | 'running';
40
27
  elapsedMs: number;
@@ -42,157 +29,36 @@ interface StatusBarProps {
42
29
  tokPerSec: number;
43
30
  theme: Theme;
44
31
  caps: TermCapabilities;
45
- /**
46
- * Lines scrolled off the bottom; > 0 renders `↑N`. Inline mode has no
47
- * self-drawn viewport, so it simply never sends anything but 0 — the bar
48
- * needs no render-mode branch of its own.
49
- */
50
32
  scrolledLines?: number;
51
- /**
52
- * Ctrl+L redraw carrier — invariant I-5 (spec §4.13). See `redrawChar`.
53
- */
54
33
  redrawNonce?: number;
55
34
  agentMode?: AgentMode;
56
- /** Non-null while a deferred `plan -> build` waits for `agent_end`. */
57
35
  pendingAgentMode?: AgentMode | null;
58
- /**
59
- * Live subagent counts (team-subagents §6.2), or absent when no dispatch is
60
- * running — an ordinary session's bar is then unchanged.
61
- *
62
- * LEFT CLUSTER, and it degrades (D-20 / P2-1). The left cluster is
63
- * `flexShrink={0}`, so every column it takes comes out of the context gauge
64
- * and the cost readout opposite it; on an 80-column terminal the full
65
- * `agents 3/5` visibly squeezes them, and the gauge is how a user notices they
66
- * are about to run out of context. Below `TEAM_LIMITS.statusCompactCols` this
67
- * therefore renders as `[3]` — the running count alone.
68
- *
69
- * This does NOT contradict the "no `cols >= N` breakpoint" rule above: that
70
- * rule is written about `agentMode`, whose reason is that the mode must never
71
- * be unreportable. The team counter has a second, fuller home in `TeamPanel`
72
- * and a third in `TeamCard`; it is the one readout on this bar that can afford
73
- * to degrade.
74
- */
75
36
  teamActive?: {
76
37
  running: number;
77
38
  total: number;
78
39
  };
79
- /**
80
- * Live todo counts (todo-plan-execution §6.3), or absent.
81
- *
82
- * `App` sends this ONLY when a list exists AND the rail does not: with the
83
- * rail on screen the numbers are already there in a larger typeface, and
84
- * duplicating them costs the context gauge columns it cannot spare. This is
85
- * therefore the INLINE-MODE and NARROW-TERMINAL fallback for R-d, which is
86
- * exactly why it is here rather than being folded into the rail's own gate.
87
- *
88
- * Degrades below `TODO_LIMITS.statusCompactCols` from `todo 2/7` to `[2/7]`,
89
- * the same treatment and the same justification as the team counter's
90
- * `agents 3/5` -> `[3]`. See the note on `agentMode` for why that rule does
91
- * NOT apply to the mode word.
92
- */
93
40
  todoActive?: {
94
41
  done: number;
95
42
  total: number;
96
43
  };
97
- /**
98
- * Live background services (background-service-supervision §3.8), or absent
99
- * when none are running — an ordinary session's bar is then unchanged.
100
- *
101
- * LEFT CLUSTER, after the todo chip, and it DEGRADES rather than hiding below
102
- * `PROC_LIMITS.statusCompactCols`. That is the `todoActive` / `teamActive`
103
- * ladder rather than `compactionActive`'s hide-below-threshold one, and the
104
- * choice is deliberate: `compact` and `fast` describe something the CLI is
105
- * doing to itself, whereas this is a PROCESS ON THE USER'S MACHINE HOLDING A
106
- * PORT. A narrow terminal is a reason to shorten it, never a reason to stop
107
- * saying it.
108
- *
109
- * `live` counts `starting | ready | running`. The transcript records events
110
- * about a service (D-11); this is where "what is running right now" lives,
111
- * alongside `/bg` and `bash_output`.
112
- */
113
44
  servicesActive?: {
114
45
  live: number;
115
46
  };
116
- /**
117
- * Live retry state (llm-api-retry-backoff §6.6), or absent when nothing is
118
- * retrying — an ordinary session's bar is then unchanged.
119
- *
120
- * LEFT CLUSTER, after `teamActive`, and it DEGRADES. It is a run-state fact like
121
- * `agents 3/5`, and the left cluster is `flexShrink={0}` so every column it takes
122
- * comes out of the context gauge; below `RETRY_UI.statusCompactCols` it renders
123
- * as `[r3]`.
124
- *
125
- * This does NOT contradict the "no `cols >= N` breakpoint" rule on `agentMode`
126
- * above: that rule exists because the MODE must never be unreportable. The retry
127
- * has a second, fuller home in the transcript card, which is exactly why it is
128
- * the kind of readout that can afford to shrink.
129
- */
130
47
  retryActive?: {
131
48
  attempt: number;
132
49
  max: number;
133
50
  secondsLeft: number;
134
51
  };
135
- /**
136
- * Render-governor rung (tui-render-performance L4). `> 0` renders a muted
137
- * `eco` chip.
138
- *
139
- * I-L4-2 — THE GOVERNOR IS USER-VISIBLE, and this is where. A user who cannot
140
- * see why the stream got chunkier will conclude the model got slower, and
141
- * degrading silently is the one thing a claim of robustness cannot afford.
142
- * It sits in the RIGHT cluster next to the other derived numbers and is
143
- * suppressed on a narrow terminal, which is the `teamActive` treatment rather
144
- * than the `agentMode` one: the mode must never be unreportable, whereas an
145
- * eco chip on a 50-column terminal costs the context gauge more than it says.
146
- */
147
52
  ecoRung?: number;
148
- /**
149
- * The live fast tier (fast-model-tier §6), or absent when it is off for this
150
- * session — in which case an ordinary session's bar is byte-identical.
151
- *
152
- * RIGHT CLUSTER, next to `eco`, and it DEGRADES below
153
- * `FAST_LIMITS.statusCompactCols`. This is the `teamActive` / `ecoRung`
154
- * treatment rather than the `agentMode` one, and the distinction is the note
155
- * on `agentMode` above: the mode must never be unreportable, whereas the fast
156
- * tier has a fuller home in `/fast status` — which is the GUARANTEED reporting
157
- * surface — and a chip on a 60-column terminal costs the context gauge more
158
- * than it says.
159
- *
160
- * `inFlight` renders `fast*`, so a review that is taking a while is visible
161
- * without a spinner competing with the run's own.
162
- */
163
53
  fastActive?: {
164
54
  inFlight: boolean;
165
55
  };
166
- /**
167
- * Live context compaction (context-auto-compaction §6.2), or absent when it is
168
- * off for this session — in which case an ordinary session's bar is unchanged.
169
- *
170
- * RIGHT CLUSTER, next to `fast` and `eco`, and it DEGRADES below
171
- * `COMPACTION_LIMITS.statusCompactCols`. The `teamActive` / `ecoRung` /
172
- * `fastActive` treatment rather than the `agentMode` one, for the reason the
173
- * note on `agentMode` gives: the mode must never be unreportable, whereas
174
- * compaction has a fuller home in `/compact status` — the GUARANTEED reporting
175
- * surface (§6.5) — and a chip on a 60-column terminal costs the context gauge
176
- * more than it says.
177
- *
178
- * `inFlight` renders `compacting`, so a summarization that is taking a while is
179
- * visible on this row without a spinner competing with the activity line's.
180
- */
181
56
  compactionActive?: {
182
57
  inFlight: boolean;
183
58
  };
184
- /**
185
- * Where the gauge turns amber and red (§6.2).
186
- *
187
- * ABSENT means today's hardcoded `{ warn: 60, high: 85 }`, so a session with
188
- * compaction off renders a byte-identical bar. When present these are
189
- * `warnThreshold * 100` and `threshold * 100`, which is what makes the colour
190
- * the user sees agree with the number the trigger fires on.
191
- */
192
59
  gaugeMarks?: {
193
60
  warn: number;
194
61
  high: number;
195
62
  };
196
63
  }
197
64
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
198
- export {};