@aragon-agent/cli 0.6.20 → 0.6.22

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 (38) hide show
  1. package/README.md +32 -12
  2. package/dist/agent/controller.js +13 -0
  3. package/dist/commands/builtins.js +2 -1
  4. package/dist/config/cli-commands.d.ts +1 -1
  5. package/dist/config/cli-commands.js +3 -0
  6. package/dist/config/schema.d.ts +28 -12
  7. package/dist/config/schema.js +4 -0
  8. package/dist/team/limits.d.ts +73 -13
  9. package/dist/team/limits.js +82 -15
  10. package/dist/team/normalize.js +7 -2
  11. package/dist/team/overseer.d.ts +154 -16
  12. package/dist/team/overseer.js +342 -46
  13. package/dist/team/report.d.ts +9 -0
  14. package/dist/team/report.js +32 -4
  15. package/dist/team/runtime.d.ts +51 -9
  16. package/dist/team/runtime.js +478 -71
  17. package/dist/team/task-tool.d.ts +7 -0
  18. package/dist/team/task-tool.js +2 -0
  19. package/dist/team/types.d.ts +43 -2
  20. package/dist/ui/App.js +80 -2
  21. package/dist/ui/Composer.d.ts +14 -1
  22. package/dist/ui/Composer.js +6 -3
  23. package/dist/ui/PromptInput.d.ts +10 -1
  24. package/dist/ui/PromptInput.js +11 -1
  25. package/dist/ui/TeamPanel.d.ts +22 -0
  26. package/dist/ui/TeamPanel.js +71 -2
  27. package/dist/ui/editor-reducer.js +10 -5
  28. package/dist/ui/interaction-copy.d.ts +1 -0
  29. package/dist/ui/interaction-copy.js +1 -0
  30. package/dist/ui/interaction-hints.d.ts +2 -0
  31. package/dist/ui/interaction-hints.js +1 -1
  32. package/dist/ui/layout/status-layout.js +1 -0
  33. package/dist/ui/layout/team-panel.js +4 -1
  34. package/dist/ui/overlays/HelpOverlay.js +4 -1
  35. package/dist/ui/overlays/ModelProfilePicker.js +12 -1
  36. package/dist/ui/status-feedback.d.ts +3 -1
  37. package/dist/ui/status-feedback.js +3 -1
  38. package/package.json +2 -2
@@ -39,6 +39,13 @@ export interface TaskToolDeps {
39
39
  activeProvider: () => string;
40
40
  /** The lead's cost table, so the report header states real spend (R-5). */
41
41
  modelCost: () => ModelCost | undefined;
42
+ /**
43
+ * The FAST tier's cost table, so the report's supervisor spend line states
44
+ * real spend (subagent-overseer-v2 D-7 / R-P1-2). OPTIONAL and returning
45
+ * `undefined`: an unpriced fast model renders `pricing unknown`, never a
46
+ * fabricated `$0.00` - the C-11 / RV-4 rule.
47
+ */
48
+ fastModelCost?: () => ModelCost | undefined;
42
49
  /**
43
50
  * Run the dispatch with the LEAD's idle watchdog paused, `try/finally` inside.
44
51
  *
@@ -123,8 +123,10 @@ export function createTaskTool(deps) {
123
123
  // shape, and D-14 one level up). Every child ran the lead's own model,
124
124
  // so the lead's cost table is the one honest price for the total.
125
125
  const cost = deps.modelCost();
126
+ const fastCost = deps.fastModelCost?.();
126
127
  return textResult(buildDispatchReport(outcome, {
127
128
  ...(cost ? { cost } : {}),
129
+ ...(fastCost ? { fastCost } : {}),
128
130
  }));
129
131
  },
130
132
  };
@@ -159,6 +159,22 @@ export interface SubagentRun {
159
159
  * report reads "this row is the Nth body of the same brief".
160
160
  */
161
161
  replacements?: number;
162
+ /**
163
+ * The most recent supervisor decision applied to this child
164
+ * (subagent-overseer-v2 section 6). TRANSIENT, never persisted - the
165
+ * same rule as `activity` / `activityArgs`; a resumed session has no
166
+ * live dispatch.
167
+ *
168
+ * `at` is ABSOLUTE epoch ms, the same units as `startedAt` / `endedAt`
169
+ * (R-P2-3), so the panel renders `nudged 2m` with one subtraction and
170
+ * no second clock convention. `reasonHead` <= 60 chars, for the UI
171
+ * badge line only.
172
+ */
173
+ lastIntervention?: {
174
+ action: OverseerAction;
175
+ at: number;
176
+ reasonHead: string;
177
+ };
162
178
  }
163
179
  /** What the supervisor may do with one inspected child. */
164
180
  export type OverseerAction = 'wait' | 'nudge' | 'replace' | 'abandon';
