@aragon-agent/cli 0.6.12 → 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 (101) hide show
  1. package/README.md +81 -15
  2. package/dist/agent/controller.d.ts +29 -1
  3. package/dist/agent/controller.js +123 -22
  4. package/dist/agent/model-profile-settings.d.ts +31 -0
  5. package/dist/agent/model-profile-settings.js +199 -0
  6. package/dist/agent/queued-messages.d.ts +12 -0
  7. package/dist/agent/queued-messages.js +18 -0
  8. package/dist/agent/reducer.d.ts +13 -1
  9. package/dist/agent/reducer.js +51 -12
  10. package/dist/cli.js +15 -1
  11. package/dist/commands/builtins.js +66 -39
  12. package/dist/commands/queue-command.d.ts +3 -0
  13. package/dist/commands/queue-command.js +5 -0
  14. package/dist/commands/registry.d.ts +3 -0
  15. package/dist/compaction/child.d.ts +4 -2
  16. package/dist/compaction/child.js +4 -1
  17. package/dist/compaction/compactor.d.ts +7 -2
  18. package/dist/compaction/compactor.js +13 -11
  19. package/dist/compaction/context-command.js +5 -0
  20. package/dist/compaction/wiring.d.ts +4 -3
  21. package/dist/compaction/wiring.js +6 -1
  22. package/dist/config/cli-commands.d.ts +2 -0
  23. package/dist/config/cli-commands.js +16 -4
  24. package/dist/config/load.d.ts +2 -1
  25. package/dist/config/load.js +15 -8
  26. package/dist/config/model-profile-resolution.d.ts +20 -0
  27. package/dist/config/model-profile-resolution.js +81 -0
  28. package/dist/config/model-profile-store.d.ts +67 -0
  29. package/dist/config/model-profile-store.js +153 -0
  30. package/dist/config/model-profiles.d.ts +44 -0
  31. package/dist/config/model-profiles.js +121 -0
  32. package/dist/config/schema.d.ts +9 -0
  33. package/dist/config/store.d.ts +11 -1
  34. package/dist/config/store.js +43 -27
  35. package/dist/fast/resolve.d.ts +2 -1
  36. package/dist/fast/resolve.js +6 -4
  37. package/dist/fast/wiring.d.ts +5 -3
  38. package/dist/fast/wiring.js +8 -4
  39. package/dist/input/stdin-filter.js +3 -2
  40. package/dist/logging/install.js +4 -2
  41. package/dist/logging/secret-registry.d.ts +2 -0
  42. package/dist/logging/secret-registry.js +18 -0
  43. package/dist/team/runtime.d.ts +2 -1
  44. package/dist/team/subagent.d.ts +3 -1
  45. package/dist/team/subagent.js +3 -1
  46. package/dist/ui/ActivityLine.d.ts +6 -70
  47. package/dist/ui/ActivityLine.js +12 -34
  48. package/dist/ui/App.d.ts +1 -0
  49. package/dist/ui/App.js +113 -205
  50. package/dist/ui/BottomStatusRow.d.ts +6 -86
  51. package/dist/ui/BottomStatusRow.js +11 -26
  52. package/dist/ui/Composer.d.ts +16 -83
  53. package/dist/ui/Composer.js +9 -100
  54. package/dist/ui/PromptInput.d.ts +4 -1
  55. package/dist/ui/PromptInput.js +41 -34
  56. package/dist/ui/QueuePanel.d.ts +8 -0
  57. package/dist/ui/QueuePanel.js +9 -0
  58. package/dist/ui/QueueStatusRow.d.ts +2 -0
  59. package/dist/ui/QueueStatusRow.js +1 -1
  60. package/dist/ui/StatusBar.d.ts +13 -149
  61. package/dist/ui/StatusBar.js +27 -142
  62. package/dist/ui/clipboard-task.d.ts +29 -0
  63. package/dist/ui/clipboard-task.js +66 -0
  64. package/dist/ui/clipboard.d.ts +29 -50
  65. package/dist/ui/clipboard.js +141 -75
  66. package/dist/ui/composer-limits.d.ts +3 -4
  67. package/dist/ui/composer-limits.js +4 -9
  68. package/dist/ui/composer-rows.d.ts +1 -1
  69. package/dist/ui/composer-rows.js +24 -137
  70. package/dist/ui/editor-navigation.d.ts +45 -0
  71. package/dist/ui/editor-navigation.js +85 -0
  72. package/dist/ui/editor-reducer.d.ts +4 -0
  73. package/dist/ui/editor-reducer.js +29 -15
  74. package/dist/ui/interaction-copy.d.ts +77 -0
  75. package/dist/ui/interaction-copy.js +40 -0
  76. package/dist/ui/interaction-hints.d.ts +18 -0
  77. package/dist/ui/interaction-hints.js +92 -0
  78. package/dist/ui/layout/budget.js +1 -1
  79. package/dist/ui/layout/queue-layout.d.ts +19 -0
  80. package/dist/ui/layout/queue-layout.js +26 -0
  81. package/dist/ui/layout/status-layout.d.ts +55 -0
  82. package/dist/ui/layout/status-layout.js +109 -0
  83. package/dist/ui/layout/todo-layout.d.ts +1 -0
  84. package/dist/ui/layout/todo-layout.js +1 -1
  85. package/dist/ui/overlays/HelpOverlay.js +8 -8
  86. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  87. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  88. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  89. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  90. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  91. package/dist/ui/overlays/QueueOverlay.js +79 -0
  92. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  93. package/dist/ui/overlays/SettingsScreen.js +347 -3
  94. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  95. package/dist/ui/overlays/model-profile-state.js +119 -0
  96. package/dist/ui/run-status-row.d.ts +7 -19
  97. package/dist/ui/run-status-row.js +2 -18
  98. package/dist/ui/selection/selection-controller.d.ts +2 -2
  99. package/dist/ui/selection/selection-controller.js +1 -0
  100. package/dist/ui/use-wheel-routing.js +2 -1
  101. package/package.json +2 -2
