@aragon-agent/cli 0.6.7 → 0.6.8

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 (63) hide show
  1. package/README.md +72 -43
  2. package/dist/agent/controller.d.ts +10 -6
  3. package/dist/agent/controller.js +35 -48
  4. package/dist/agent/headless.d.ts +2 -1
  5. package/dist/agent/prompt-options.d.ts +15 -0
  6. package/dist/agent/prompt-options.js +1 -0
  7. package/dist/cli.js +83 -53
  8. package/dist/commands/builtins.js +3 -1
  9. package/dist/commands/registry.d.ts +2 -3
  10. package/dist/input/interrupt-gesture.d.ts +18 -0
  11. package/dist/input/interrupt-gesture.js +19 -0
  12. package/dist/input/pointer-router.d.ts +11 -0
  13. package/dist/input/pointer-router.js +14 -0
  14. package/dist/todo/store.d.ts +8 -13
  15. package/dist/todo/store.js +13 -14
  16. package/dist/todo/types.d.ts +2 -1
  17. package/dist/ui/App.d.ts +2 -0
  18. package/dist/ui/App.js +166 -128
  19. package/dist/ui/Composer.d.ts +11 -5
  20. package/dist/ui/Composer.js +7 -11
  21. package/dist/ui/PromptCaret.d.ts +11 -0
  22. package/dist/ui/PromptCaret.js +31 -0
  23. package/dist/ui/PromptInput.d.ts +9 -6
  24. package/dist/ui/PromptInput.js +34 -14
  25. package/dist/ui/StatusBar.d.ts +1 -0
  26. package/dist/ui/StatusBar.js +14 -8
  27. package/dist/ui/Transcript.js +1 -0
  28. package/dist/ui/composer-rows.js +50 -9
  29. package/dist/ui/frame-differ.js +2 -19
  30. package/dist/ui/frame-observer.d.ts +17 -0
  31. package/dist/ui/frame-observer.js +81 -0
  32. package/dist/ui/frame-parser.d.ts +7 -0
  33. package/dist/ui/frame-parser.js +18 -0
  34. package/dist/ui/layout/AppShell.d.ts +7 -52
  35. package/dist/ui/layout/AppShell.js +2 -2
  36. package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
  37. package/dist/ui/layout/ScrollIndicator.js +4 -4
  38. package/dist/ui/layout/ScrollViewport.d.ts +13 -66
  39. package/dist/ui/layout/ScrollViewport.js +183 -185
  40. package/dist/ui/layout/budget.d.ts +3 -51
  41. package/dist/ui/layout/budget.js +4 -53
  42. package/dist/ui/layout/document-layout.d.ts +13 -0
  43. package/dist/ui/layout/document-layout.js +11 -0
  44. package/dist/ui/layout/follow-state.d.ts +1 -0
  45. package/dist/ui/layout/follow-state.js +1 -3
  46. package/dist/ui/layout/scroll-indicator.d.ts +1 -1
  47. package/dist/ui/layout/scroll-indicator.js +3 -2
  48. package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
  49. package/dist/ui/layout/scrollbar-geometry.js +13 -0
  50. package/dist/ui/layout/todo-layout.d.ts +1 -0
  51. package/dist/ui/layout/todo-layout.js +3 -2
  52. package/dist/ui/layout/viewport-geometry.d.ts +1 -0
  53. package/dist/ui/layout/viewport-geometry.js +1 -0
  54. package/dist/ui/layout/virtual-window.d.ts +1 -0
  55. package/dist/ui/layout/virtual-window.js +1 -1
  56. package/dist/ui/overlays/HelpOverlay.js +5 -2
  57. package/dist/ui/screen.d.ts +1 -1
  58. package/dist/ui/screen.js +4 -6
  59. package/dist/ui/scrollbar-controller.d.ts +30 -0
  60. package/dist/ui/scrollbar-controller.js +124 -0
  61. package/dist/ui/use-wheel-routing.d.ts +2 -0
  62. package/dist/ui/use-wheel-routing.js +6 -0
  63. package/package.json +2 -2
package/README.md CHANGED
@@ -438,8 +438,9 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
438
438
  | `Alt+Enter` / `Shift+Enter` | Insert a newline |