@@ -184,8 +200,14 @@ export interface OverseerIntervention {
184
200
  at: number;
185
201
  action: OverseerAction;
186
202
  reason: string;
187
- /** What the trigger was: event stall, wall-clock check, or post-mortem. */
188
- trigger: 'silence' | 'clock' | 'postmortem';
203
+ /**
204
+ * What the trigger was: event stall, wall-clock check, post-mortem, or
205
+ * the supervisor going QUIET (subagent-overseer-v2 D-10) - the look
206
+ * budget ran out and supervision stopped waking this child. A VALUE OF
207
+ * THE UNION, never a reason prefix: a human-readable sentence is not a
208
+ * machine contract (the `run.error` rule).
209
+ */
210
+ trigger: 'silence' | 'clock' | 'postmortem' | 'quiet';
189
211
  }
190
212
  /**
191
213
  * A terminal supervisor decision handed to the WORKER LOOP, which is the only
@@ -231,6 +253,25 @@ export interface DispatchOutcome {
231
253
  * total priced at the lead's table is the honest accounting.
232
254
  */
233
255
  usage: TokenUsage;
256
+ /**
257
+ * The supervisor's own fast-tier spend for this dispatch
258
+ * (subagent-overseer-v2 D-7). Absent when nothing supervised the
259
+ * dispatch; read straight off the provider at dispatch end so there is
260
+ * exactly one accounting of it.
261
+ */
262
+ overseerUsage?: TokenUsage;
263
+ /** Inspection calls made, including ones that failed softly. */
264
+ overseerCalls?: number;
265
+ /**
266
+ * EDGE-LATCHED, once per dispatch (D-10): the fast tier was missing at
267
+ * least one cadence tick and the children waited UNASSISTED - announced,
268
+ * never killed. The per-child wait events of a degraded tick
269
+ * deliberately do NOT enter `interventions`; this flag plus the tick
270
+ * count below are the report's aggregate note.
271
+ */
272
+ overseerDegraded?: boolean;
273
+ /** How many cadence ticks ran unassisted (the aggregate note's number). */
274
+ overseerDegradedTicks?: number;
234
275
  }
235
276
  /**
236
277
  * What the UI renders: a cheap immutable projection, rebuilt per event.
package/dist/ui/App.js CHANGED
@@ -128,6 +128,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
128
128
  const { stdout } = useStdout();
129
129
  const size = useTerminalSize();
130
130
  const [draftRows, setDraftRows] = useState(1);
131
+ // Ctrl+C draft ladder (ctrl-c-clear-draft-exit). The nonce ticks when
132
+ // the armed ladder spends its clear rung; the flag marks "clear spent"
133
+ // so the render-time feedback already reads the exit wording at the
134
+ // nonce render (the async clear lands a frame later - see the timing
135
+ // invariant beside the ladder itself).
136
+ const [draftClearNonce, setDraftClearNonce] = useState(0);
137
+ const [draftClearedByCtrlC, setDraftClearedByCtrlC] = useState(false);
131
138
  const [popupRows, setPopupRows] = useState(0);
132
139
  const [statusExpanded, setStatusExpanded] = useState(false);
133
140
  const dimensions = buildFrameBudget({ ...size, draftRows, popupRows, statusExpanded });
@@ -657,6 +664,20 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
657
664
  const snap = controller.getTeamSnapshot();
658
665
  if (snap)
659
666
  dispatch({ type: 'teamUpdate', snapshot: snap });
667
+ // G-4 / AC-5 (subagent-overseer-v2): a TERMINAL verdict is the
668
+ // one decision the user must not discover from the final report
669
+ // - it removes work they watched running. wait and nudge stay
670
+ // panel-only so the transcript gains no per-tick noise.
671
+ if (event.decision.action === 'replace' || event.decision.action === 'abandon') {
672
+ const verb = event.decision.action === 'replace' ? 'replaced' : 'abandoned';
673
+ const level = event.decision.action === 'replace' ? 'info' : 'warn';
674
+ const reason = event.decision.reason.slice(0, 120);
675
+ dispatch({
676
+ type: 'notice',
677
+ level,
678
+ text: `team supervisor ${verb} ${event.label} - ${reason}`,
679
+ });
680
+ }
660
681
  break;
661
682
  }
662
683
  case 'usage': {
@@ -1145,6 +1166,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1145
1166
  const onDraftRows = useCallback((next) => {
1146
1167
  setDraftRows(previous => previous === next ? previous : next);
1147
1168
  }, []);
1169
+ /**
1170
+ * Draft presence, written to a REF on purpose (ctrl-c-clear-draft-exit):
1171
+ * promoting it to state would re-render the transcript on the first
1172
+ * keystroke of every message - the exact cost Composer.tsx documents
1173
+ * for keeping `hasDraft` local. The ladder reads the ref inside its key
1174
+ * handler, where freshness is guaranteed; the render-time feedback
1175
+ * uses `hasDraftRef.current && !draftClearedByCtrlC` instead.
1176
+ */
1177
+ const hasDraftRef = useRef(false);
1178
+ const onDraftPresence = useCallback((hasDraft) => {
1179
+ hasDraftRef.current = hasDraft;
1180
+ // A draft that re-appears after a ladder clear re-arms the clear rung.
1181
+ if (hasDraft)
1182
+ setDraftClearedByCtrlC(false);
1183
+ }, []);
1148
1184
  // --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