@@ -1,27 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { ToastStack } from './ToastStack.js';
4
- export function BottomStatusRow({ toasts, activity, activityGlyph, update, theme, }) {
5
- // A toast wins the row outright — but a run in flight keeps its spinner on it.
6
- if (toasts.length > 0) {
7
- const spinner = activity ? activityGlyph : null;
8
- // Byte-identical to the old branch whenever there is no live spinner to
9
- // carry: no run, an ASCII terminal, or `reducedMotion`.
10
- if (!spinner)
11
- return _jsx(ToastStack, { toasts: toasts, theme: theme });
12
- return (
13
- // `flexDirection` SPELLED OUT even though `row` is Yoga's default: it is
14
- // what keeps two children on ONE row, and the header above turns on that
15
- // being true. Left implicit, it reads as an oversight next to the child
16
- // below that declares `column` — and "make them match" is a one-word edit
17
- // that doubles the row, takes it from the transcript, and desynchronises
18
- // every `viewportRows()` consumer without failing a type or a lint.
19
- _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { color: theme.thinking, children: [' ', spinner] }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(ToastStack, { toasts: toasts, theme: theme }) })] }));
20
- }
21
- if (activity)
22
- return _jsx(Box, { flexShrink: 0, children: activity });
23
- if (update)
24
- return _jsx(Box, { flexShrink: 0, children: update });
25
- // The blank full-screen row that IS the budget must keep being emitted.
26
- return _jsx(ToastStack, { toasts: toasts, theme: theme });
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import { buildActionClauses } from './interaction-hints.js';
4
+ export function BottomStatusRow(props) {
5
+ const { stdout } = useStdout();
6
+ const toast = props.toasts[props.toasts.length - 1];
7
+ const hints = props.hints ?? { cols: stdout?.columns ?? 80,
8
+ interactionPhase: props.activity ? 'running' : 'idle' };
9
+ const clauses = buildActionClauses({ ...hints, toast: hints.toast ?? toast?.text,
10
+ updateAvailable: hints.updateAvailable ?? Boolean(props.update) });
11
+ return _jsx(Box, { height: 1, width: hints.cols, flexShrink: 0, children: _jsxs(Text, { color: toast?.level === 'error' ? props.theme.noticeError : props.theme.muted, children: [' ', clauses.join(' | ')] }) });
27
12
  }
@@ -1,17 +1,4 @@
1
- /**
2
- * Composer (spec §4.7) — the thin shell around the existing `PromptInput`.
3
- *
4
- * Owns exactly two things: the semantic border color and the hint row. The
5
- * editor's pure helpers (`applyEdit` / `moveVertical` / `slashSuggestions` /
6
- * `fileTokenAt`) are untouched, so `input.test.ts` keeps passing.
7
- *
8
- * The draft flag is held HERE rather than in `App`: the border has to react to
9
- * "the input is no longer empty", and routing that through App would re-render
10
- * the transcript on the first keystroke of every message.
11
- *
12
- * The fixed status row retains emergency and history-navigation hints while
13
- * this footer scrolls outside the viewport. Editor state stays mounted.
14
- */
1
+ /** Input border and draft shell. Global action hints and activity live below the viewport. */
15
2
  import React from 'react';
