@aragon-agent/cli 0.6.7 → 0.6.9

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 (117) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +107 -77
  3. package/dist/agent/controller.d.ts +11 -7
  4. package/dist/agent/controller.js +35 -48
  5. package/dist/agent/headless.d.ts +2 -1
  6. package/dist/agent/prompt-options.d.ts +15 -0
  7. package/dist/agent/prompt-options.js +1 -0
  8. package/dist/agent/reducer.d.ts +7 -113
  9. package/dist/agent/reducer.js +0 -62
  10. package/dist/cli.js +111 -182
  11. package/dist/commands/builtins.js +3 -1
  12. package/dist/commands/perf.d.ts +32 -2
  13. package/dist/commands/perf.js +32 -2
  14. package/dist/commands/registry.d.ts +4 -5
  15. package/dist/config/env.js +0 -130
  16. package/dist/config/load.d.ts +0 -2
  17. package/dist/config/load.js +2 -31
  18. package/dist/config/schema.d.ts +1 -14
  19. package/dist/config/schema.js +0 -1
  20. package/dist/config/store.js +2 -0
  21. package/dist/input/interrupt-gesture.d.ts +18 -0
  22. package/dist/input/interrupt-gesture.js +19 -0
  23. package/dist/input/pointer-router.d.ts +11 -0
  24. package/dist/input/pointer-router.js +14 -0
  25. package/dist/logging/install.d.ts +2 -2
  26. package/dist/logging/install.js +2 -2
  27. package/dist/todo/follow-through.d.ts +0 -4
  28. package/dist/todo/follow-through.js +0 -4
  29. package/dist/todo/limits.d.ts +0 -10
  30. package/dist/todo/limits.js +0 -10
  31. package/dist/todo/prompt.d.ts +0 -15
  32. package/dist/todo/store.d.ts +8 -13
  33. package/dist/todo/store.js +13 -14
  34. package/dist/todo/types.d.ts +2 -1
  35. package/dist/ui/ActivityLine.d.ts +26 -34
  36. package/dist/ui/ActivityLine.js +30 -19
  37. package/dist/ui/App.d.ts +3 -41
  38. package/dist/ui/App.js +223 -247
  39. package/dist/ui/BottomStatusRow.d.ts +1 -3
  40. package/dist/ui/BottomStatusRow.js +4 -8
  41. package/dist/ui/Composer.d.ts +38 -5
  42. package/dist/ui/Composer.js +45 -23
  43. package/dist/ui/Header.d.ts +2 -16
  44. package/dist/ui/Header.js +3 -18
  45. package/dist/ui/Logo.d.ts +1 -17
  46. package/dist/ui/Logo.js +0 -16
  47. package/dist/ui/ModeChip.d.ts +7 -18
  48. package/dist/ui/ModeChip.js +9 -0
  49. package/dist/ui/PromptCaret.d.ts +11 -0
  50. package/dist/ui/PromptCaret.js +31 -0
  51. package/dist/ui/PromptInput.d.ts +10 -33
  52. package/dist/ui/PromptInput.js +37 -20
  53. package/dist/ui/StatusBar.d.ts +1 -11
  54. package/dist/ui/StatusBar.js +14 -8
  55. package/dist/ui/ToastStack.d.ts +2 -18
  56. package/dist/ui/ToastStack.js +4 -11
  57. package/dist/ui/Transcript.d.ts +5 -71
  58. package/dist/ui/Transcript.js +10 -199
  59. package/dist/ui/composer-rows.js +50 -9
  60. package/dist/ui/entries/AssistantEntry.d.ts +0 -23
  61. package/dist/ui/entries/AssistantEntry.js +3 -4
  62. package/dist/ui/entries/DiffView.d.ts +1 -7
  63. package/dist/ui/entries/DiffView.js +2 -2
  64. package/dist/ui/entries/ToolCard.d.ts +0 -6
  65. package/dist/ui/entries/ToolCard.js +4 -13
  66. package/dist/ui/frame-differ.d.ts +13 -0
  67. package/dist/ui/frame-differ.js +37 -24
  68. package/dist/ui/frame-observer.d.ts +17 -0
  69. package/dist/ui/frame-observer.js +81 -0
  70. package/dist/ui/frame-parser.d.ts +7 -0
  71. package/dist/ui/frame-parser.js +18 -0
  72. package/dist/ui/ink-runtime.d.ts +1 -0
  73. package/dist/ui/ink-runtime.js +21 -0
  74. package/dist/ui/layout/AppShell.d.ts +4 -58
  75. package/dist/ui/layout/AppShell.js +3 -6
  76. package/dist/ui/layout/OverlayFrame.d.ts +1 -25
  77. package/dist/ui/layout/OverlayFrame.js +0 -24
  78. package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
  79. package/dist/ui/layout/ScrollIndicator.js +4 -4
  80. package/dist/ui/layout/ScrollViewport.d.ts +16 -66
  81. package/dist/ui/layout/ScrollViewport.js +195 -185
  82. package/dist/ui/layout/budget.d.ts +3 -51
  83. package/dist/ui/layout/budget.js +4 -53
  84. package/dist/ui/layout/document-layout.d.ts +13 -0
  85. package/dist/ui/layout/document-layout.js +11 -0
  86. package/dist/ui/layout/follow-state.d.ts +1 -0
  87. package/dist/ui/layout/follow-state.js +1 -3
  88. package/dist/ui/layout/frame.d.ts +0 -22
  89. package/dist/ui/layout/frame.js +0 -36
  90. package/dist/ui/layout/scroll-indicator.d.ts +1 -1
  91. package/dist/ui/layout/scroll-indicator.js +3 -2
  92. package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
  93. package/dist/ui/layout/scrollbar-geometry.js +13 -0
  94. package/dist/ui/layout/todo-layout.d.ts +2 -3
  95. package/dist/ui/layout/todo-layout.js +9 -9
  96. package/dist/ui/layout/viewport-geometry.d.ts +2 -1
  97. package/dist/ui/layout/viewport-geometry.js +2 -1
  98. package/dist/ui/layout/virtual-window.d.ts +1 -0
  99. package/dist/ui/layout/virtual-window.js +1 -1
  100. package/dist/ui/live-clamp.d.ts +1 -29
  101. package/dist/ui/live-clamp.js +1 -29
  102. package/dist/ui/overlays/HelpOverlay.js +6 -2
  103. package/dist/ui/run-status-row.d.ts +61 -0
  104. package/dist/ui/run-status-row.js +70 -0
  105. package/dist/ui/screen.d.ts +1 -1
  106. package/dist/ui/screen.js +4 -6
  107. package/dist/ui/scrollbar-controller.d.ts +30 -0
  108. package/dist/ui/scrollbar-controller.js +124 -0
  109. package/dist/ui/transcript-text.d.ts +0 -8
  110. package/dist/ui/transcript-text.js +0 -8
  111. package/dist/ui/use-startup-notices.d.ts +1 -57
  112. package/dist/ui/use-startup-notices.js +3 -54
  113. package/dist/ui/use-wheel-routing.d.ts +3 -1
  114. package/dist/ui/use-wheel-routing.js +6 -0
  115. package/package.json +2 -2
  116. package/dist/ui/TodoStrip.d.ts +0 -36
  117. package/dist/ui/TodoStrip.js +0 -32