1149
1185
  //
1150
1186
  // TWO ARRIVAL ORDERS, AND BOTH HAPPEN. The service is built by a dynamic
@@ -1743,7 +1779,43 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1743
1779
  toast('warn', `Stopping ${live} service${live === 1 ? '' : 's'}.`);
1744
1780
  return;
1745
1781
  }
1782
+ //
1783
+ // RENDER-TIMING INVARIANT (part of this fix, not an implementation
1784
+ // detail): the status feedback is evaluated during render, but
1785
+ // `hasDraftRef` is a ref that does not itself trigger one. Every
1786
+ // rung still lands exactly one render - arm bumps `metricsClock`,
1787
+ // the clear rung bumps `draftClearNonce`/`draftClearedByCtrlC` - and
1788
+ // the async clear (nonce -> effect -> 'clear' -> the onDraftChange
1789
+ // transition) refills the ref within the same tick, far faster than
1790
+ // a human's next press. `draftClearedByCtrlC` exists because the
1791
+ // nonce render happens BEFORE that chain: without it the feedback
1792
+ // would still read hasDraft=true and repeat the clear prompt after
1793
+ // clearing. If a future rung ever becomes a pure ref write with no
1794
+ // state change, its feedback text will silently go stale.
1795
+ //
1796
+ // TWO ACCEPTED EDGES (facts drive the ladder, by design):
1797
+ // - arm (empty, "to exit") then PASTE a draft within 1.5s: the
1798
+ // second press clears instead of exiting - the cleared text is
1799
+ // still in the clipboard, so nothing is actually lost.
1800
+ // - arm (draft, "to clear") then Ctrl+U kills the draft by hand:
1801
+ // the second press falls to the exit branch - with nothing left
1802
+ // to clear, the next rung owns the press (tmux/htop behave the
1803
+ // same way).
1804
+ //
1805
+ // RUNG TWO/TREE, composed from two existing facts: armed x
1806
+ // hasDraft. No new counter, no new timer - clearing the draft
1807
+ // flips hasDraftRef to false, so the NEXT press naturally lands on
1808
+ // the exit branch: draft present -> x2 clears, x3 exits; empty ->
1809
+ // x2 exits, byte-identical to the previous behaviour.
1746
1810
  if (ctrlCArmed.current) {
1811
+ if (hasDraftRef.current) {
1812
+ setDraftClearNonce(value => value + 1);
1813
+ setDraftClearedByCtrlC(true);
1814
+ // Stays armed and the 1500ms clock keeps running: the clear
1815
+ // rung must not re-arm the whole ladder (that would make
1816
+ // exiting a draft x4 presses).
1817
+ return;
1818
+ }
1747
1819
  doExit();
1748
1820
  return;
1749
1821
  }
@@ -1755,6 +1827,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1755
1827
  ctrlCTimer.current = setTimeout(() => {
1756
1828
  ctrlCTimer.current = null;
1757
1829
  ctrlCArmed.current = false;
1830
+ setDraftClearedByCtrlC(false);
1758
1831
  setMetricsClock(value => value + 1);
1759
1832
  }, 1500);
1760
1833
  return;
@@ -2135,6 +2208,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2135
2208
  const feedbackProjection = projectStatusFeedback({
2136
2209
  interactionPhase: interactionPhase.current, interruptPhase: interruptGesture.current.phase,
2137
2210
  completion, overlay, copyState, copyResult, ctrlCArmed: ctrlCArmed.current,
2211
+ // The flag, not the raw ref: the nonce render precedes the async
2212
+ // clear, so the ref alone would repeat the clear wording (see the
2213
+ // render-timing invariant beside the ladder).
2214
+ hasDraft: hasDraftRef.current && !draftClearedByCtrlC,
2138
2215
  selectionPending: selectionController?.hasPendingSelection() ?? false, liveServices,
2139
2216
  toast: latestToast ? { text: latestToast.text,
2140
2217
  level: latestToast.level === 'success' ? 'info' : latestToast.level } : undefined,
@@ -2159,10 +2236,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2159
2236
  hints: { interactionPhase: interactionPhase.current, cols, completion, overlay,
2160
2237
  selectionPending: selectionController?.hasPendingSelection() ?? false,
2161
2238
  copyInFlight: copyState.busy, copyCleanupPending: copyState.cleanupPending,
2162
- services: liveServices, hintsEnabled: cfg.hints, modeToggleKey,
2239
+ hasDraft: hasDraftRef.current, services: liveServices,
2240
+ hintsEnabled: cfg.hints, modeToggleKey,
2163
2241
  interruptPhase: interruptGesture.current.phase,
2164
2242
  updateAvailable: !!updateSnapshot && shouldRenderUpdateLine(updateSnapshot) } });