16
3
  import { type DOMElement } from 'ink';
17
4
  import type { Theme } from './theme.js';
@@ -19,14 +6,9 @@ import type { TermCapabilities } from './capabilities.js';
19
6
  import { pickGlyphs } from './glyphs.js';
20
7
  import type { ComposerSubmitResult } from './composer-input.js';
21
8
  import { type CommandOption } from './PromptInput.js';
22
- import { type RunActivity } from './run-status-row.js';
9
+ import type { RunActivity } from './run-status-row.js';
23
10
  import { type AgentMode } from '../agent/agent-mode.js';
24
- /**
25
- * The run status row (tui-scrollbar-edge-and-run-row §3.3): the spinner, the phrase
26
- * and the `queue / interrupt / exit` clauses on ONE row above the input box.
27
- * `App` passes it only while a run is in flight AND the idle hint row would have
28
- * been shown, so enabling it never changes the footer's height.
29
- */
11
+ /** Legacy caller shape retained for source compatibility; Composer no longer mounts it. */
30
12
  export interface RunRowProps {
31
13
  activity: RunActivity;
32
14
  /** The row is in the viewport with no overlay: draw the animated spinner. */
@@ -35,9 +17,11 @@ export interface RunRowProps {
35
17
  rowRef?: React.RefObject<DOMElement>;
36
18
  }
37
19
  export interface ComposerProps {
38
- /** Present only while the run status row replaces the hint row below the input. */
20
+ /** Compatibility prop; activity is now rendered by the global StatusBar. */
39
21
  runRow?: RunRowProps | null;
40
22
  cols?: number;
23
+ deleteDisambiguated?: boolean;
24
+ onCompletionContextChange?: (context: 'none' | 'slash' | 'file') => void;
41
25
  cursorVisible?: boolean;
42
26
  onInteraction?: () => void;
43
27
  measureRef?: React.RefObject<DOMElement>;
@@ -52,48 +36,20 @@ export interface ComposerProps {
52
36
  cwd: string;
53
37
  /** Dropped on short terminals to buy back a row (§5.4). */
54
38
  showHint: boolean;
55
- /** Lifetime submissions; drives progressive disclosure of the idle hint. */
39
+ /** Compatibility prop; submission count no longer hides essential actions. */
56
40
  submitCount: number;
57
- /** User opt-out (`--no-hints` / `hints: false`). */
41
+ /** Compatibility prop; App owns secondary teaching-hint visibility. */
58
42
  hintsEnabled: boolean;
59
- /** Mirror of the controller's effective mode; drives the chip and the border. */
43
+ /** Mirror of the controller's effective mode; drives the border. */
60
44
  agentMode: AgentMode;
61
- /**
62
- * Which key this hint should NAME for the mode toggle
63
- * (shift-tab-mode-toggle-still-dead-on-windows, C3-1).
64
- *
65
- * A PROP RATHER THAN A CONSTANT because on some consoles the constant is a
66
- * lie. This row used to render `shift+tab` unconditionally, with no link to
67
- * the capability check three files away, so the machines where that key is
68
- * swallowed were the ones being told to press it - permanently, on every
69
- * frame, while the one warning they got scrolled out of the transcript
70
- * minutes earlier (analysis section 3.7). Defaults to the primary key, which
71
- * is what every unaffected machine renders.
72
- */
45
+ /** Compatibility prop; the global action row names the effective mode key. */
73
46
  modeToggleKey?: string;
74
47
  popupMaxRows?: number;
75
48
  popupMaxHeight?: number;
76
49
  onPopupRowsChange?: (rows: number) => void;
77
- /**
78
- * Rows of newer output below the viewport, forwarded to the input row's chip
79
- * (tui-selection-and-scroll-follow §4.2 / G7).
80
- *
81
- * IT IS STATE, NOT A FADEABLE HINT, and it therefore does NOT pass through the
82
- * `showHint` / `hintsEnabled` gate below. Those two govern the teaching row —
83
- * which retires keys the user has demonstrably learned and is dropped outright
84
- * on a short terminal — and "you are 12 rows behind the newest output" is
85
- * neither learnable nor optional. Same argument that keeps `esc abort`
86
- * un-faded in `hintText`.
87
- */
50
+ /** Rows of newer output below the viewport, forwarded to the input's scroll chip. */
88
51
  scrolledLines?: number;
89
- /**
90
- * Live background services (background-service-supervision §3.8), 0 when none.
91
- *
92
- * IT IS STATE, NOT A FADEABLE HINT, exactly like `scrolledLines` above: the
93
- * clause it adds is the only visible way to stop something, and `hintText`
94
- * below keeps it out of the faded form for the same reason `esc abort` is kept
95
- * there.
96
- */
52
+ /** Compatibility prop; the global status and action rows show live services. */
97
53
  services?: number;
98
54
  /**
99
55
  * The composer's DRAFT row count, forwarded upward (tui-paste-handling
@@ -113,27 +69,12 @@ export interface ComposerProps {
113
69
  onSubmit: (text: string) => ComposerSubmitResult;
114
70
  onHelp?: () => void;
115
71
  }
116
- /**
117
- * The running-state hint as separate clauses, most urgent first.
118
- *
119
- * `hintText` joins them (its output is pinned by AC-43), and the run status row
120
- * consumes the SAME array so it can drop whole clauses from the tail on a narrow
121
- * terminal instead of cutting `interrupt` in half. One source, two consumers.
122
- */
72
+ /** Legacy helper retained for consumers and contract tests; not rendered by Composer. */
123
73
  export declare function runningHintClauses(opts: {
124
74
  glyphs: ReturnType<typeof pickGlyphs>;
125
75
  services: number;
126
76
  }): string[];
127
- /**
128
- * Build the hint row.
129
- *
130
- * The `running` branch is NEVER abbreviated, no matter how experienced the user
131
- * is. Once the status bar's hint cluster was removed, the composer became the
132
- * only place that names the abort key — and fading that out after eight
133
- * submissions would leave a long-running task with no visible way to stop it.
134
- * Progressive disclosure is for teaching discoverable things, not for retiring
135
- * an emergency exit (§4.6 / A-14 / M-10b).
136
- */
77
+ /** Compatibility formatter. Product hints use interaction-hints.ts. */
137
78
  declare function hintText(opts: {
138
79
  running: boolean;
139
80
  submitCount: number;
@@ -143,15 +84,7 @@ declare function hintText(opts: {
143
84
  /** Live background services; 0 leaves both rows byte-identical to today's. */
144
85
  services: number;
145
86
  }): string;
146
- /**
147
- * `hintText`, for the interrupt-ladder tests (AC-43).
148
- *
149
- * EXPORTED UNDER A TEST-FACING NAME rather than exporting `hintText` itself, so
150
- * the private helper stays private in the module graph and nothing in `src/`
151
- * grows a second caller by accident. The row is the ONLY visible way to stop a
152
- * process the agent left running, so it is worth asserting on directly rather
153
- * than fishing the string out of a rendered frame.
154
- */
87
+ /** Compatibility export for existing hint contract tests. */
155
88
  export declare const hintTextForTest: typeof hintText;
156
- export declare function Composer({ runRow, cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
89
+ export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
157
90
  export {};
@@ -1,80 +1,26 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- /**
3
- * Composer (spec §4.7) — the thin shell around the existing `PromptInput`.
4
- *
5
- * Owns exactly two things: the semantic border color and the hint row. The
6
- * editor's pure helpers (`applyEdit` / `moveVertical` / `slashSuggestions` /
7
- * `fileTokenAt`) are untouched, so `input.test.ts` keeps passing.
8
- *
9
- * The draft flag is held HERE rather than in `App`: the border has to react to
10
- * "the input is no longer empty", and routing that through App would re-render
11
- * the transcript on the first keystroke of every message.
12
- *
13
- * The fixed status row retains emergency and history-navigation hints while
14
- * this footer scrolls outside the viewport. Editor state stays mounted.
15
- */
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** Input border and draft shell. Global action hints and activity live below the viewport. */
16
3
  import { useCallback, useState } from 'react';
17
- import { Box, Text } from 'ink';
18
- import stringWidth from 'string-width';
19
- import { pickGlyphs } from './glyphs.js';
4
+ import { Box } from 'ink';
20
5
  import { HINT_FADE_AFTER } from '../config/schema.js';
21
6
  import { PromptInput } from './PromptInput.js';
22
- import { ModeChip, modeChipCols } from './ModeChip.js';
23
- import { ActivityLabel, resolveActivityLabel } from './ActivityLine.js';
24
- import { RUN_ROW_LEAD, planRunRow } from './run-status-row.js';
25
- import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
26
- /**
27
- * The running-state hint as separate clauses, most urgent first.
28
- *
29
- * `hintText` joins them (its output is pinned by AC-43), and the run status row
30
- * consumes the SAME array so it can drop whole clauses from the tail on a narrow
31
- * terminal instead of cutting `interrupt` in half. One source, two consumers.
32
- */
7
+ import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
8
+ /** Legacy helper retained for consumers and contract tests; not rendered by Composer. */
33
9
  export function runningHintClauses(opts) {
34
10
  const { glyphs, services } = opts;
35
- // `queue`, NOT `steer` (tui-shift-enter-copy-queue 5.5): the gesture's
36
- // consequence now has a name in the transcript (`Queue: ...`), and one
37
- // behaviour with two names is undiscoverable.
38
11
  const clauses = [`${glyphs.enterKey} queue`, `esc${glyphs.times}2 interrupt`];
39
- // Confirmation is named even when service controls also need space.
40
12
  if (services > 0)
41
13
  clauses.push(`ctrl+c stop ${services}`);
42
14
  clauses.push(`ctrl+c${glyphs.times}2 exit`);
43
15
  return clauses;
44
16
  }
45
- /**
46
- * Build the hint row.
47
- *
48
- * The `running` branch is NEVER abbreviated, no matter how experienced the user
49
- * is. Once the status bar's hint cluster was removed, the composer became the
50
- * only place that names the abort key — and fading that out after eight
51
- * submissions would leave a long-running task with no visible way to stop it.
52
- * Progressive disclosure is for teaching discoverable things, not for retiring
53
- * an emergency exit (§4.6 / A-14 / M-10b).
54
- */
17
+ /** Compatibility formatter. Product hints use interaction-hints.ts. */
55
18
  function hintText(opts) {
56
19
  const { running, submitCount, glyphs, mode, toggleKey, services } = opts;
57
20
  const dot = ` ${glyphs.midDot} `;
58
- // THE EXIT CLAUSE SURVIVES IN BOTH ROWS (P1-4). The obvious edit here is to
59
- // replace `ctrl+c*2 exit` with `ctrl+c stop N` — and that would delete the
60
- // only visible way to quit at exactly the moment Ctrl+C stops meaning "quit",
61
- // on the screen a user reaches when something has already gone wrong. Both
62
- // clauses are shown; the row is `wrap="truncate"` and they are ordered
63
- // most-urgent-first, so a terminal too narrow for all of them drops the exit
64
- // clause last rather than the abort key.
65
21
  if (running)
66
22
  return runningHintClauses({ glyphs, services }).join(dot);
67
- // The hint names the DESTINATION, not the current state — the correct label
68
- // for a toggle affordance, and the reason the mode word here is the OPPOSITE
69
- // of the one on the chip beside it.
70
23
  const toggle = `${toggleKey} ${MODE_LABEL[nextMode(mode)].toLowerCase()}`;
71
- // The faded form KEEPS the mode toggle. Progressive disclosure exists to
72
- // retire things the user has demonstrably learned, and `submitCount` counts
73
- // submissions — a user can send fifty messages without ever discovering
74
- // Shift+Tab. Same argument that keeps `esc abort` un-faded above.
75
- // NEVER FADED, for the reason the props doc gives: it is the only visible way
76
- // to stop a process the agent left running, and progressive disclosure retires
77
- // things the user has demonstrably learned — not emergency exits.
78
24
  const stop = services > 0 ? [`ctrl+c stop ${services}`] : [];
79
25
  if (submitCount >= HINT_FADE_AFTER)
80
26
  return [...stop, toggle, '? help'].join(dot);
@@ -88,31 +34,9 @@ function hintText(opts) {
88
34
  '? help',
89
35
  ].join(dot);
90
36
  }
91
- /**
92
- * `hintText`, for the interrupt-ladder tests (AC-43).
93
- *
94
- * EXPORTED UNDER A TEST-FACING NAME rather than exporting `hintText` itself, so
95
- * the private helper stays private in the module graph and nothing in `src/`
96
- * grows a second caller by accident. The row is the ONLY visible way to stop a
97
- * process the agent left running, so it is worth asserting on directly rather
98
- * than fishing the string out of a rendered frame.
99
- */
37
+ /** Compatibility export for existing hint contract tests. */
100
38
  export const hintTextForTest = hintText;
101
- function RunRow({ runRow, cols, services, agentMode, theme, caps, }) {
102
- const glyphs = pickGlyphs(caps);
103
- const separator = ` ${glyphs.midDot} `;
104
- const clauses = runningHintClauses({ glyphs, services });
105
- const plan = planRunRow({
106
- cols,
107
- // Spinner glyph + the space after it, then the label.
108
- labelCols: 2 + stringWidth(resolveActivityLabel(runRow.activity, glyphs)),
109
- hintClauseCols: clauses.map((clause) => stringWidth(clause)),
110
- separatorCols: stringWidth(separator),
111
- chipCols: modeChipCols(agentMode),
112
- });
113
- return (_jsxs(Box, { ref: runRow.rowRef, flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { width: RUN_ROW_LEAD, flexShrink: 0, children: _jsx(Text, { children: ' ' }) }), _jsx(Box, { width: plan.labelCols, flexShrink: 0, children: _jsx(ActivityLabel, { ...runRow.activity, spinnerLive: runRow.live, theme: theme, caps: caps }) }), plan.hintClauses > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [separator, clauses.slice(0, plan.hintClauses).join(separator)] })), plan.chip && (_jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps })] }))] }));
114
- }
115
- export function Composer({ runRow, cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
39
+ export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
116
40
  const [hasDraft, setHasDraft] = useState(false);
117
41
  const onDraftChange = useCallback((next) => {
118
42
  setHasDraft(next.hasDraft);
@@ -130,20 +54,5 @@ export function Composer({ runRow, cols, cursorVisible, onInteraction, measureRe
130
54
  : hasDraft
131
55
  ? theme.focusBorder ?? theme.primary
132
56
  : theme.idleBorder ?? theme.border;
133
- const glyphs = pickGlyphs(caps);
134
- // While the run row is up it REPLACES the hint row below the input (same row
135
- // count, so starting a run never moves the layout).
136
- const runRowActive = running && !!runRow;
137
- const visible = showHint && hintsEnabled && !runRowActive;
138
- return (_jsxs(Box, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: [runRowActive && (_jsx(RunRow, { runRow: runRow, cols: cols ?? process.stdout.columns ?? 80, services: services, agentMode: agentMode, theme: theme, caps: caps })), _jsx(PromptInput, { cols: cols, cursorVisible: cursorVisible, onInteraction: onInteraction, isActive: isActive, reducedMotion: reducedMotion, onEscape: onEscape, onEscapeDismiss: onEscapeDismiss, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, borderColor: borderColor, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
139
- // The chip rides the EXISTING hint row rather than adding one, so it
140
- // costs zero rows on a 24-row terminal.
141
- _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText({
142
- running,
143
- submitCount,
144
- glyphs,
145
- mode: agentMode,
146
- toggleKey: modeToggleKey,
147
- services,
148
- }) })] }))] }));
57
+ return (_jsx(Box, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, cols: cols, cursorVisible: cursorVisible, onInteraction: onInteraction, isActive: isActive, reducedMotion: reducedMotion, onEscape: onEscape, onEscapeDismiss: onEscapeDismiss, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, borderColor: borderColor, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }) }));
149
58
  }