@@ -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 {};
@@ -115,20 +115,7 @@ export type Entry = {
115
115
  kind: 'notice';
116
116
  level: NoticeLevel;
117
117
  text: string;
118
- }
119
- /**
120
- * A team dispatch (team-subagents §5.3). JSON-SERIALIZABLE by construction, so
121
- * `/save` and `/resume` need no format change.
122
- *
123
- * `active: true` is normalized away ON LOAD, in `session/persist.ts`, not here
124
- * (P1-5). A session saved mid-dispatch would otherwise resume with a card that
125
- * claims to be running while nothing is: it spins forever, and because
126
- * `Transcript`'s settled boundary is MONOTONIC an entry that never settles
127
- * never reaches `<Static>` and is re-rendered on every frame for the rest of
128
- * the session. The reducer never legitimately sees a stale-active entry — only
129
- * the file does — which is why the fix belongs in the load path.
130
- */
131
- | {
118
+ } | {
132
119
  id: string;
133
120
  kind: 'team';
134
121
  dispatchId: string;
@@ -137,20 +124,7 @@ export type Entry = {
137
124
  aborted: boolean;
138
125
  durationMs?: number;
139
126
  active: boolean;
140
- }
141
- /**
142
- * The turn's todo checklist (todo-plan-execution §3.8). ONE per user turn,
143
- * rewritten in place — a 7-item list updated twice per item would otherwise be
144
- * 14 near-identical tool cards in the one surface the user reads most.
145
- *
146
- * JSON-SERIALIZABLE by construction, so `/save` needs no format change.
147
- *
148
- * `live: true` is normalized away ON LOAD, in `session/persist.ts`, for the
149
- * same reason the team entry's `active` is (C-5): `Transcript`'s settled
150
- * boundary is MONOTONIC, so an entry that never settles never reaches
151
- * `<Static>` and is re-rendered on every frame for the rest of the session.
152
- */
153
- | {
127
+ } | {
154
128
  id: string;
155
129
  kind: 'todo';
156
130
  items: TodoItem[];
@@ -165,21 +139,7 @@ export type Entry = {
165
139
  * card this session produced.
166
140
  */
167
141
  interrupted?: boolean;
168
- }
169
- /**
170
- * The turn's retry story (llm-api-retry-backoff §6.4). ONE per turn, REWRITTEN
171
- * IN PLACE — ten retries rendered as ten notices would bury the transcript in
172
- * the one moment the user most needs it legible.
173
- *
174
- * JSON-SERIALIZABLE by construction, so `/save` needs no format change.
175
- *
176
- * `phase: 'waiting' | 'retrying'` is normalized away ON LOAD, in
177
- * `session/persist.ts`, exactly as the team entry's `active` and the todo
178
- * card's `live` are (R-10): `Transcript`'s settled boundary is MONOTONIC, so a
179
- * card that never settles never reaches `<Static>` and is re-rendered on every
180
- * frame for the rest of the session.
181
- */
182
- | {
142
+ } | {
183
143
  id: string;
184
144
  kind: 'retry';
185
145
  /** 1-based retry index of the LATEST attempt this card has seen. */
@@ -195,22 +155,7 @@ export type Entry = {
195
155
  /** Filled on settle. */
196
156
  totalRetries?: number;
197
157
  elapsedMs?: number;
198
- }
199
- /**
200
- * One fast-model review (fast-model-tier §5.4). ONE CARD PER REVIEW, never
201
- * rewritten across reviews — a critique is a single event, unlike the todo
202
- * card's per-turn checklist.
203
- *
204
- * JSON-SERIALIZABLE by construction, so `/save` needs no format change.
205
- *
206
- * `live: true` is normalized away ON LOAD, in `session/persist.ts`, next to
207
- * `team.active`, `todo.live` and the retry card's phase, and for the identical
208
- * reason (C-5): `Transcript`'s settled boundary is MONOTONIC, so an entry that
209
- * never settles never reaches `<Static>` and is re-rendered on every frame for
210
- * the rest of the session. The reducer never legitimately sees a stale-live
211
- * entry — only the file does.
212
- */
213
- | {
158
+ } | {
214
159
  id: string;
215
160
  kind: 'fast';
216
161
  /** 1-based within the session, so the card can name itself. */
@@ -224,27 +169,7 @@ export type Entry = {
224
169
  turn: number;
225
170
  durationMs?: number;
226
171
  live: boolean;
227
- }
228
- /**
229
- * One context compaction (context-auto-compaction §5.5). ONE CARD PER
230
- * COMPACTION, rewritten once when the call returns — like the fast card, and
231
- * unlike the todo card's per-turn checklist.
232
- *
233
- * JSON-SERIALIZABLE by construction, so `/save` and `/resume` need no format
234
- * change.
235
- *
236
- * `live: true` is normalized away ON LOAD, in `session/persist.ts`, next to
237
- * `team.active`, `todo.live`, the retry card's phase and `fast.live`, and for
238
- * the identical reason (C-8): `Transcript`'s settled boundary is MONOTONIC, so
239
- * an entry that never settles never reaches `<Static>` and is re-rendered on
240
- * every frame for the rest of the session.
241
- *
242
- * EVERY STRING ON THIS CARD DESCRIBES THE HISTORY, NEVER THE SCREEN (P2-9).
243
- * `/clear` empties the transcript view but not `messages`, so a compaction can
244
- * legitimately fire against a visually empty transcript and report
245
- * "112 -> 9 messages" with nine visible rows.
246
- */
247
- | {
172
+ } | {
248
173
  id: string;
249
174
  kind: 'compaction';
250
175
  /** 1-based within the session, so the card can name itself. */
@@ -275,29 +200,7 @@ export type Entry = {
275
200
  charsRemoved: number;
276
201
  };
277
202
  live: boolean;
278
- }
279
- /**
280
- * One supervised background service (background-service-supervision §3.10).
281
- *
282
- * JSON-SERIALIZABLE by construction, so `/save` needs no format change.
283
- *
284
- * IT IS THE ONE LIVE CARD THAT DOES **NOT** BLOCK THE SETTLED BOUNDARY ONCE IT
285
- * IS UP (D-11 / P0-4). Every other live clause in
286
- * `Transcript.tsx::computeSettledCount` is bounded by an OPERATION — a
287
- * dispatch, a turn, one LLM call, one countdown — and each carries a comment
288
- * saying that an entry which never settles is re-rendered on every frame for
289
- * the rest of the session. A service is bounded by the USER'S INTENT: a dev
290
- * server left up for an hour is the normal case. So only `starting` blocks
291
- * (bounded by `readyTimeoutMs`), a `ready`/`running` card commits its body to
292
- * `<Static>`, and a later terminal transition appends a NEW one-row entry
293
- * rather than trying to rewrite a card `<Static>` has already printed.
294
- *
295
- * `terminal: true` marks that second entry. `session/persist.ts` rewrites any
296
- * restored non-terminal card to `stopped`, next to `team.active`, `todo.live`,
297
- * the retry phase, `fast.live` and `compaction.live`, and for the identical
298
- * reason.
299
- */
300
- | {
203
+ } | {
301
204
  id: string;
302
205
  kind: 'service';
303
206
  /** `s1`, `s2`, ... — the id the model quotes back. */
@@ -677,16 +580,7 @@ export type ViewAction = {
677
580
  } | {
678
581
  type: 'serviceUpdate';
679
582
  service: ServiceSnapshot;
680
- }
681
- /**
682
- * A service reached a terminal state.
683
- *
684
- * IT APPENDS A NEW ONE-ROW ENTRY rather than rewriting the card (D-11). The
685
- * card may already have been printed into `<Static>`, and `<Static>` cannot
686
- * un-print — so any design that rewrites a printed card is wrong whatever it
687
- * claims. A second event at a second time is also simply more honest.
688
- */
689
- | {
583
+ } | {
690
584
  type: 'serviceEnd';
691
585
  service: ServiceSnapshot;
692
586
  };
@@ -382,31 +382,6 @@ function settleRetryCard(state, entries, phase) {
382
382
  retryEntryId: undefined,
383
383
  };
384
384
  }
385
- /**
386
- * EVERY PATH THAT REACHES `status: 'idle'` RELEASES THE LIVE TAIL (D-35 / AC-40).
387
- *
388
- * `runEnd` finalizes only the streaming ASSISTANT entry and `abortMark` maps only
389
- * assistant entries, so neither touches a tool entry stranded at
390
- * `status: 'running'` -- the engine's own idle watchdog, a provider error mid-call
391
- * and `Esc` all produce one. Today that costs a single spinner row. With a tail
392
- * attached it costs a permanent multi-row card whose stall counter is frozen at
393
- * its last value, because the 200 ms ticker that feeds `nowSec` is torn down the
394
- * instant `state.status` leaves `running` -- and, worse, `computeSettledCount`
395
- * breaks on a non-settled tool entry, so that card pins `Transcript`'s MONOTONIC
396
- * settled boundary and re-renders the tail every frame for the rest of the
397
- * session. The rule and the stakes are `settleRetryCard`'s, verbatim.
398
- *
399
- * THE STATUS IS DELIBERATELY NOT CHANGED. That is a different decision with
400
- * different consequences for `bashBadge` and for the run's own bookkeeping, and
401
- * it is not this round's to make. Only the tail is released.
402
- *
403
- * `liveSeq` is BUMPED rather than cleared, and that is not bookkeeping: clearing
404
- * `live` is a non-append mutation, and with the status untouched nothing else in
405
- * the tool revision moves -- so without the bump the height cache would keep
406
- * serving the multi-row measurement for a card that now draws one row (I-L3-1).
407
- * Entries with no tail are returned by identity, which is what keeps
408
- * `EntryView`'s memo boundary intact for the rest of the transcript.
409
- */
410
385
  function releaseLiveTails(entries) {
411
386
  let touched = false;
412
387
  const next = entries.map((e) => {
@@ -1218,22 +1193,6 @@ export function viewReducer(state, action) {
1218
1193
  entries: mapEntry(state.entries, known, (e) => e.kind === 'retry' ? { ...e, phase: 'retrying', resumeAt: undefined } : e),
1219
1194
  };
1220
1195
  }
1221
- /**
1222
- * Rewind the view for a mid-stream restart.
1223
- *
1224
- * THE ONLY ACTION IN THIS REDUCER THAT SHRINKS `entries`, and it is safe for
1225
- * one specific reason (§6.4 / AC-22). `Transcript` clamps its settled boundary
1226
- * with a MONOTONIC high-water mark: if `highWater` ever exceeded
1227
- * `entries.length` the clamp would mark EVERYTHING settled — including the
1228
- * still-streaming assistant entry — and `<Static>` would duplicate it.
1229
- *
1230
- * It cannot, and the ORDERING is why: the entries removed here are
1231
- * `kind: 'tool'` cards created by `toolCallStart` AFTER `turnStart` created the
1232
- * streaming assistant entry, so they sit strictly above it in the array; and
1233
- * the boundary scan already breaks at that entry. Removing only entries above
1234
- * the boundary is the invariant. A future action that removed a SETTLED entry
1235
- * would break `<Static>` and nothing would report it.
1236
- */
1237
1196
  case 'streamRestart': {
1238
1197
  const discarded = new Set(action.discardedToolCallIds);
1239
1198
  const entries = state.entries.filter((e) => !(e.kind === 'tool' && discarded.has(e.toolCallId)));
@@ -1326,18 +1285,6 @@ export function viewReducer(state, action) {
1326
1285
  droppedEntries: appended.droppedEntries,
1327
1286
  };
1328
1287
  }
1329
- /**
1330
- * Rewrite the LIVE card in place.
1331
- *
1332
- * SAFE ONLY BECAUSE OF D-11'S OTHER HALF: a `ready` / `running` card has
1333
- * reached `<Static>` and cannot be re-printed, so `serviceEnd` appends a new
1334
- * entry instead of routing through here. What this handles is the window in
1335
- * which the card is still in the live region — `starting`, and the one
1336
- * transition out of it — plus the output tail while it is there.
1337
- *
1338
- * The lookup is by `serviceId` and SKIPS terminal entries, so a stray update
1339
- * cannot rewrite the one-row record `serviceEnd` appended.
1340
- */
1341
1288
  case 'serviceUpdate': {
1342
1289
  const target = findServiceEntryId(state, action.service.id);
1343
1290
  if (!target)
@@ -1347,15 +1294,6 @@ export function viewReducer(state, action) {
1347
1294
  entries: mapEntry(state.entries, target, (e) => e.kind === 'service' ? { ...e, ...serviceFields(action.service) } : e),
1348
1295
  };
1349
1296
  }
1350
- /**
1351
- * A service reached a terminal state.
1352
- *
1353
- * TWO WRITES, AND BOTH ARE REQUIRED. The live card is settled in place so
1354
- * `computeSettledCount` can move past it and `estimateEntryRows` stops
1355
- * charging for a tail nothing will add to; the NEW one-row entry is what the
1356
- * user actually reads, because the card may already be printed into
1357
- * `<Static>` and `<Static>` cannot un-print (D-11 / R-13).
1358
- */
1359
1297
  case 'serviceEnd': {
1360
1298
  const target = findServiceEntryId(state, action.service.id);
1361
1299
  const settled = target