2165
- const composer = (_jsx(Composer, { deleteDisambiguated: terminal?.deleteDisambiguated ?? false, onCompletionContextChange: setCompletion, cols: frameBudget.composerCols, terminalRows: rows, statusExpanded: statusExpanded, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: false, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: frameBudget.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
2243
+ const composer = (_jsx(Composer, { deleteDisambiguated: terminal?.deleteDisambiguated ?? false, onCompletionContextChange: setCompletion, cols: frameBudget.composerCols, terminalRows: rows, statusExpanded: statusExpanded, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: false, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: frameBudget.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onDraftRows: onDraftRows, onDraftPresence: onDraftPresence, draftClearNonce: draftClearNonce, onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
2166
2244
  const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2167
2245
  const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2168
2246
  const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: overlayNode, footer: _jsxs(_Fragment, { children: [team, _jsx(QueuePanel, { layout: queueLayout, theme: theme })] }), rail: rail, scrollbar: terminal?.scrollbar, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, onGeometryChange: onGeometryChange, geometryKey: `${rows}:${cols}:${viewportBudget}:${frameBudget.composerSlotRows}:${frameBudget.statusRows}:${contentCols}:${overlay}:${tooSmall}`, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: transcriptEntries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }));
@@ -64,6 +64,19 @@ export interface ComposerProps {
64
64
  * character, which is what keeps the re-render rate the same (R-6).
65
65
  */
66
66
  onDraftRows?: (rows: number) => void;
67
+ /**
68
+ * Draft PRESENCE, forwarded upward for the Ctrl+C ladder
69
+ * (ctrl-c-clear-draft-exit). App stores it in a ref only - no state,
70
+ * so the transcript does not re-render on the first keystroke of a
71
+ * message (the same cost the comment above documents).
72
+ */
73
+ onDraftPresence?: (hasDraft: boolean) => void;
74
+ /**
75
+ * Downward half of the same channel: ticks when App asks the editor
76
+ * to clear the draft (Ctrl+C x2 while drafting). Passed through to
77
+ * PromptInput unchanged.
78
+ */
79
+ draftClearNonce?: number;
67
80
  /** Paste refusals from `PromptInput`, routed to `App`'s notice dispatch. */
68
81
  onNotice?: (level: 'warn' | 'error', text: string) => void;
69
82
  theme: Theme;
@@ -88,5 +101,5 @@ declare function hintText(opts: {
88
101
  }): string;
89
102
  /** Compatibility export for existing hint contract tests. */
90
103
  export declare const hintTextForTest: typeof hintText;
91
- export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
104
+ export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onDraftPresence, draftClearNonce, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
92
105
  export {};
@@ -36,12 +36,15 @@ function hintText(opts) {
36
36
  }
37
37
  /** Compatibility export for existing hint contract tests. */
38
38
  export const hintTextForTest = hintText;
39
- export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
39
+ export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onDraftPresence, draftClearNonce, onNotice, theme, caps, onSubmit, onHelp, }) {
40
40
  const [hasDraft, setHasDraft] = useState(false);
41
41
  const onDraftChange = useCallback((next) => {
42
42
  setHasDraft(next.hasDraft);
43
+ // Presence FIRST: the App ref must be current before the row-count
44
+ // report below can trigger the App render that reads it.
45
+ onDraftPresence?.(next.hasDraft);
43
46
  onDraftRows?.(next.rows);
44
- }, [onDraftRows]);
47
+ }, [onDraftPresence, onDraftRows]);
45
48
  // Plan mode inserts ONE branch, above `draft` and below `running`. The
46
49
  // ordering is load-bearing: an in-flight run's border must stay `toolRunning`,
47
50
  // because "is it running" is more urgent than "which mode".
@@ -54,5 +57,5 @@ export function Composer({ deleteDisambiguated, onCompletionContextChange, cols,
54
57
  : hasDraft
55
58
  ? theme.focusBorder ?? theme.primary
56
59
  : theme.idleBorder ?? theme.border;
57
- return (_jsx(Box, { ref: measureRef, width: cols, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, cols: cols, terminalRows: terminalRows, statusExpanded: statusExpanded, 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 }) }));
60
+ return (_jsx(Box, { ref: measureRef, width: cols, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, cols: cols, terminalRows: terminalRows, statusExpanded: statusExpanded, 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, draftClearNonce: draftClearNonce, onNotice: onNotice }) }));
58
61
  }