@@ -52,6 +52,9 @@ interface PromptInputProps {
52
52
  /** Total menu rows, including borders and the overflow hint. */
53
53
  popupMaxHeight?: number;
54
54
  onPopupRowsChange?: (rows: number) => void;
55
+ onCompletionContextChange?: (context: 'none' | 'slash' | 'file') => void;
56
+ /** True only when the stdin filter normalized DEL before Ink parsed it. */
57
+ deleteDisambiguated?: boolean;
55
58
  onSubmit: (text: string) => ComposerSubmitResult;
56
59
  onHelp?: () => void;
57
60
  onEscape?: () => void;
@@ -154,5 +157,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
154
157
  end: number;
155
158
  query: string;
156
159
  } | null;
157
- export declare function PromptInput({ cols: colsProp, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
160
+ export declare function PromptInput({ cols: colsProp, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
158
161
  export {};
@@ -34,6 +34,8 @@ import { planComposerInput } from './composer-input.js';
34
34
  import { allocatePasteId, expandPastes, expandRangeOverTokens } from './paste-tokens.js';
35
35
  import { draftMaxRows } from './composer-limits.js';
36
36
  import { layoutComposer, splitRowAtColumn } from './composer-rows.js';
37
+ import { moveVisualCursor, snapGrapheme, stepGrapheme } from './editor-navigation.js';
38
+ import { interactionCopy } from './interaction-copy.js';
37
39
  import { PASTE_DRAFT_MAX_BYTES, PASTE_MAX_BLOCKS, formatPasteSize, } from '../input/limits.js';
38
40
  // ---------------------------------------------------------------------------
39
41
  // The scroll chip (§4.2)
@@ -133,6 +135,8 @@ export function draftLimitRefusal(segments, editor) {
133
135
  // ---------------------------------------------------------------------------
134
136
  const isSpace = (ch) => !!ch && /\s/.test(ch);
135
137
  function lineStart(buffer, cursor) {
138
+ if (cursor <= 0)
139
+ return 0;
136
140
  const nl = buffer.lastIndexOf('\n', cursor - 1);
137
141
  return nl === -1 ? 0 : nl + 1;
138
142
  }
@@ -166,23 +170,23 @@ function nextWord(buffer, cursor) {
166
170
  * label the user then sends INSTEAD of the text it stands for.
167
171
  */
168
172
  function killRange(buffer, from, to) {
169
- const range = expandRangeOverTokens(buffer, from, to);
173
+ const range = expandRangeOverTokens(buffer, snapGrapheme(buffer, from), snapGrapheme(buffer, to, true));
170
174
  return { buffer: buffer.slice(0, range.from) + buffer.slice(range.to), cursor: range.from };
171
175
  }
172
176
  /** Apply an edit intent as a pure buffer/cursor transform. */
173
177
  export function applyEdit(buffer, cursor, intent) {
178
+ cursor = snapGrapheme(buffer, cursor);
174
179
  switch (intent) {
175
- // Cursor MOVEMENT is deliberately not token-aware (D-8): arrows, Home/End
176
- // and word jumps step by character, because re-deriving `moveVertical` around
177
- // 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.
178
182
  case 'home':
179
183
  return { buffer, cursor: lineStart(buffer, cursor) };
180
184
  case 'end':
181
185
  return { buffer, cursor: lineEnd(buffer, cursor) };
182
186
  case 'wordLeft':
183
- return { buffer, cursor: prevWord(buffer, cursor) };
187
+ return { buffer, cursor: snapGrapheme(buffer, prevWord(buffer, cursor)) };
184
188
  case 'wordRight':
185
- return { buffer, cursor: nextWord(buffer, cursor) };
189
+ return { buffer, cursor: snapGrapheme(buffer, nextWord(buffer, cursor), true) };
186
190
  case 'deleteWordBack':
187
191
  return killRange(buffer, prevWord(buffer, cursor), cursor);
188
192
  case 'killToStart':
@@ -197,19 +201,8 @@ export function applyEdit(buffer, cursor, intent) {
197
201
  * prompt-history recall.
198
202
  */
199
203
  export function moveVertical(buffer, cursor, dir) {
200
- const lines = buffer.split('\n');
201
- if (lines.length < 2)
202
- return null;
203
- const before = buffer.slice(0, cursor);
204
- const idx = before.split('\n').length - 1;
205
- const col = before.length - (before.lastIndexOf('\n') + 1);
206
- const target = dir === 'up' ? idx - 1 : idx + 1;
207
- if (target < 0 || target >= lines.length)
208
- return null;
209
- let start = 0;
210
- for (let i = 0; i < target; i += 1)
211
- start += lines[i].length + 1;
212
- 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;
213
206
  }
214
207
  /**
215
208
  * Slash-command suggestions when the buffer is a bare `/word` (no space yet).
@@ -250,7 +243,7 @@ function isControlSeq(input) {
250
243
  // ---------------------------------------------------------------------------
251
244
  // Component
252
245
  // ---------------------------------------------------------------------------
253
- 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, }) {
254
247
  const { stdout } = useStdout();
255
248
  const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
256
249
  const { buffer, cursor, historyIndex, dismissed, sel } = editor;
@@ -280,6 +273,17 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
280
273
  onPopupRowsChange?.(popupLayout.rowCount);
281
274
  }, [onPopupRowsChange, popupLayout.rowCount]);
282
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'), []);
283
287
  // --- Debounced `@file` scan. ---------------------------------------------
284
288
  useEffect(() => {
285
289
  if (fileQuery === null || fileQuery.length < 1) {
@@ -347,6 +351,8 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
347
351
  const overflows = wide.hiddenAbove > 0 || wide.hiddenBelow > 0;
348
352
  const usableCols = overflows ? Math.max(8, baseCols - OVERFLOW_CELLS - 1) : baseCols;
349
353
  const layout = overflows ? measure(usableCols) : wide;
354
+ const preferredColumnWidth = useRef();
355
+ useLayoutEffect(() => { preferredColumnWidth.current = undefined; }, [usableCols]);
350
356
  const draftRowCount = Math.max(1, layout.rows.length);
351
357
  const caretResetKey = useMemo(() => ({}), [buffer, cursor, isActive, usableCols]);
352
358
  // --- Draft presence and height, reported only on a transition. -----------
@@ -446,9 +452,12 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
446
452
  };
447
453
  const verticalOrHistory = (dir) => {
448
454
  onInteraction?.();
449
- 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 });
450
458
  if (moved) {
451
- dispatch({ type: 'moveCursor', cursor: moved.cursor });
459
+ preferredColumnWidth.current = usableCols;
460
+ dispatch({ type: 'moveCursor', ...moved });
452
461
  return;
453
462
  }
454
463
  // At an edge: recall history only from an empty buffer or while already
@@ -514,11 +523,15 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
514
523
  return;
515
524
  }
516
525
  // Line-editing intents (Home/End/word/kill) recognized from raw keys.
517
- const intent = recognize(input, key);
526
+ const intent = key.meta && (key.backspace || key.delete)
527
+ ? 'deleteWordBack' : recognize(input, key);
518
528
  if (intent) {
519
529
  onInteraction?.();
520
530
  const next = applyEdit(buffer, cursor, intent);
521
- 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 });
522
535
  return;
523
536
  }
524
537
  if (key.ctrl)
@@ -539,19 +552,17 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
539
552
  // unconditional dispatch and a no-op at the left edge returns the same
540
553
  // state object — which React bails out on, exactly as the old
541
554
  // `if (cursor > 0)` skipped the `setState`s.
542
- dispatch({ type: 'backspace' });
555
+ dispatch({ type: key.delete && deleteDisambiguated ? 'delete' : 'backspace' });
543
556
  return;
544
557
  }
545
558
  if (key.leftArrow) {
546
559
  onInteraction?.();
547
- const step = cursor >= 2 && (buffer.codePointAt(cursor - 2) ?? 0) > 0xffff ? 2 : 1;
548
- dispatch({ type: 'moveCursor', cursor: Math.max(0, cursor - step) });
560
+ dispatch({ type: 'moveCursor', cursor: stepGrapheme(buffer, cursor, 'left') });
549
561
  return;
550
562
  }
551
563
  if (key.rightArrow) {
552
564
  onInteraction?.();
553
- const step = (buffer.codePointAt(cursor) ?? 0) > 0xffff ? 2 : 1;
554
- dispatch({ type: 'moveCursor', cursor: Math.min(buffer.length, cursor + step) });
565
+ dispatch({ type: 'moveCursor', cursor: stepGrapheme(buffer, cursor, 'right') });
555
566
  return;
556
567
  }
557
568
  // `!key.shift` is load-bearing: Shift+↑/↓ is the viewport's line-scroll
@@ -576,11 +587,7 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
576
587
  }, { isActive });
577
588
  const marker = running ? glyphs.steer : glyphs.caret;
578
589
  const markerColor = running ? theme.toolRunning : theme.primary;
579
- const placeholder = running
580
- ? `Type to queue a message, Esc twice to interrupt${glyphs.ellipsis}`
581
- : agentMode === 'plan'
582
- ? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
583
- : `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
590
+ const placeholder = running ? interactionCopy.runningPlaceholder : interactionCopy.idlePlaceholder;
584
591
  const overflow = overflowChip(layout.hiddenAbove, layout.hiddenBelow, glyphs);
585
592
  const tokenColor = theme.hintFg ?? theme.muted;
586
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
+ });
@@ -12,6 +12,8 @@ export interface QueueStatusRowProps {
12
12
  }
13
13
  /** Return the first nonempty logical line without terminal control characters. */
14
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;
15
17
  /** Format a queue using only the columns assigned by the status bar. */
16
18
  export declare function formatQueueStatus(props: Omit<QueueStatusRowProps, 'theme'>): {
17
19
  prefix: string;
@@ -11,7 +11,7 @@ export function queueSummary(text) {
11
11
  .find(Boolean) ?? '(empty)';
12
12
  }
13
13
  /** Truncate at a whole grapheme, reserving the ellipsis in the same cell budget. */
14
- function truncateSummary(text, columns, ellipsis) {
14
+ export function truncateSummary(text, columns, ellipsis) {
15
15
  if (stringWidth(text) <= columns)
16
16
  return text;
17
17
  if (columns < stringWidth(ellipsis))