439
439
  | `Shift+Tab` | Toggle **plan mode** (`BUILD` <-> `PLAN`). Equivalent: `/plan`. On Windows this needs a console flag Node < 22.17 does not set; aragon sets it at startup — see [Requirements](#requirements) |
440
440
  | `Ctrl+P` | The same toggle, over a channel no Windows console can swallow. Use it if `Shift+Tab` does nothing |
441
- | `Esc` | Abort the run / close an overlay / close a popup |
442
- | `Esc` ×2 | **Force-stop** a run that did not abort: kills the shell command it is blocked on and returns the view to `idle`. The second press only does anything while the run is *still* running, so in a healthy run it is a no-op |
441
+ | `Esc` | Close an overlay or popup; otherwise ask for interruption confirmation. While idle, cancel pending auto-continuation |
442
+ | `Esc` ×2 | Interrupt the run when pressed within 1.5 seconds. Other keys or closing a menu cancel the first press |
443
+ | `Esc` again after interruption | **Force-stop** a run that is still stopping, kill foreground shell commands and return to `idle`; background services remain running |
443
444
  | `Ctrl+C` | Stop the background **services** the agent started, when there are any. It does not arm exit |
444
445
  | `Ctrl+C` ×2 | Exit (first press warns), once no services are running |
445
446
  | `Ctrl+L` | Redraw the frame (clear the screen in inline mode) |
@@ -460,8 +461,11 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
460
461
  In **full-screen mode** the transcript is scrolled by the app itself: `PgUp` /
461
462
  `PgDn` and `Shift+↑` / `Shift+↓`. While pinned to the bottom the viewport follows
462
463
  new output automatically; once you scroll away the status bar shows `↑N`, a slim
463
- rail on the right edge shows where you are, and a hint counts the lines below
464
- you. Submitting a message always re-pins to the newest output. In **inline mode**
464
+ scrollbar occupies the terminal's last column (including 40-column windows). Drag its thumb
465
+ to move through history, or click the track to move one page. Messages, the team panel
466
+ and the input area scroll together; typing or editing returns to the input without
467
+ losing the draft. The fixed status row shows `PgDn down` while reading history.
468
+ `PgDn` moves one page down; editing or submitting returns directly to the end. In **inline mode**
465
469
  (`--no-fullscreen`) history lives in your terminal's native scrollback, printed
466
470
  once via Ink `<Static>`.
467
471
 
@@ -474,9 +478,10 @@ open. It never touches your draft, and it never recalls prompt history: that is
474
478
  Two things follow from the app owning the mouse, and they are worth knowing
475
479
  before you meet them:
476
480
 
477
- - **Selecting text now needs `Shift`+drag** in most terminals. That is the
478
- terminal's convention for "an application is tracking the mouse", not
479
- something this CLI can opt out of while still reading wheel events.
481
+ - **Dragging text selects and copies it when mouse selection is enabled.**
482
+ Dragging the last-column scrollbar scrolls instead. `--no-mouse-select` disables
483
+ text selection while preserving scrollbar dragging. `Shift`+drag uses the
484
+ terminal's native selection on hosts that support that convention.
480
485
  - **`tmux` with `mouse on` keeps the wheel for itself**, so the app never sees
481
486
  it. `set -g mouse off` hands it back.
482
487
  - **A Windows console on Node below 22.17.0 cannot report the wheel at all**,
@@ -512,7 +517,7 @@ Density and hints are settable the same three ways: `--compact` / `--no-hints`
512
517
  for one run, `aragon config set density compact` / `aragon config set hints false`
513
518
  to persist. The composer hint row also shortens to `? help` on its own after a
514
519
  few sessions — except while a run is in progress, when it always spells out
515
- `esc abort` in full.
520
+ `esc ×2 interrupt` in full.
516
521
 
517
522
  ## What you see while it runs
518
523
 
@@ -624,8 +629,9 @@ near-identical added ones.
624
629
 
625
630
  - Your previous shell output is **covered, not erased**, and returns untouched
626
631
  when you exit. Nothing in your scrollback is destroyed.
627
- - The frame is fixed at `rows - 1` tall, which is what keeps the composer and the
628
- status bar at the bottom of the screen even on an empty session.
632
+ - The frame is fixed at `rows - 1` tall. The header, activity row and status bar
633
+ stay visible. The input belongs to the scrolling document; short sessions fill
634
+ the space above it, and browsing history moves it out of view.
629
635
 
630
636
  On exit the session is replayed into the normal buffer as plain text so the
631
637
  conversation survives leaving the screen (`--no-exit-transcript` opts out;
@@ -638,10 +644,13 @@ writing screen-control sequences into a pipe or a redirected file is never safe.
638
644
  `--no-fullscreen`, `ARAGON_FULLSCREEN=0`, or `aragon config set fullscreen false`
639
645
  opt out permanently.
640
646
 
641
- The terminal's native scrollback and mouse wheel do not scroll the transcript in
642
- this mode, and mouse tracking is deliberately left off because enabling it costs
643
- text selection and copy in most terminals. If a crash ever strands your terminal
644
- on the alternate screen, `reset` restores it.
647
+ The alternate screen uses an application scrollbar rather than the terminal's
648
+ native scrollback. Windows Terminal and conhost have different mouse/VT capabilities;
649
+ the PowerShell version alone does not determine dragging support. If the host cannot
650
+ deliver SGR mouse reports, the scrollbar remains visible and keyboard scrolling works.
651
+ `--no-mouse` provides that keyboard-only path; `--no-diff-render` disables output
652
+ differencing without disabling the scrollbar. A mid-session resize below 40 columns
653
+ or 12 rows displays a size hint and preserves the draft for restoration.
645
654
 
646
655
  ## Render performance
647
656
 
@@ -742,8 +751,8 @@ assumptions rather than to retry.
742
751
 
743
752
  **Dismissing a plan is not how you stop.** `Esc` on a plan card asks for a
744
753
  *better* plan — the agent is told to refine it and submit again — so a model that
745
- keeps submitting keeps getting cards. To stop the run itself, **press `Esc`
746
- twice**: the first closes the card, the second aborts the run. `Ctrl+C` twice
754
+ keeps submitting keeps getting cards. To stop the run itself, close the card with `Esc`, then **press `Esc`
755
+ twice within 1.5 seconds** to interrupt. Closing the card never counts as the first press. `Ctrl+C` twice
747
756
  still exits the session outright.
748
757
 
749
758
  ### Where the mode is shown
@@ -800,8 +809,8 @@ ask — and the agent writes the finished plan as its final message in markdown.
800
809
  For work with three or more distinct steps the agent keeps a **visible plan**:
801
810
  it writes a checklist with the `todo_write` tool, marks exactly one item as in
802
811
  progress, and ticks it off before starting the next. In full-screen mode that
803
- list is rendered as a right-hand rail, targeting 15% of the terminal width
804
- (14–36 columns, including its separator and padding).
812
+ list is rendered as a right-hand rail, targeting 15% of the terminal width
813
+ (14–36 columns, including its separator and padding).
805
814
 
806
815
  ```
807
816
  transcript ... | TODO 2/7
@@ -809,7 +818,7 @@ list is rendered as a right-hand rail, targeting 15% of the terminal width
809
818
  |
810
819
  | v 1 Read the reducer
811
820
  | v 2 Design the store
812
- | > 3 Wiring the rail...
821
+ | > 3 Wiring the rail...
813
822
  | o 4 Add the panel-rows test
814
823
  | o 5 Update the README
815
824
  | +2 more
@@ -821,8 +830,8 @@ for a question, or for a search with no work attached — and a one-item list
821
830
  against an empty plan is refused outright. A session that never plans has
822
831
  byte-identical layout to a build without this feature.
823
832
 
824
- **The list is a projection of what the model believes**, so nothing that does
825
- not also tell the model may change it — except you, saying so:
833
+ **The list shows the current task's plan.** Starting a new task or explicitly
834
+ clearing the list removes this live view while preserving conversation history:
826
835
 
827
836
  - `/clear` and `/todo clear` take the plan off the screen. Neither touches the
828
837
  conversation, so the model still knows what it was doing and its next update
@@ -831,10 +840,14 @@ not also tell the model may change it — except you, saying so:
831
840
  - `/save` writes the list and `/resume` restores it. Resuming a session that has
832
841
  no list clears the current one, rather than leaving a plan on screen whose
833
842
  conversation has just been replaced.
834
- - A finished plan is dropped at the START of your next message, so you still get
835
- to see `7/7 done`. An unfinished one survives "continue" / "now do the rest",
836
- and is dropped after three unrelated turns rather than holding part of the
837
- screen for the rest of the session.
843
+ - A normal new task clears the previous plan, including unfinished steps, only
844
+ when it starts successfully. The completed run keeps its final plan visible.
845
+ `/todo continue` and automatic continuation preserve the plan and its timestamp;
846
+ ordinary text such as "continue" starts a new task. Steering keeps the current
847
+ plan. Until a valid new `todo_write`, the rail, inline strip and count stay hidden.
848
+ Startup failure or cancellation keeps the old plan. History cards remain available.
849
+ - During a live run, interrupt before `/reset` or `/resume`. A conversation switch
850
+ while startup is waiting cancels that pending request and returns the view to idle.
838
851
 
839
852
  ### When a run ends with steps left
840
853
 
@@ -857,7 +870,7 @@ setting:
857
870
 
858
871
  `Esc` during the grace window cancels; so does typing anything. A run that ended
859
872
  with an **error** never auto-continues in any mode — you get a warning instead.
860
- A run **you** aborted with `Esc` now says nothing at all, where it used to
873
+ A run **you** interrupted with two `Esc` presses now says nothing at all, where it used to
861
874
  report what you had just interrupted.
862
875
 
863
876
  `/todo follow off` is the third mode: no notice, no continuation, silence.
@@ -889,19 +902,19 @@ planning discipline and hides the column — which is what you want with a scree
889
902
  reader, and what `--no-todo-panel` does for a whole session. Under `-p` the plan
890
903
  is written to stderr as `[todo] 3/7 <step>` lines, suppressed by `--quiet`.
891
904
 
892
- 右栏从 76 列开始显示,正文至少保留 62 列;100 列终端使用 15 列右栏。
893
- 已有任务时,团队面板按实际行数占用空间,补全菜单的边框和提示也计入预算。
894
- 例如 100×20、团队占 8 行时,TODO 仍以 4 行紧凑视图显示。
895
-
896
- 剩余 3–5 行时保留标题、完成计数、当前任务与合并提示 `-A +B`:
897
- `-9 +10` 表示上方隐藏 9 项、下方隐藏 10 项。6 行起使用常规视图;
898
- 所有任务各占一行,全文仍可在正文任务卡片中查看。进度条需要至少 20 列、8 行。
899
- 输入增长导致空间不足时团队先折叠为摘要;菜单不足 3 行时隐藏,其候选不会拦截输入。
900
-
901
- 低于 76 列、共享区域不足 3 行、打开 overlay、显式关闭面板、没有任务或
902
- inline 模式时不显示右栏。低于 12 行使用原有小终端提示;扩大窗口后自动恢复。
903
- inline 仍使用上文的单行任务条。`/todo status` 中的 `Panel: on` 仅表示配置开启,
904
- 实际可见性还取决于数据和窗口空间;可以通过该命令区分未生成计划与暂时隐藏。
905
+ 右栏从 76 列开始显示,正文至少保留 62 列;100 列终端使用 15 列右栏。
906
+ 已有任务时,团队面板按实际行数占用空间,补全菜单的边框和提示也计入预算。
907
+ 例如 100×20、团队占 8 行时,TODO 仍以 4 行紧凑视图显示。
908
+
909
+ 剩余 3–5 行时保留标题、完成计数、当前任务与合并提示 `-A +B`:
910
+ `-9 +10` 表示上方隐藏 9 项、下方隐藏 10 项。6 行起使用常规视图;
911
+ 所有任务各占一行,全文仍可在正文任务卡片中查看。进度条需要至少 20 列、8 行。
912
+ 输入增长导致空间不足时团队先折叠为摘要;菜单不足 3 行时隐藏,其候选不会拦截输入。
913
+
914
+ 低于 76 列、共享区域不足 3 行、打开 overlay、显式关闭面板、没有任务或
915
+ inline 模式时不显示右栏。低于 12 行使用原有小终端提示;扩大窗口后自动恢复。
916
+ inline 仍使用上文的单行任务条。`/todo status` 中的 `Panel: on` 仅表示配置开启,
917
+ 实际可见性还取决于数据和窗口空间;可以通过该命令区分未生成计划与暂时隐藏。
905
918
 
906
919
  ## Built-in tools
907
920
 
@@ -1007,7 +1020,8 @@ than features. `bash` now always settles: it returns when the process exits
1007
1020
  rather than when its output stream closes, so a command that detaches a
1008
1021
  grandchild can no longer leave the turn pending forever, and it is handed a
1009
1022
  closed stdin so a command that prompts fails instead of blocking on a read
1010
- nobody will answer. And `Esc` twice always force-stops, whatever the flag says.
1023
+ nobody will answer. Two `Esc` presses request interruption; another press force-stops a stuck run,
1024
+ whatever the flag says.
1011
1025
 
1012
1026
  ## Team subagents
1013
1027
 
@@ -1079,7 +1093,7 @@ children's tokens are folded into the `[usage]` footer.
1079
1093
  `debug`. A `team_send` subject is recorded, never its body; a subagent's brief
1080
1094
  and summary only at `trace`.
1081
1095
 
1082
- **`Esc`** aborts the whole dispatch — every subagent within a couple of seconds
1096
+ **Two `Esc` presses within 1.5 seconds** abort the whole dispatch — every subagent within a couple of seconds
1083
1097
  — and the report comes back marked `ABORTED` with whatever was finished.
1084
1098
 
1085
1099
  ## Fast model tier
@@ -2234,7 +2248,7 @@ node packages/cli/dist/cli.js --version
2234
2248
  shebang; `node packages/cli/dist/cli.js --version` prints the version.
2235
2249
  - `npm pack -w packages/cli --dry-run --json` lists `dist/cli.js` under `files`.
2236
2250
  - With `ANTHROPIC_API_KEY` set: `aragon` streams a reply, a tool card renders,
2237
- `Esc` aborts mid-run, `/model` switches models, `/settings` saves a key,
2251
+ `Esc` twice interrupts mid-run, `/model` switches models, `/settings` saves a key,
2238
2252
  `Ctrl+C` twice exits.
2239
2253
  - `echo "list files" | node dist/cli.js -p` prints an answer and exits `0`.
2240
2254
  - A missing/invalid key produces a visible, actionable message (never a blank
@@ -2252,7 +2266,7 @@ node packages/cli/dist/cli.js --version
2252
2266
  nothing is truncated to a single line.
2253
2267
  - `--no-hints` on a 20-row terminal in plan mode - the status bar still says
2254
2268
  `PLAN` with no chip on screen.
2255
- - Dismiss a plan card with `Esc`, let the agent resubmit, then press `Esc` twice:
2269
+ - Dismiss a plan card with `Esc`, let the agent resubmit, then close the card and press `Esc` twice within 1.5 seconds:
2256
2270
  the run aborts and the session returns to an idle prompt. Do this one BY HAND
2257
2271
  as well as in a test - the point of the check is whether the exit is findable,
2258
2272
  which no assertion can tell you.
@@ -2260,3 +2274,18 @@ node packages/cli/dist/cli.js --version
2260
2274
  ## License
2261
2275
 
2262
2276
  MIT
2277
+
2278
+
2279
+ ### 输入光标与任务切换验收
2280
+
2281
+ 输入获得应用内焦点时,光标每 500 ms 明暗切换;编辑、移动和恢复焦点后立即亮起。
2282
+ `reducedMotion` 开启时保持常亮,弹层打开时暂停。fullscreen 和 inline 使用相同规则。
2283
+ 单色终端用等列宽的下划线标记,原文相同时改用 `^`,零宽附加码点保留以兼容 Ink。
2284
+ 标记不进入草稿、历史或提交文本;屏幕选择复制会包含当时可见的标记。
2285
+ UTF-16 编辑和 ZWJ/组合字形仍有既有限制,本次修复保证视觉光标定位及相位布局稳定。
2286
+
2287
+ 在 Windows Terminal/PowerShell 与 POSIX TTY 分别验证两种模式,窗口覆盖
2288
+ 120×30、100×20、80×24、60×16:观察空输入、英文、中文、emoji、多行和组合附加符至少三个周期。
2289
+ 检查流式输出不阻止闪烁;单 ESC 不停止任务,两击中断,卡住时第三击强停;菜单关闭不计入两击。
2290
+ 任务结束后保留最终计划,下一条普通消息启动时旧计划消失,新有效计划出现才恢复;显式续跑保留计划。
2291
+ 强停后立即提交,在等待中双 ESC 取消,旧引擎退出后不得自行执行已取消的消息。
@@ -8,6 +8,7 @@
8
8
  * success — that promise resolves even on failure. Success/failure is derived
9
9
  * from the event stream by the reducer (TUI) and the headless writer.
10
10
  */
11
+ import type { PromptOptions, PromptOutcome } from './prompt-options.js';
11
12
  import { ModelRegistry, type AgentEvent, type AgentTool, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ThinkingLevel } from '@aragon-agent/core';
12
13
  import type { CliConfig, CompactionConfig, FastConfig, RetryConfig, TeamConfig, ThemeName, TodoConfig } from '../config/schema.js';
13
14
  import { type ConfirmRequest } from '../tools/index.js';
@@ -207,6 +208,7 @@ export declare class AgentController {
207
208
  * abort lands.
208
209
  */
209
210
  private runGen;
211
+ private startupSequence;
210
212
  /**
211
213
  * Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
212
214
  * Cleared at the next `agent_start`.
@@ -264,21 +266,23 @@ export declare class AgentController {
264
266
  * underlying promise so headless mode can await completion. `agent.prompt()`
265
267
  * never rejects, so callers must not treat resolution as success.
266
268
  */
267
- prompt(text: string): Promise<void>;
269
+ prompt(text: string, options?: PromptOptions): Promise<PromptOutcome>;
268
270
  /**
269
271
  * Wait for the engine to unwind, bounded.
270
272
  *
271
273
  * BOUNDED BECAUSE THE UNBOUNDED CASE IS THE ONE THIS FEATURE IS ABOUT: a tool
272
- * that never settles is exactly why rung two exists, and `Agent.waitForIdle()`
274
+ * that never settles is exactly why the force-stop rung exists, and `Agent.waitForIdle()`
273
275
  * alone would hang here for as long as it hangs there. The budget is the two
274
276
  * graces a wedged `bash` can legitimately spend, plus the executor's own.
275
277
  */
276
278
  private waitForEngineIdle;
277
- /** The pre-round body of `prompt`, unchanged apart from being private. */
279
+ /** Prepare before clearing the plan, then enter the engine without yielding. */
278
280
  private startPrompt;
281
+ /** Cancel pending startup as well as any engine run already in progress. */
279
282
  abort(): void;
283
+ private stopEngine;
280
284
  /**
281
- * Rung two of the Esc ladder: stop the AGENT, whatever it is blocked on
285
+ * Final rung of the Esc ladder: stop the AGENT, whatever it is blocked on
282
286
  * (§3.6 / G3).
283
287
  *
284
288
  * FOUR STEPS, AND THE FOURTH IS AN ABSENCE.
@@ -286,7 +290,7 @@ export declare class AgentController {
286
290
  * 1. `abort()` - everything it does today, unchanged.
287
291
  * 2. `killForeground('force')` - hard-kill every tracked foreground `bash`
288
292
  * child. THIS is what actually unblocks a wedged `await tool.execute`;
289
- * an abort signal a tool ignores is advisory, and rung two exists for
293
+ * an abort signal a tool ignores is advisory, and the force-stop rung exists for
290
294
  * precisely the tools that ignore it.
291
295
  * 3. `runGen += 1` - so `App` can drop the events the engine will keep
292
296
  * emitting while it unwinds (I-7). Without it the view bounces straight
@@ -295,7 +299,7 @@ export declare class AgentController {
295
299
  * `Ctrl+C` stops the SERVICES. That asymmetry is the user's own two
296
300
  * sentences and it is exactly the kind of thing a later reader will
297
301
  * "fix" by making this kill everything - which would mean a user who
298
- * pressed Esc twice to stop a runaway turn also lost the dev server they
302
+ * confirmed a force-stop of a runaway turn also lost the dev server they
299
303
  * had been working against for an hour. It is invariant I-4.
300
304
  *
301
305
  * UNCONDITIONAL ON `bash.background` (P1-6 / D-9). The supervisor is always
@@ -45,7 +45,7 @@ import { buildSystemPrompt } from './system-prompt.js';
45
45
  *
46
46
  * `bash`'s own kill grace is what a wedged foreground command costs; this covers
47
47
  * the loop's own unwinding on top of it. Waiting FOREVER is not an option - a
48
- * tool that never settles is exactly why rung two exists.
48
+ * tool that never settles is exactly why the force-stop rung exists.
49
49
  */
50
50
  const ENGINE_UNWIND_GRACE_MS = 3000;
51
51
  /** Approval gate used whenever there is provably no human to ask (headless). */
@@ -246,6 +246,7 @@ export class AgentController {
246
246
  * abort lands.
247
247
  */
248
248
  runGen = 0;
249
+ startupSequence = 0;
249
250
  /**
250
251
  * Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
251
252
  * Cleared at the next `agent_start`.
@@ -855,43 +856,33 @@ export class AgentController {
855
856
  * underlying promise so headless mode can await completion. `agent.prompt()`
856
857
  * never rejects, so callers must not treat resolution as success.
857
858
  */
858
- async prompt(text) {
859
- // I-8 - THIS METHOD NEVER REJECTS, AND THAT HAS TO BE A PROPERTY OF THE
860
- // METHOD RATHER THAN A COMMENT ABOVE IT (P0-1).
861
- //
862
- // `App` calls this as `void controller.prompt(...)`, and
863
- // `logging/install.ts` routes an `unhandledRejection` into `handleFatal`,
864
- // which EXITS THE PROCESS. `Agent.prompt()` throws SYNCHRONOUSLY while
865
- // `Agent.running` is true - so before the force-stop ladder existed this was
866
- // merely nearly-true (the view was only ever idle when the engine was), and
867
- // rung two makes the view idle while the engine is still unwinding. Without
868
- // the wait and the guard below, "Esc, Esc, then type" would kill the CLI on
869
- // the very screen this feature exists to rescue.
870
- //
871
- // TWO PARTS, BOTH REQUIRED. First WAIT the engine out, bounded, so the
872
- // common case (the engine unwinds in a few hundred ms) starts a real run
873
- // rather than reporting a failure. Then swallow anything that still escapes
874
- // and surface it through the existing notify channel, because a message that
875
- // cannot be started must produce a visible sentence - never a crash, and
876
- // never silence.
877
- if (this.agent.state.isRunning) {
878
- this.abort();
879
- await this.waitForEngineIdle();
880
- }
859
+ async prompt(text, options = {}) {
860
+ // Capture BEFORE stopping: synchronous abort listeners may cancel this request.
861
+ const request = ++this.startupSequence;
881
862
  try {
882
- await this.startPrompt(text);
863
+ if (this.agent.state.isRunning) {
864
+ this.stopEngine();
865
+ await this.waitForEngineIdle();
866
+ }
867
+ if (request !== this.startupSequence) {
868
+ return { status: 'not-started', reason: 'cancelled' };
869
+ }
870
+ if (this.agent.state.isRunning)
871
+ throw new Error('The previous run is still stopping.');
872
+ return await this.startPrompt(text, options, request);
883
873
  }
884
874
  catch (err) {
885
875
  const reason = err instanceof Error ? err.message : String(err);
886
876
  getLogger().warn('agent', 'prompt_failed', { reason });
887
877
  this.notifyHostFn?.('error', `Could not start the run: ${reason}`);
878
+ return { status: 'not-started', reason: 'failed' };
888
879
  }
889
880
  }
890
881
  /**
891
882
  * Wait for the engine to unwind, bounded.
892
883
  *
893
884
  * BOUNDED BECAUSE THE UNBOUNDED CASE IS THE ONE THIS FEATURE IS ABOUT: a tool
894
- * that never settles is exactly why rung two exists, and `Agent.waitForIdle()`
885
+ * that never settles is exactly why the force-stop rung exists, and `Agent.waitForIdle()`
895
886
  * alone would hang here for as long as it hangs there. The budget is the two
896
887
  * graces a wedged `bash` can legitimately spend, plus the executor's own.
897
888
  */
@@ -911,31 +902,25 @@ export class AgentController {
911
902
  void this.agent.waitForIdle().then(finish, finish);
912
903
  });
913
904
  }
914
- /** The pre-round body of `prompt`, unchanged apart from being private. */
915
- startPrompt(text) {
916
- // A new user message is a new intent, so the previous turn's tool ceiling
917
- // expires here and anything a slash command queued is promoted (D-G1 / D-G2).
918
- // This and `steer()` below are the ONLY two places a user message enters the
919
- // engine, which is what makes a per-turn scope implementable at all (FG9).
905
+ /** Prepare before clearing the plan, then enter the engine without yielding. */
906
+ async startPrompt(text, options, request) {
920
907
  if (this.skillsEnabled)
921
908
  this.skills.beginUserTurn();
922
- // The ask-round budget is per USER TURN and resets here for the same reason
923
- // the skill frame does. `steer()` deliberately does NOT reset it: a user
924
- // steering "actually, use Postgres" into a running plan is answering the
925
- // questions, not buying four more rounds of them.
926
909
  this.askRounds = 0;
927
- // Same asymmetry, same reason (§3.5): a completed plan is dropped HERE
928
- // rather than at `agent_end`, so the user gets their `7/7 done` moment, and
929
- // an unfinished one survives "continue" / "now do the rest" but not four
930
- // unrelated turns.
931
- this.todos?.beginUserTurn();
932
- // The fast reviewer is asked whether the run is on track FOR THIS (§3.5.2).
933
- // Without it the reviewer judges progress without being told the goal, which
934
- // is the one input it cannot reconstruct from the event stream.
935
910
  this.fast?.setGoal(text);
936
- return this.agent.prompt(text);
911
+ if (request !== this.startupSequence) {
912
+ return { status: 'not-started', reason: 'cancelled' };
913
+ }
914
+ this.todos?.beginUserTurn(options.todoPolicy);
915
+ await this.agent.prompt(text);
916
+ return { status: 'finished' };
937
917
  }
918
+ /** Cancel pending startup as well as any engine run already in progress. */
938
919
  abort() {
920
+ this.startupSequence += 1;
921
+ this.stopEngine();
922
+ }
923
+ stopEngine() {
939
924
  // SET BEFORE `agent.abort()`, SYNCHRONOUSLY — guard 1 of §3.5.4a. The loop
940
925
  // still emits the batch's final `tool_execution_end` (`agent-loop.ts:249`)
941
926
  // after an Esc lands inside the last tool's `await`, and the reviewer's
@@ -959,7 +944,7 @@ export class AgentController {
959
944
  this.teamRuntime?.abortAll();
960
945
  }
961
946
  /**
962
- * Rung two of the Esc ladder: stop the AGENT, whatever it is blocked on
947
+ * Final rung of the Esc ladder: stop the AGENT, whatever it is blocked on
963
948
  * (§3.6 / G3).
964
949
  *
965
950
  * FOUR STEPS, AND THE FOURTH IS AN ABSENCE.
@@ -967,7 +952,7 @@ export class AgentController {
967
952
  * 1. `abort()` - everything it does today, unchanged.
968
953
  * 2. `killForeground('force')` - hard-kill every tracked foreground `bash`
969
954
  * child. THIS is what actually unblocks a wedged `await tool.execute`;
970
- * an abort signal a tool ignores is advisory, and rung two exists for
955
+ * an abort signal a tool ignores is advisory, and the force-stop rung exists for
971
956
  * precisely the tools that ignore it.
972
957
  * 3. `runGen += 1` - so `App` can drop the events the engine will keep
973
958
  * emitting while it unwinds (I-7). Without it the view bounces straight
@@ -976,7 +961,7 @@ export class AgentController {
976
961
  * `Ctrl+C` stops the SERVICES. That asymmetry is the user's own two
977
962
  * sentences and it is exactly the kind of thing a later reader will
978
963
  * "fix" by making this kill everything - which would mean a user who
979
- * pressed Esc twice to stop a runaway turn also lost the dev server they
964
+ * confirmed a force-stop of a runaway turn also lost the dev server they
980
965
  * had been working against for an hour. It is invariant I-4.
981
966
  *
982
967
  * UNCONDITIONAL ON `bash.background` (P1-6 / D-9). The supervisor is always
@@ -1492,6 +1477,7 @@ export class AgentController {
1492
1477
  }
1493
1478
  clearMessages() {
1494
1479
  this.agent.clearMessages();
1480
+ this.startupSequence += 1;
1495
1481
  // INVALIDATION SITE 3 (context-auto-compaction-hardening §3.2.3). The history
1496
1482
  // was CLEARED, not appended to, so the prefix length the last measurement
1497
1483
  // covered indexes an array that no longer exists.
@@ -1519,6 +1505,7 @@ export class AgentController {
1519
1505
  }
1520
1506
  replaceMessages(messages) {
1521
1507
  this.agent.replaceMessages(messages);
1508
+ this.startupSequence += 1;
1522
1509
  // INVALIDATION SITE 4 (§3.2.3) - `/resume`, and any future host-side rewrite.
1523
1510
  // `estimateAppendedTokens` bounds-checks as well, so a MISSED site costs
1524
1511
  // accuracy and never correctness; that belt does not make this call optional.
@@ -4,6 +4,7 @@
4
4
  * derived from the event stream (R2), not from the resolved promise:
5
5
  * 0 = success, 1 = agent error (incl. a silently-swallowed throw), 2 = config.
6
6
  */
7
+ import type { PromptOutcome } from './prompt-options.js';
7
8
  import type { AgentEvent, ModelInfo } from '@aragon-agent/core';
8
9
  import type { PreflightResult } from './controller.js';
9
10
  import type { TeamEvent } from '../team/types.js';
@@ -15,7 +16,7 @@ import { type FollowThroughMode } from '../todo/follow-through.js';
15
16
  export interface HeadlessController {
16
17
  preflight(): PreflightResult;
17
18
  subscribe(listener: (event: AgentEvent) => void): () => void;
18
- prompt(text: string): Promise<void>;
19
+ prompt(text: string): Promise<void | PromptOutcome>;
19
20
  getModelInfo(): ModelInfo;
20
21
  /**
21
22
  * The CLI-local team stream — OPTIONAL, deliberately (team-subagents §3.12 /
@@ -0,0 +1,15 @@
1
+ /** Explicit TUI task boundary; omission retains legacy programmatic behavior. */
2
+ export interface PromptOptions {
3
+ todoPolicy?: 'new-task' | 'continue';
4
+ }
5
+ /** History recording is independent of whether this message continues a plan. */
6
+ export interface SubmitMessageOptions extends PromptOptions {
7
+ userInitiated?: boolean;
8
+ }
9
+ /** Startup outcome only; engine events still describe the result of the run. */
10
+ export type PromptOutcome = {
11
+ status: 'finished';
12
+ } | {
13
+ status: 'not-started';
14
+ reason: 'cancelled' | 'failed';
15
+ };
@@ -0,0 +1 @@
1
+ export {};