@@ -93,6 +93,15 @@ interface PromptInputProps {
93
93
  * component is the only place that holds both facts.
94
94
  */
95
95
  onNotice?: (level: 'warn' | 'error', text: string) => void;
96
+ /**
97
+ * App-owned draft clearing (ctrl-c-clear-draft-exit): the second
98
+ * Ctrl+C of the armed ladder asks the editor to drop the draft. A
99
+ * NUMBER, not a boolean, so repeated requests stay distinguishable
100
+ * and the effect below never re-fires for an unchanged value.
101
+ * `'clear'` returns the editor to INITIAL_EDITOR_STATE, releasing
102
+ * paste payloads with it.
103
+ */
104
+ draftClearNonce?: number;
96
105
  scrolledLines?: number;
97
106
  }
98
107
  export interface ScrollChip {
@@ -159,5 +168,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
159
168
  end: number;
160
169
  query: string;
161
170
  } | null;
162
- export declare function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, onNotice, }: PromptInputProps): React.ReactElement;
171
+ export declare function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, draftClearNonce, onNotice, }: PromptInputProps): React.ReactElement;
163
172
  export {};
@@ -244,7 +244,7 @@ function isControlSeq(input) {
244
244
  // ---------------------------------------------------------------------------
245
245
  // Component
246
246
  // ---------------------------------------------------------------------------
247
- export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded = false, 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, }) {
247
+ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded = false, 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, draftClearNonce, onNotice, }) {
248
248
  const { stdout } = useStdout();
249
249
  const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
250
250
  const { buffer, cursor, historyIndex, dismissed, sel } = editor;
@@ -373,6 +373,16 @@ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, st
373
373
  lastReportedDraft.current = { hasDraft, rows: draftRowCount };
374
374
  onDraftChange({ hasDraft, rows: draftRowCount });
375
375
  }, [hasDraft, draftRowCount, onDraftChange]);
376
+ // --- App-requested draft clear (ctrl-c-clear-draft-exit). --------
377
+ // The NONCE, not the buffer, is the dependency: App cannot reach this
378
+ // reducer directly, and keying on content would miss back-to-back
379
+ // requests that land on the same (empty) buffer. `'clear'` is
380
+ // idempotent, so a StrictMode double-invoked effect is harmless.
381
+ useEffect(() => {
382
+ if (draftClearNonce === undefined || draftClearNonce <= 0)
383
+ return;
384
+ dispatch({ type: 'clear' });
385
+ }, [draftClearNonce]);
376
386
  // --- Mutators. -----------------------------------------------------------
377
387
  // ONE DISPATCH EACH, and that is the rule §3.4 asks a reviewer to check line
378
388
  // by line. Two dispatches from one key would put the legacy root back to two
@@ -57,4 +57,26 @@ export declare function activityBudget(cols: number): number;
57
57
  * columns and break at 80.
58
58
  */
59
59
  export declare function activityLine(run: SubagentRun, cols: number): string;
60
+ /**
61
+ * The row's supervisor badge (subagent-overseer-v2 AC-5): what the supervisor
62
+ * last did to THIS child and how long ago, e.g. `nudged 2m03s`.
63
+ *
64
+ * `lastIntervention.at` is ABSOLUTE epoch ms (R-P2-3), the same units as
65
+ * `startedAt`, so this is one subtraction - no second clock convention.
66
+ */
67
+ export declare function overseerBadge(run: SubagentRun, at: number): string;
68
+ /**
69
+ * The activity column WITH the badge composed in (AC-5 / risk table): the
70
+ * badge rides the SAME `activityBudget(cols)` allocation the prose uses, so
71
+ * a row can never grow past its terminal; on a narrow terminal the badge
72
+ * degrades to the action word alone - the who-acted fact outranks the
73
+ * what-they-are-doing tail exactly when there is room for only one.
74
+ */
75
+ export declare function activityWithBadge(run: SubagentRun, cols: number, at: number): string;
76
+ /**
77
+ * The panel's one supervisor status line (AC-5): the most recent intervention
78
+ * across the roster, human-phrased. Empty when the supervisor has not acted
79
+ * yet - furniture that says nothing is worse than no line.
80
+ */
81
+ export declare function supervisorStatusLine(runs: SubagentRun[], at: number): string;
60
82
  export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }: TeamPanelProps): React.ReactElement | null;
@@ -101,6 +101,69 @@ export function activityLine(run, cols) {
101
101
  return 'aborted';
102
102
  }
103
103
  }
104
+ /** The past-tense word the supervisor badge and status line render. */
105
+ function interventionWord(action) {
106
+ switch (action) {
107
+ case 'nudge':
108
+ return 'nudged';
109
+ case 'replace':
110
+ return 'replaced';
111
+ case 'abandon':
112
+ return 'abandoned';
113
+ default:
114
+ return 'waited';
115
+ }
116
+ }
117
+ /**
118
+ * The row's supervisor badge (subagent-overseer-v2 AC-5): what the supervisor
119
+ * last did to THIS child and how long ago, e.g. `nudged 2m03s`.
120
+ *
121
+ * `lastIntervention.at` is ABSOLUTE epoch ms (R-P2-3), the same units as
122
+ * `startedAt`, so this is one subtraction - no second clock convention.
123
+ */
124
+ export function overseerBadge(run, at) {
125
+ const last = run.lastIntervention;
126
+ if (last === undefined)
127
+ return '';
128
+ return `${interventionWord(last.action)} ${formatDuration(Math.max(0, at - last.at))}`;
129
+ }
130
+ /**
131
+ * The activity column WITH the badge composed in (AC-5 / risk table): the
132
+ * badge rides the SAME `activityBudget(cols)` allocation the prose uses, so
133
+ * a row can never grow past its terminal; on a narrow terminal the badge
134
+ * degrades to the action word alone - the who-acted fact outranks the
135
+ * what-they-are-doing tail exactly when there is room for only one.
136
+ */
137
+ export function activityWithBadge(run, cols, at) {
138
+ const last = run.lastIntervention;
139
+ if (last === undefined)
140
+ return activityLine(run, cols);
141
+ const badge = overseerBadge(run, at);
142
+ if (cols < TEAM_LIMITS.activityWideCols)
143
+ return interventionWord(last.action);
144
+ return sanitizeActivity(`${badge}; ${activityLine(run, cols)}`, activityBudget(cols));
145
+ }
146
+ /**
147
+ * The panel's one supervisor status line (AC-5): the most recent intervention
148
+ * across the roster, human-phrased. Empty when the supervisor has not acted
149
+ * yet - furniture that says nothing is worse than no line.
150
+ */
151
+ export function supervisorStatusLine(runs, at) {
152
+ let label = '';
153
+ let latest;
154
+ for (const run of runs) {
155
+ const last = run.lastIntervention;
156
+ if (last !== undefined && (latest === undefined || last.at > latest.at)) {
157
+ latest = last;
158
+ label = run.label;
159
+ }
160
+ }
161
+ if (latest === undefined)
162
+ return '';
163
+ const ago = formatDuration(Math.max(0, at - latest.at));
164
+ const head = latest.reasonHead.length > 0 ? ` (${latest.reasonHead})` : '';
165
+ return `supervisor: ${interventionWord(latest.action)} ${label} ${ago} ago${head}`;
166
+ }
104
167
  export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }) {
105
168
  const projection = layout ?? buildTeamPanelLayout({
106
169
  snapshot, terminalRows: rows, availableRows: Infinity,
@@ -130,8 +193,14 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
130
193
  // `reducedMotion` and a non-Unicode terminal fall back to the same
131
194
  // static marker, exactly as `AssistantEntry` and `ToolCard` already do.
132
195
  const animate = isRunning(run) && !reducedMotion && caps.unicode;
133
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
134
- }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
196
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityWithBadge(run, cols, at)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
197
+ }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), (() => {
198
+ // The panel's supervisor line (AC-5): one row, only when the
199
+ // supervisor has actually acted - the per-row badges above already
200
+ // carry the per-child facts.
201
+ const line = supervisorStatusLine(snapshot.runs, at);
202
+ return line ? (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', line] })) : null;
203
+ })(), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
135
204
  }
136
205
  function marker(run, glyphs) {
137
206
  if (run.phase === 'done')
@@ -160,17 +160,22 @@ export function editorReducer(state, action) {
160
160
  case 'recall':
161
161
  // `historyIndex` is SET, not reset, which is why recall cannot reuse
162
162
  // `replace` — the two differ in exactly the field that keeps the walk
163
- // alive. `dismissed` and `sel` are LEFT ALONE, which is not an oversight:
164
- // the three `setState` calls this replaces did not touch them either, and
165
- // an Esc-dismissed popup that reopened when the user stepped through their
166
- // history would be a behaviour change smuggled in by a refactor whose whole
167
- // claim is that input semantics are identical (K-7).
163
+ // alive. `dismissed` is forced TRUE (history-recall-autocomplete-popup):
164
+ // recalled content is not a query the user is typing, so the suggestion
165
+ // popup must not appear over it - while it is visible it owns Up/Down and
166
+ // the second Up moves the highlight instead of stepping history. Any
167
+ // buffer edit (insert / backspace / replace / input ...) re-arms the
168
+ // popup through DRAFT_FLAGS, so typing over a recall restores them. The
169
+ // same gate covers the `@file` popup, which shares `dismissed`.
170
+ // `sel` is still LEFT ALONE: invisible while dismissed, re-homed by the
171
+ // next buffer change; touching it would only widen the diff (K-7 note).
168
172
  return withPrune({
169
173
  ...state,
170
174
  buffer: action.buffer,
171
175
  cursor: snapGrapheme(action.buffer, action.cursor),
172
176
  preferredVisualColumn: undefined,
173
177
  historyIndex: action.historyIndex,
178
+ dismissed: true,
174
179
  });
175
180
  case 'select':
176
181
  return state.sel === action.sel ? state : { ...state, sel: action.sel };
@@ -68,6 +68,7 @@ export declare const interactionCopy: {
68
68
  readonly confirm: "Enter confirm";
69
69
  readonly stopServices: "Ctrl+C stop services";
70
70
  readonly exit: "Ctrl+C x2 exit";
71
+ readonly exitDraft: "Ctrl+C x2 clear input, x3 exit";
71
72
  readonly help: "/help";
72
73
  readonly shiftNewline: "Shift+Enter newline needs terminal support";
73
74
  readonly mode: "Shift+Tab switch mode";
@@ -26,6 +26,7 @@ export const interactionCopy = {
26
26
  close: 'Esc close', closeCompletion: 'Esc menu', executeCommand: 'Enter command',
27
27
  complete: 'Tab complete', select: 'Up/Down select', scroll: 'PgUp/PgDn scroll',
28
28
  confirm: 'Enter confirm', stopServices: 'Ctrl+C stop services', exit: 'Ctrl+C x2 exit',
29
+ exitDraft: 'Ctrl+C x2 clear input, x3 exit',
29
30
  help: '/help', shiftNewline: 'Shift+Enter newline needs terminal support',
30
31
  mode: 'Shift+Tab switch mode', update: '/update',
31
32
  idlePlaceholder: 'Ask a question or describe a task...', runningPlaceholder: 'Add a follow-up...',
@@ -17,6 +17,8 @@ export interface ActionHintInput {
17
17
  updateAvailable?: boolean;
18
18
  exitHint?: string;
19
19
  modeToggleKey?: string;
20
+ /** Draft present: the exit clause becomes the clear-then-exit ladder. */
21
+ hasDraft?: boolean;
20
22
  }
21
23
  export declare function escapeClause(input: ActionHintInput): string;
22
24
  export declare function clipStatusText(text: string, cells: number): string;
@@ -71,7 +71,7 @@ export function buildActionClauses(input) {
71
71
  if (!feedback && input.hintsEnabled !== false && !input.overlay) {
72
72
  if (input.updateAvailable)
73
73
  add(copy.update);
74
- add(copy.exit);
74
+ add(input.hasDraft ? copy.exitDraft : copy.exit);
75
75
  add((input.modeToggleKey ?? 'Shift+Tab') + ' ' + copy.modeLabel);
76
76
  add(copy.help);
77
77
  }
@@ -83,6 +83,7 @@ function feedbackLabel(input, form) {
83
83
  const labels = {
84
84
  confirm: ['Awaiting confirmation', 'Ask', 'Ask'], stopping: ['Esc stop', 'EscS', 'EscS'],
85
85
  'force-stop': ['Esc force', 'EscF', 'EscF'], exit: ['^C exit', '^Cexit', '^C'],
86
+ 'clear-input': ['^C clear', '^Cclear', '^C'],
86
87
  copying: ['Copying', 'Copy', 'Copy'], copied: ['Copied', 'Copied', 'Copy'],
87
88
  'copy-sent': ['Copy sent', 'Sent', 'Sent'], 'copy-cleanup': ['Copy cleanup', 'Err', 'Err'],
88
89
  'copy-error': ['Copy err', 'Err', 'Err'], notice: ['Notice', 'Notice', 'Note'],
@@ -12,7 +12,10 @@ export function buildTeamPanelLayout(input) {
12
12
  }
13
13
  const selection = selectPanelRows(input.snapshot.runs, TEAM_LIMITS.panelMaxRows);
14
14
  const natural = 1 + selection.visible.length + Number(selection.hiddenTotal > 0)
15
- + Number(Boolean(input.snapshot.lastMessage));
15
+ + Number(Boolean(input.snapshot.lastMessage))
16
+ // The supervisor status line (subagent-overseer-v2 AC-5) occupies one
17
+ // row whenever any run carries an intervention badge.
18
+ + Number(input.snapshot.runs.some((run) => run.lastIntervention !== undefined));
16
19
  const collapsed = nonNegativeInt(input.terminalRows) < TEAM_LIMITS.panelCollapseRows
17
20
  || available - natural < TODO_LIMITS.panelMinRows;
18
21
  return { ...selection, collapsed, rowCount: collapsed ? 1 : natural };
@@ -17,7 +17,10 @@ function keyRows(times) {
17
17
  // either of them is a key the affected user never learns about, which is the
18
18
  // one-shot notice's failure repeated with a different switch.
19
19
  ['Shift+Tab / Ctrl+P', 'Toggle plan mode (same as /plan)'],
20
- [`Ctrl+C ${times}2`, 'Exit'],
20
+ // The ladder clears a draft first (ctrl-c-clear-draft-exit):
21
+ // with a draft x2 clears the input and x3 exits; an empty input
22
+ // keeps the previous x2 exit.
23
+ ['Ctrl+C', `Draft: ${times}2 clear input, ${times}3 exit. Empty: ${times}2 exit`],
21
24
  ['Ctrl+L', 'Redraw the frame'],
22
25
  ['Ctrl+T', 'Show/hide thinking (off by default)'],
23
26
  ['Ctrl+I', 'Build the project index (bundled skill; asks first; needs keyboard enhancement)'],
@@ -128,6 +128,16 @@ function usePickerInput(props, state) {
128
128
  activate();
129
129
  else if (input === '/' && searchable)
130
130
  setSearching(true);
131
+ // `e` edits the focused profile directly (profile-picker-edit-key). The
132
+ // branch is deliberately NOT guarded by `!props.manager`: the manager page
133
+ // gains the same one-keystroke path (there `selected === focused`, see the
134
+ // effect in usePickerState), which spares the Enter-to-the-action-row hop.
135
+ // No-op on the `Current custom` row and the action rows (`focused` is
136
+ // undefined there), and the `searching` early-return above keeps `e` a
137
+ // query character while typing a search.
138
+ else if (!key.ctrl && !key.meta && input.toLowerCase() === 'e' && focused) {
139
+ props.onEdit(focused);
140
+ }
131
141
  });
132
142
  }
133
143
  function profilePickerLabel(props, entry) {
@@ -165,5 +175,6 @@ export function ModelProfilePicker(props) {
165
175
  usePickerInput(props, state);
166
176
  return _jsx(OverlayFrame, { ...props, title: props.error ?? (props.manager ? 'Manage profiles'
167
177
  : `${props.role === 'main' ? 'Main' : 'Fast'} profile`), rows: buildPickerRows(props, state), scrollOffset: state.searching ? 0 : state.offset, hint: state.searching ? 'Type search | Enter done | Esc back'
168
- : 'Enter select/action | Esc back' });
178
+ : props.manager ? 'Enter action | e edit | Esc back'
179
+ : 'Enter select | e edit | Esc back' });
169
180
  }
@@ -1,6 +1,6 @@
1
1
  /** Facts, never translated toast text, determine action ownership and feedback. */
2
2
  export interface StatusFeedback {
3
- kind: 'confirm' | 'stopping' | 'force-stop' | 'exit' | 'copying' | 'copied' | 'copy-sent' | 'copy-cleanup' | 'copy-error' | 'notice';
3
+ kind: 'confirm' | 'stopping' | 'force-stop' | 'exit' | 'clear-input' | 'copying' | 'copied' | 'copy-sent' | 'copy-cleanup' | 'copy-error' | 'notice';
4
4
  text: string;
5
5
  level: 'info' | 'warn' | 'error';
6
6
  }
@@ -19,6 +19,8 @@ export interface StatusFeedbackInput {
19
19
  text: string;
20
20
  };
21
21
  ctrlCArmed?: boolean;
22
+ /** Draft present: the armed feedback names the clear rung, not exit. */
23
+ hasDraft?: boolean;
22
24
  selectionPending?: boolean;
23
25
  liveServices?: number;
24
26
  toast?: {
@@ -7,7 +7,9 @@ export function projectStatusFeedback(input) {
7
7
  let feedback;
8
8
  if (input.ctrlCArmed && !input.copyState?.busy && !input.copyState?.cleanupPending
9
9
  && !input.selectionPending && !input.liveServices) {
10
- feedback = { kind: 'exit', text: 'Press Ctrl+C again to exit', level: 'warn' };
10
+ feedback = input.hasDraft
11
+ ? { kind: 'clear-input', text: 'Press Ctrl+C again to clear input', level: 'warn' }
12
+ : { kind: 'exit', text: 'Press Ctrl+C again to exit', level: 'warn' };
11
13
  }
12
14
  else if (input.copyState?.cleanupPending) {
13
15
  feedback = { kind: 'copy-cleanup', text: 'Copy failed; cleaning up', level: 'error' };