@aragon-agent/cli 0.6.8 → 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 (78) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +38 -37
  3. package/dist/agent/controller.d.ts +1 -1
  4. package/dist/agent/reducer.d.ts +7 -113
  5. package/dist/agent/reducer.js +0 -62
  6. package/dist/cli.js +48 -149
  7. package/dist/commands/perf.d.ts +32 -2
  8. package/dist/commands/perf.js +32 -2
  9. package/dist/commands/registry.d.ts +2 -2
  10. package/dist/config/env.js +0 -130
  11. package/dist/config/load.d.ts +0 -2
  12. package/dist/config/load.js +2 -31
  13. package/dist/config/schema.d.ts +1 -14
  14. package/dist/config/schema.js +0 -1
  15. package/dist/config/store.js +2 -0
  16. package/dist/logging/install.d.ts +2 -2
  17. package/dist/logging/install.js +2 -2
  18. package/dist/todo/follow-through.d.ts +0 -4
  19. package/dist/todo/follow-through.js +0 -4
  20. package/dist/todo/limits.d.ts +0 -10
  21. package/dist/todo/limits.js +0 -10
  22. package/dist/todo/prompt.d.ts +0 -15
  23. package/dist/ui/ActivityLine.d.ts +26 -34
  24. package/dist/ui/ActivityLine.js +30 -19
  25. package/dist/ui/App.d.ts +1 -41
  26. package/dist/ui/App.js +66 -128
  27. package/dist/ui/BottomStatusRow.d.ts +1 -3
  28. package/dist/ui/BottomStatusRow.js +4 -8
  29. package/dist/ui/Composer.d.ts +28 -1
  30. package/dist/ui/Composer.js +43 -17
  31. package/dist/ui/Header.d.ts +2 -16
  32. package/dist/ui/Header.js +3 -18
  33. package/dist/ui/Logo.d.ts +1 -17
  34. package/dist/ui/Logo.js +0 -16
  35. package/dist/ui/ModeChip.d.ts +7 -18
  36. package/dist/ui/ModeChip.js +9 -0
  37. package/dist/ui/PromptInput.d.ts +2 -28
  38. package/dist/ui/PromptInput.js +4 -7
  39. package/dist/ui/StatusBar.d.ts +0 -11
  40. package/dist/ui/ToastStack.d.ts +2 -18
  41. package/dist/ui/ToastStack.js +4 -11
  42. package/dist/ui/Transcript.d.ts +5 -71
  43. package/dist/ui/Transcript.js +9 -199
  44. package/dist/ui/entries/AssistantEntry.d.ts +0 -23
  45. package/dist/ui/entries/AssistantEntry.js +3 -4
  46. package/dist/ui/entries/DiffView.d.ts +1 -7
  47. package/dist/ui/entries/DiffView.js +2 -2
  48. package/dist/ui/entries/ToolCard.d.ts +0 -6
  49. package/dist/ui/entries/ToolCard.js +4 -13
  50. package/dist/ui/frame-differ.d.ts +13 -0
  51. package/dist/ui/frame-differ.js +36 -6
  52. package/dist/ui/ink-runtime.d.ts +1 -0
  53. package/dist/ui/ink-runtime.js +21 -0
  54. package/dist/ui/layout/AppShell.d.ts +1 -10
  55. package/dist/ui/layout/AppShell.js +2 -5
  56. package/dist/ui/layout/OverlayFrame.d.ts +1 -25
  57. package/dist/ui/layout/OverlayFrame.js +0 -24
  58. package/dist/ui/layout/ScrollViewport.d.ts +3 -0
  59. package/dist/ui/layout/ScrollViewport.js +17 -5
  60. package/dist/ui/layout/frame.d.ts +0 -22
  61. package/dist/ui/layout/frame.js +0 -36
  62. package/dist/ui/layout/todo-layout.d.ts +1 -3
  63. package/dist/ui/layout/todo-layout.js +7 -8
  64. package/dist/ui/layout/viewport-geometry.d.ts +1 -1
  65. package/dist/ui/layout/viewport-geometry.js +1 -1
  66. package/dist/ui/live-clamp.d.ts +1 -29
  67. package/dist/ui/live-clamp.js +1 -29
  68. package/dist/ui/overlays/HelpOverlay.js +1 -0
  69. package/dist/ui/run-status-row.d.ts +61 -0
  70. package/dist/ui/run-status-row.js +70 -0
  71. package/dist/ui/transcript-text.d.ts +0 -8
  72. package/dist/ui/transcript-text.js +0 -8
  73. package/dist/ui/use-startup-notices.d.ts +1 -57
  74. package/dist/ui/use-startup-notices.js +3 -54
  75. package/dist/ui/use-wheel-routing.d.ts +1 -1
  76. package/package.json +2 -2
  77. package/dist/ui/TodoStrip.d.ts +0 -36
  78. package/dist/ui/TodoStrip.js +0 -32
@@ -77,8 +77,6 @@ export function readEnvConfig() {
77
77
  if (process.env.ARAGON_THINKING) {
78
78
  partial.thinkingLevel = clampThinkingLevel(process.env.ARAGON_THINKING.trim(), 'off');
79
79
  }
80
- // Parsed by the POSITIVE list, like ARAGON_FULLSCREEN: anything else is left
81
- // ABSENT so the config file underneath still wins.
82
80
  const showThinking = process.env.ARAGON_SHOW_THINKING?.trim().toLowerCase();
83
81
  if (showThinking !== undefined && showThinking.length > 0) {
84
82
  partial.showThinking =
@@ -130,16 +128,7 @@ export function readEnvConfig() {
130
128
  if (process.env.ARAGON_THEME) {
131
129
  partial.theme = clampTheme(process.env.ARAGON_THEME.trim(), 'auto');
132
130
  }
133
- // ARAGON_FULLSCREEN=0|1 — the one channel that works where neither a flag nor
134
131
  // a config file is reachable (containers, SSH, being spawned by another tool).
135
- const fullscreen = process.env.ARAGON_FULLSCREEN?.trim().toLowerCase();
136
- if (fullscreen !== undefined && fullscreen.length > 0) {
137
- partial.fullscreen = fullscreen === '1' || fullscreen === 'true' || fullscreen === 'on' || fullscreen === 'yes';
138
- }
139
- // ARAGON_MOUSE=0 turns off wheel region routing. Parsed by the POSITIVE list
140
- // above, exactly as ARAGON_FULLSCREEN is — NOT by `envBool` below, which uses
141
- // a negative list. The two disagree on `ARAGON_MOUSE=disable`, and `mouse` is
142
- // modelled on `--fullscreen`, so it uses the reader `--fullscreen` uses.
143
132
  const mouse = process.env.ARAGON_MOUSE?.trim().toLowerCase();
144
133
  if (mouse !== undefined && mouse.length > 0) {
145
134
  partial.mouse = mouse === '1' || mouse === 'true' || mouse === 'on' || mouse === 'yes';
@@ -174,8 +163,6 @@ export function readEnvConfig() {
174
163
  if (Number.isFinite(n) && n >= 0)
175
164
  partial.scrollResumeMs = n;
176
165
  }
177
- // ARAGON_PLAN=1 starts the session in PLAN mode — the same channel story as
178
- // ARAGON_FULLSCREEN, for terminals reached through a wrapper that owns argv.
179
166
  const plan = process.env.ARAGON_PLAN?.trim().toLowerCase();
180
167
  if (plan !== undefined && plan.length > 0) {
181
168
  partial.planModeDefault = plan === '1' || plan === 'true' || plan === 'on' || plan === 'yes';
@@ -191,12 +178,6 @@ export function readEnvConfig() {
191
178
  enabled: !(skills === '0' || skills === 'false' || skills === 'off' || skills === 'no'),
192
179
  };
193
180
  }
194
- // ARAGON_TEAM=0 turns team mode off; ARAGON_TEAM_MAX=<n> overrides the fan-out
195
- // width. Parsed by the POSITIVE list, exactly as ARAGON_FULLSCREEN /
196
- // ARAGON_MOUSE / ARAGON_PLAN are — NOT by `envBool` below, which uses a
197
- // negative list. The two readers disagree on `ARAGON_TEAM=disable`, and the
198
- // documented-but-dead outcome of copying the wrong one is what `load.ts`
199
- // records for `ARAGON_MOUSE`.
200
181
  const team = process.env.ARAGON_TEAM?.trim().toLowerCase();
201
182
  const teamMax = process.env.ARAGON_TEAM_MAX?.trim();
202
183
  if ((team !== undefined && team.length > 0) || (teamMax !== undefined && teamMax.length > 0)) {
@@ -217,27 +198,6 @@ export function readEnvConfig() {
217
198
  if (Object.keys(section).length > 0)
218
199
  partial.team = section;
219
200
  }
220
- // ARAGON_TODO=0 turns todo planning off entirely (no tool, no rail). Parsed by
221
- // the POSITIVE list, exactly as ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN
222
- // / ARAGON_TEAM are — NOT by `envBool` below, which uses a negative list. The
223
- // two readers disagree on values like `ARAGON_TODO=disable`, and this file
224
- // already records that once for team mode.
225
- //
226
- // There is no `ARAGON_TODO_PANEL`: the panel is a per-machine display
227
- // preference, which is what `config.json` is for, and every env var here
228
- // exists because a flag and a config file are BOTH unreachable in the channel
229
- // it serves (containers, SSH, being spawned by another tool).
230
- //
231
- // ARAGON_TODO_FOLLOW=notify|auto|off PASSES THAT SAME TEST (D-18), which is
232
- // why it exists where `ARAGON_TODO_PANEL` does not: follow-through is exactly
233
- // the knob an unattended `-p` run in a container with a fixed entrypoint needs
234
- // and can reach no other way.
235
- //
236
- // ONE ACCUMULATED SECTION, ASSIGNED ONCE — the shape the `team` branch above
237
- // already uses, and NOT two `if` blocks each assigning `partial.todo` (P1-4).
238
- // THE CAST IS WHAT WOULD MAKE THAT MISTAKE SILENT: a second assignment
239
- // compiles clean and drops `enabled`, so `ARAGON_TODO=0 ARAGON_TODO_FOLLOW=auto`
240
- // would quietly re-enable the tool the user turned off.
241
201
  const todo = process.env.ARAGON_TODO?.trim().toLowerCase();
242
202
  const todoFollow = process.env.ARAGON_TODO_FOLLOW?.trim().toLowerCase();
243
203
  if ((todo !== undefined && todo.length > 0) ||
@@ -260,24 +220,6 @@ export function readEnvConfig() {
260
220
  if (Object.keys(section).length > 0)
261
221
  partial.todo = section;
262
222
  }
263
- // ARAGON_BASH_BACKGROUND=0 turns background services off entirely (no
264
- // `background` param, no `bash_output`/`bash_kill`, no prompt block, no chip);
265
- // ARAGON_BASH_AUTO_BACKGROUND=0 keeps them but stops the classifier firing on
266
- // an unset `background`. Parsed by the POSITIVE list, exactly as
267
- // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO
268
- // are - NOT by `envBool` below, which uses a negative list.
269
- //
270
- // Both pass the test every env var in this file has to pass: an unattended run
271
- // in a container with a fixed entrypoint can reach neither a flag nor a config
272
- // file, and "do not leave a dev server running in my CI job" is exactly the
273
- // kind of decision that channel needs to make.
274
- //
275
- // ONE ACCUMULATED SECTION, ASSIGNED ONCE - the shape the `team` and `todo`
276
- // branches above already use, and NOT two `if` blocks each assigning
277
- // `partial.bash` (the `todo` P1-4 trap). THE CAST IS WHAT WOULD MAKE THAT
278
- // MISTAKE SILENT: a second assignment compiles clean and drops the first key,
279
- // so `ARAGON_BASH_BACKGROUND=0 ARAGON_BASH_AUTO_BACKGROUND=0` would quietly
280
- // re-register the tools the user turned off.
281
223
  const bashBackground = process.env.ARAGON_BASH_BACKGROUND?.trim().toLowerCase();
282
224
  const bashAuto = process.env.ARAGON_BASH_AUTO_BACKGROUND?.trim().toLowerCase();
283
225
  if ((bashBackground !== undefined && bashBackground.length > 0) ||
@@ -300,22 +242,6 @@ export function readEnvConfig() {
300
242
  if (Object.keys(section).length > 0)
301
243
  partial.bash = section;
302
244
  }
303
- // ARAGON_RETRY=0 is the kill switch for API retry; ARAGON_RETRY_MAX=<n>
304
- // overrides the count. Parsed by the POSITIVE list, exactly as
305
- // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO
306
- // are — NOT by `envBool` below, which uses a negative list.
307
- //
308
- // TWO VARS ONLY, and no third for the delays: every env var in this file exists
309
- // because a flag and a config file are BOTH unreachable in the channel it
310
- // serves, and nobody tunes a backoff multiplier from a container entrypoint.
311
- //
312
- // `>= 0`, NOT `> 0`, and the difference is the whole point (R-16).
313
- // `ARAGON_TEAM_MAX` above guards `parsed > 0`, which is right for a fan-out
314
- // width and wrong here: `0` is this key's documented floor AND its kill switch,
315
- // so a `> 0` guard would drop `ARAGON_RETRY_MAX=0` on the floor and leave the
316
- // user with the default 10. "Left ABSENT when unparseable" still holds and
317
- // still matters — an unparseable value must not look permanently supplied, or
318
- // the config file can never win again.
319
245
  const retry = process.env.ARAGON_RETRY?.trim().toLowerCase();
320
246
  const retryMax = process.env.ARAGON_RETRY_MAX?.trim();
321
247
  if ((retry !== undefined && retry.length > 0) || (retryMax !== undefined && retryMax.length > 0)) {
@@ -333,26 +259,6 @@ export function readEnvConfig() {
333
259
  if (Object.keys(section).length > 0)
334
260
  partial.retry = section;
335
261
  }
336
- // ARAGON_FAST=1 turns the fast model tier on; the other three name it
337
- // (fast-model-tier §4.3). Parsed by the POSITIVE list, exactly as
338
- // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO /
339
- // ARAGON_RETRY are — NOT by `envBool` below, whose NEGATIVE list disagrees with
340
- // it on values like `ARAGON_FAST=disable`.
341
- //
342
- // FOUR VARS, AND DELIBERATELY NO `ARAGON_FAST_REVIEW_EVERY` / `_MAX_CHARS` /
343
- // `_DELEGATE` (D-14). The rule this file states is that an environment variable
344
- // exists only where a flag AND a config file are BOTH unreachable — a
345
- // container, an ssh session, being spawned by another tool. "Which model, and
346
- // is it on" passes that test; tuning a review cadence does not, and every key
347
- // that fails it is documentation debt plus a fifth place for the value to
348
- // disagree with itself.
349
- //
350
- // ONE ACCUMULATED SECTION, ASSIGNED ONCE — the shape the `team`, `todo` and
351
- // `retry` branches above all use, and NOT four `if` blocks each assigning
352
- // `partial.fast`. THE CAST IS WHAT WOULD MAKE THAT MISTAKE SILENT: a second
353
- // assignment compiles clean and drops `enabled`, so
354
- // `ARAGON_FAST=1 ARAGON_FAST_MODEL=x` would quietly resolve with the tier off
355
- // (the P1-4 defect this file records for `todo`).
356
262
  const fast = process.env.ARAGON_FAST?.trim().toLowerCase();
357
263
  const fastProvider = process.env.ARAGON_FAST_PROVIDER?.trim();
358
264
  const fastModel = process.env.ARAGON_FAST_MODEL?.trim();
@@ -376,25 +282,6 @@ export function readEnvConfig() {
376
282
  if (Object.keys(section).length > 0)
377
283
  partial.fast = section;
378
284
  }
379
- // ARAGON_COMPACTION=0 turns context compaction off; the other two tune it
380
- // (context-auto-compaction §4.3). Parsed by the POSITIVE list, exactly as
381
- // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO /
382
- // ARAGON_RETRY / ARAGON_FAST are — NOT by `envBool` below, whose NEGATIVE list
383
- // disagrees with it on values like `ARAGON_COMPACTION=disable`.
384
- //
385
- // THREE VARS, AND DELIBERATELY NO `ARAGON_COMPACTION_ON_FAILURE` /
386
- // `_USE_FAST_TIER` / `_WARN_THRESHOLD`. The rule this file states is that an
387
- // environment variable exists only where a flag AND a config file are BOTH
388
- // unreachable — a container, an ssh session, being spawned by another tool.
389
- // "Is it on, when does it fire, and how much does it keep" passes that test;
390
- // "what should it do if the summarizer 500s" does not, and every key that fails
391
- // it is documentation debt plus a fifth place for the value to disagree with
392
- // itself.
393
- //
394
- // ONE ACCUMULATED SECTION, ASSIGNED ONCE — the shape the four branches above
395
- // all use, and NOT three `if` blocks each assigning `partial.compaction`. THE
396
- // CAST IS WHAT WOULD MAKE THAT MISTAKE SILENT: a second assignment compiles
397
- // clean and drops `enabled`.
398
285
  const compaction = process.env.ARAGON_COMPACTION?.trim().toLowerCase();
399
286
  const compactionThreshold = process.env.ARAGON_COMPACTION_THRESHOLD?.trim();
400
287
  const compactionKeepTurns = process.env.ARAGON_COMPACTION_KEEP_TURNS?.trim();
@@ -471,23 +358,12 @@ export function readEnvConfig() {
471
358
  partial.update = { mode };
472
359
  }
473
360
  }
474
- // Render bounds (tui-render-performance §5.3). Same channel story as
475
- // ARAGON_FULLSCREEN: a container or a wrapper that owns argv can reach neither
476
- // a flag nor a config file, and a session that freezes is exactly the case
477
- // where the user needs a knob they can turn from the outside.
478
- //
479
- // ARAGON_TRANSCRIPT_RETAIN / ARAGON_MAX_RENDER_INTERVAL_MS are left ABSENT
480
- // when unparseable rather than written as a default, so the config file can
481
- // still win — the rule `ARAGON_TEAM_MAX` above records.
482
361
  const retain = process.env.ARAGON_TRANSCRIPT_RETAIN?.trim();
483
362
  if (retain !== undefined && retain.length > 0) {
484
363
  const parsed = Number.parseInt(retain, 10);
485
364
  if (Number.isFinite(parsed) && parsed > 0)
486
365
  partial.transcriptRetain = parsed;
487
366
  }
488
- // Parsed by the POSITIVE list, exactly as ARAGON_FULLSCREEN / ARAGON_MOUSE /
489
- // ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO are — NOT by `envBool` below, which
490
- // uses a negative list. The two readers disagree on `ARAGON_RENDER_GOVERNOR=disable`.
491
367
  const governor = process.env.ARAGON_RENDER_GOVERNOR?.trim().toLowerCase();
492
368
  if (governor !== undefined && governor.length > 0) {
493
369
  partial.renderGovernor =
@@ -499,12 +375,6 @@ export function readEnvConfig() {
499
375
  if (Number.isFinite(parsed) && parsed > 0)
500
376
  partial.maxRenderIntervalMs = parsed;
501
377
  }
502
- // ARAGON_DIFF_RENDER=0 restores the pre-fix full-frame repaint;
503
- // ARAGON_SYNC_OUTPUT=0 drops the DEC 2026 envelope (tui-input-flicker-fix
504
- // §5.3). Parsed by the POSITIVE list, exactly as ARAGON_FULLSCREEN /
505
- // ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO / ARAGON_RENDER_GOVERNOR
506
- // are — NOT by `envBool` below, whose NEGATIVE list disagrees with it on values
507
- // like `ARAGON_DIFF_RENDER=disable`.
508
378
  const diffRender = process.env.ARAGON_DIFF_RENDER?.trim().toLowerCase();
509
379
  if (diffRender !== undefined && diffRender.length > 0) {
510
380
  partial.diffRender =
@@ -19,8 +19,6 @@ export interface CliFlags {
19
19
  confirm?: boolean;
20
20
  toolTimeout?: string | number;
21
21
  idleTimeout?: string | number;
22
- /** `--fullscreen` ⇒ true, `--no-fullscreen` ⇒ false, absent ⇒ undefined. */
23
- fullscreen?: boolean;
24
22
  /** `--no-exit-transcript` ⇒ false. */
25
23
  exitTranscript?: boolean;
26
24
  /** `--compact` ⇒ true, `--no-compact` ⇒ false, absent ⇒ undefined. */
@@ -23,32 +23,11 @@ function envFlagTrue(value) {
23
23
  const v = value.trim().toLowerCase();
24
24
  return v === '1' || v === 'true' || v === 'on' || v === 'yes';
25
25
  }
26
- /**
27
- * Resolve the tri-state full-screen preference (see `CliConfig.fullscreen`).
28
- * Only a flag or the env var counts as an explicit FORCE; a config file holding
29
- * the default `true` must not disable the automatic downgrades.
30
- */
31
- function resolveFullscreen(flags, env, file) {
32
- if (flags.fullscreen !== undefined)
33
- return flags.fullscreen;
34
- if (env.fullscreen !== undefined)
35
- return env.fullscreen;
36
- if (file.fullscreen === false)
37
- return false;
38
- return undefined;
39
- }
40
26
  /**
41
27
  * Resolve wheel region routing: flag › env › file › default `true`.
42
28
  *
43
- * THIS NEEDS ITS OWN RESOLVER; it does not "match every other boolean" in this
44
- * file. `hints` is `flags.hints !== undefined ? … : file.hints ?? DEFAULT` and
45
- * reads NO ENV AT ALL — copying that line would ship `ARAGON_MOUSE` as
46
- * documented-but-dead (P1-9). Only `fullscreen` has an env layer, and only
47
- * because `resolveFullscreen` above was written to give it one.
48
- *
49
- * Unlike `fullscreen` this is a plain boolean rather than a tri-state: there
50
- * are no heuristics for an explicit `true` to override, so a persisted `true`
51
- * is just a value and not an opinion that has to be told apart from silence.
29
+ * Unlike `hints`, mouse routing accepts an environment override. Keep that
30
+ * layer explicit so ARAGON_MOUSE works even when a wrapper owns the arguments.
52
31
  */
53
32
  function resolveMouse(flags, env, file) {
54
33
  if (flags.mouse !== undefined)
@@ -463,12 +442,6 @@ export function loadConfig(flags = {}) {
463
442
  const read = readConfigFile();
464
443
  const file = read.config ?? {};
465
444
  const env = readEnvConfig();
466
- // A config file that exists but will not parse used to reset EVERY setting
467
- // with nothing anywhere explaining it. The fallback is unchanged; what is new
468
- // is that it leaves a trace. The record alone is not the explanation, so the
469
- // two owners of a user-visible channel say it too: `main()` writes one stderr
470
- // line (headless, inline and every subcommand) and `App.tsx` raises a toast
471
- // (full-screen, where the alternate screen wipes that line away).
472
445
  if (read.parseError) {
473
446
  getLogger().error('config', 'config_parse_failed', { error: read.parseError });
474
447
  }
@@ -580,7 +553,6 @@ export function loadConfig(flags = {}) {
580
553
  contextWindow,
581
554
  theme,
582
555
  reducedMotion,
583
- fullscreen: resolveFullscreen(flags, env.partial, file),
584
556
  exitTranscript: flags.exitTranscript !== undefined
585
557
  ? flags.exitTranscript
586
558
  : file.exitTranscript ?? DEFAULT_CONFIG.exitTranscript,
@@ -658,7 +630,6 @@ function recordResolvedConfig(config) {
658
630
  // "why is my output truncated" was unanswerable after the fact.
659
631
  maxTokens: config.maxTokens ?? 'auto',
660
632
  theme: config.theme,
661
- fullscreen: config.fullscreen,
662
633
  toolTimeoutMs: config.toolTimeoutMs,
663
634
  idleTimeoutMs: config.idleTimeoutMs,
664
635
  historyEnabled: config.historyEnabled,
@@ -729,7 +729,6 @@ export interface PersistedConfig {
729
729
  /** Replace spinners with a static glyph for calmer, low-motion output. */
730
730
  reducedMotion: boolean;
731
731
  /** Full-screen TUI (still subject to the automatic downgrades in §4.1). */
732
- fullscreen: boolean;
733
732
  /** Replay a plain-text session summary after leaving the alternate screen. */
734
733
  exitTranscript: boolean;
735
734
  /** Entries reachable by scrolling; older ones show the "collapsed" line (I-3). */
@@ -767,8 +766,7 @@ export interface PersistedConfig {
767
766
  * transcript, whatever row the pointer is on (wheel-scrolls-transcript-only
768
767
  * §4.1).
769
768
  *
770
- * A PLAIN BOOLEAN, not a `fullscreen`-style tri-state: there are no
771
- * heuristics for an explicit `true` to override (D-10).
769
+ * A plain boolean: persisted values and per-run overrides use the same meaning.
772
770
  */
773
771
  mouse: boolean;
774
772
  /**
@@ -881,17 +879,6 @@ export interface CliConfig {
881
879
  contextWindow: number | null;
882
880
  theme: ThemeName;
883
881
  reducedMotion: boolean;
884
- /**
885
- * TRI-STATE, and deliberately so (spec §4.1 vs §6.2):
886
- * `false` — an explicit opt-out from any layer; always inline.
887
- * `true` — an explicit FORCE, only from `--fullscreen` / `ARAGON_FULLSCREEN=1`;
888
- * overrides the TERM/CI/size heuristics.
889
- * `undefined` — no explicit choice (including a config file holding the
890
- * default `true`); full-screen with the heuristics live.
891
- * Folding the persisted default into `true` would turn every user's config
892
- * file into a permanent "ignore the CI and TERM=dumb downgrades" switch.
893
- */
894
- fullscreen?: boolean;
895
882
  /** Replay a plain-text summary on exit (full-screen only — see §4.4). */
896
883
  exitTranscript: boolean;
897
884
  /** Entries reachable by scrolling; older ones show the "collapsed" line (I-3). */
@@ -879,7 +879,6 @@ export const DEFAULT_CONFIG = {
879
879
  contextWindow: null,
880
880
  theme: 'auto',
881
881
  reducedMotion: false,
882
- fullscreen: true,
883
882
  exitTranscript: true,
884
883
  transcriptWindow: DEFAULT_TRANSCRIPT_WINDOW,
885
884
  transcriptRetain: DEFAULT_TRANSCRIPT_RETAIN,
@@ -69,6 +69,8 @@ export function readConfigFile() {
69
69
  const raw = readFileSync(file, 'utf-8');
70
70
  const parsed = JSON.parse(raw);
71
71
  if (parsed && typeof parsed === 'object') {
72
+ // Retired layout preference: tolerate old files without restoring a UI mode.
73
+ delete parsed.fullscreen;
72
74
  return { config: parsed };
73
75
  }
74
76
  return { config: null, parseError: `${file} does not contain a JSON object` };
@@ -25,7 +25,7 @@
25
25
  * Adding a listener suppresses it, turning a crash into a silent hang.
26
26
  *
27
27
  * `SIGINT` / `SIGTERM` / `SIGHUP` — the handlers in `runInteractive()` exist
28
- * ONLY in its full-screen branch, so inline mode, `-p`, and every
28
+ * ONLY in its full-screen branch, so `-p` and every
29
29
  * subcommand had no handler at all and took Node's default termination,
30
30
  * which does not run `'exit'` listeners: the whole queue was lost. But a
31
31
  * signal listener registered here runs BEFORE the one `runInteractive`
@@ -45,7 +45,7 @@ import { Logger, type LogScope } from './logger.js';
45
45
  export declare function setScreenRestore(fn: () => void): void;
46
46
  /**
47
47
  * Replace what a signal does after the flush. The default (a bare
48
- * `exit(128 + signo)`) is correct for headless, inline and every subcommand;
48
+ * `exit(128 + signo)`) is correct for headless and every subcommand;
49
49
  * full-screen substitutes one that restores the terminal first.
50
50
  *
51
51
  * A TERMINATOR MAY NOW RETURN WITHOUT EXITING, AND THAT IS A CONTRACT CHANGE
@@ -25,7 +25,7 @@
25
25
  * Adding a listener suppresses it, turning a crash into a silent hang.
26
26
  *
27
27
  * `SIGINT` / `SIGTERM` / `SIGHUP` — the handlers in `runInteractive()` exist
28
- * ONLY in its full-screen branch, so inline mode, `-p`, and every
28
+ * ONLY in its full-screen branch, so `-p` and every
29
29
  * subcommand had no handler at all and took Node's default termination,
30
30
  * which does not run `'exit'` listeners: the whole queue was lost. But a
31
31
  * signal listener registered here runs BEFORE the one `runInteractive`
@@ -63,7 +63,7 @@ export function setScreenRestore(fn) {
63
63
  }
64
64
  /**
65
65
  * Replace what a signal does after the flush. The default (a bare
66
- * `exit(128 + signo)`) is correct for headless, inline and every subcommand;
66
+ * `exit(128 + signo)`) is correct for headless and every subcommand;
67
67
  * full-screen substitutes one that restores the terminal first.
68
68
  *
69
69
  * A TERMINATOR MAY NOW RETURN WITHOUT EXITING, AND THAT IS A CONTRACT CHANGE
@@ -84,10 +84,6 @@ export type FollowThroughDecision = {
84
84
  };
85
85
  /** The zero budget. A fresh App, a fresh headless run, and every cleared list. */
86
86
  export declare function emptyBudget(): FollowThroughBudget;
87
- /**
88
- * The decision. EVALUATED IN THIS ORDER, AND THE ORDER IS SEMANTICS, NOT STYLE —
89
- * the same sentence `decideRenderMode` carries.
90
- */
91
87
  export declare function decideFollowThrough(input: FollowThroughInput): FollowThroughDecision;
92
88
  /**
93
89
  * Fold a finished run into the budget. Called BEFORE `decideFollowThrough`, on
@@ -36,10 +36,6 @@ function legacyUnfinishedNotice(left) {
36
36
  function advise(interactive, text) {
37
37
  return interactive ? ` ${text}` : '';
38
38
  }
39
- /**
40
- * The decision. EVALUATED IN THIS ORDER, AND THE ORDER IS SEMANTICS, NOT STYLE —
41
- * the same sentence `decideRenderMode` carries.
42
- */
43
39
  export function decideFollowThrough(input) {
44
40
  const { mode, snapshot, runEnd, budget, interactive } = input;
45
41
  // 1. Nothing is unfinished. `beginUserTurn` already drops a fully-completed
@@ -38,16 +38,6 @@ export declare const TODO_LIMITS: {
38
38
  readonly panelFullRows: 6;
39
39
  /** Below this many terminal columns the status-bar counter degrades to `[3/7]`. */
40
40
  readonly statusCompactCols: 100;
41
- /**
42
- * Below this many columns the inline strip drops its `+N done` suffix
43
- * (todo-plan-followthrough §3.7 / D-19).
44
- *
45
- * NAMED RATHER THAN INLINED, and the reason is that it sits between two other
46
- * real bounds — `statusCompactCols` (100) above and `MIN_FULLSCREEN_COLS` (40)
47
- * in `frame.ts` — with nothing to tell a reader whether a bare `80` in the
48
- * component was a third policy or a typo for one of them.
49
- */
50
- readonly stripDoneCols: 80;
51
41
  /**
52
42
  * Consecutive user turns an UNFINISHED list survives without a `todo_write`
53
43
  * before `beginUserTurn()` drops it (§3.2 / P1-7). Three keeps the common
@@ -38,16 +38,6 @@ export const TODO_LIMITS = {
38
38
  panelFullRows: 6,
39
39
  /** Below this many terminal columns the status-bar counter degrades to `[3/7]`. */
40
40
  statusCompactCols: 100,
41
- /**
42
- * Below this many columns the inline strip drops its `+N done` suffix
43
- * (todo-plan-followthrough §3.7 / D-19).
44
- *
45
- * NAMED RATHER THAN INLINED, and the reason is that it sits between two other
46
- * real bounds — `statusCompactCols` (100) above and `MIN_FULLSCREEN_COLS` (40)
47
- * in `frame.ts` — with nothing to tell a reader whether a bare `80` in the
48
- * component was a third policy or a typo for one of them.
49
- */
50
- stripDoneCols: 80,
51
41
  /**
52
42
  * Consecutive user turns an UNFINISHED list survives without a `todo_write`
53
43
  * before `beginUserTurn()` drops it (§3.2 / P1-7). Three keeps the common
@@ -22,21 +22,6 @@
22
22
  import { TODO_BLOCK_VERSION } from './limits.js';
23
23
  export { TODO_BLOCK_VERSION };
24
24
  export interface TodoBlockParams {
25
- /**
26
- * Whether this session can actually render the rail.
27
- *
28
- * FOUR SUPPORTED PATHS HAVE THE TOOL AND NO PANEL — `-p` (§3.12), inline mode
29
- * (non-goal 4), `--no-todo-panel` (§4.3), and a terminal under 80 columns or 6
30
- * viewport rows (§6.4) — and in the first three the answer is fixed for the
31
- * whole session, before the prompt is composed. Telling the model about a
32
- * panel the user does not have is the same error `composeSystemPrompt` already
33
- * guards against for team mode with its TWO flags (P1-6 / D-24).
34
- *
35
- * Dropping the block instead would be wrong: the planning discipline is worth
36
- * having without the column, which is precisely why `enabled` and `panel` are
37
- * separate config keys, and a screen-reader user is the case that argument was
38
- * written for.
39
- */
40
25
  panelVisible: boolean;
41
26
  }
42
27
  /**
@@ -41,6 +41,8 @@
41
41
  import React from 'react';
42
42
  import type { Theme } from './theme.js';
43
43
  import type { TermCapabilities } from './capabilities.js';
44
+ import { pickGlyphs } from './glyphs.js';
45
+ import type { RunActivity } from './run-status-row.js';
44
46
  /**
45
47
  * The animated spinner this row draws while it is live, or `null` when it would
46
48
  * be STATIC — an ASCII terminal, or a user who asked for stillness.
@@ -57,42 +59,32 @@ import type { TermCapabilities } from './capabilities.js';
57
59
  * reader has to reason about; one more caller of this function is not.
58
60
  */
59
61
  export declare function liveSpinner(reducedMotion: boolean, caps: TermCapabilities): React.ReactElement | null;
60
- export interface ActivityLineProps {
61
- /** Epoch ms the current run began — the phrase sequence's seed. */
62
- startedAt: number;
63
- /** `Date.now() - startedAt`, already computed by App's 200 ms ticker. */
64
- elapsedMs: number;
65
- reducedMotion: boolean;
66
- /**
67
- * The name of the tool currently in flight, or `undefined`
68
- * (agent-activity-presentation-live L4 / D-31).
69
- *
70
- * `status === 'running'` covers the WHOLE turn, including the minutes the
71
- * model is idle and a child process is doing the work. Round 1 chose the
72
- * thinking vocabulary deliberately for the gap before the first token, and it
73
- * is right for that gap; a UI that says "Pondering" while `npm test` runs is
74
- * not calm, it is wrong.
75
- */
76
- runningTool?: string;
62
+ /**
63
+ * The label ladder, MOST SPECIFIC FIRST: `Compacting context` outranks a tool name
64
+ * because compaction can be the only thing happening, and the tool name outranks
65
+ * the rotating phrase for the reason `runningTool` exists at all. One
66
+ * implementation, because the fixed bottom row and the run status row above the
67
+ * input must never disagree about what the agent is doing.
68
+ *
69
+ * `Running ${name}` rather than a per-tool verb table: a table would be a SECOND
70
+ * registry of tool names, and `toolGlyph` is already the one place that knows
71
+ * them. ASCII only, and the ellipsis comes from `pickGlyphs` like every other one.
72
+ */
73
+ export declare function resolveActivityLabel(activity: RunActivity, glyphs: ReturnType<typeof pickGlyphs>): string;
74
+ export interface ActivityLabelProps extends RunActivity {
77
75
  /**
78
- * A context compaction is in flight (context-auto-compaction §6.1).
79
- *
80
- * HIGHEST PRECEDENCE IN THE LABEL LADDER, above `runningTool` and above the
81
- * rotating phrase. Compaction happens BEFORE `turn_start`, i.e. at a moment
82
- * when `status` is `'running'` but no tool is in flight — precisely the window
83
- * in which the phrase rotation would say something false, and it can last
84
- * tens of seconds. `runningTool` already established the principle one step
85
- * back: a UI that says "Pondering" while `npm test` runs is not calm, it is
86
- * wrong, and one that says it while a 30 k-token summarization runs is the same
87
- * error.
88
- *
89
- * STILL NO SECOND SPINNER. `single-spinner-while-running`'s D-1 stands: this
90
- * row owns the only animation while it is mounted, and the compaction card
91
- * follows `FastCard` in taking a `reducedMotion` that `App` has already
92
- * widened.
76
+ * False draws the STATIC glyph even when motion is allowed. The run status row
77
+ * uses it when it has scrolled out of the viewport (the fixed bottom row owns
78
+ * the animation then) or an overlay covers the chat. The phrase still rotates
79
+ * from the RAW `reducedMotion`, so a still icon never freezes the wording.
93
80
  */
94
- compacting?: boolean;
81
+ spinnerLive?: boolean;
82
+ /** Prefix one space (the fixed bottom row's indent); the run row indents itself. */
83
+ leadingSpace?: boolean;
95
84
  theme: Theme;
96
85
  caps: TermCapabilities;
97
86
  }
98
- export declare function ActivityLine({ startedAt, elapsedMs, reducedMotion, runningTool, compacting, theme, caps, }: ActivityLineProps): React.ReactElement;
87
+ /** `<spinner> <label>` as ONE truncating `<Text>`; the only place that composes them. */
88
+ export declare function ActivityLabel({ spinnerLive, leadingSpace, theme, caps, ...activity }: ActivityLabelProps): React.ReactElement;
89
+ export type ActivityLineProps = Omit<ActivityLabelProps, 'spinnerLive' | 'leadingSpace'>;
90
+ export declare function ActivityLine(props: ActivityLineProps): React.ReactElement;
@@ -21,32 +21,43 @@ import { pickActivityPhrase } from './activity-phrases.js';
21
21
  export function liveSpinner(reducedMotion, caps) {
22
22
  return caps.unicode && !reducedMotion ? _jsx(Spinner, { type: "dots" }) : null;
23
23
  }
24
- export function ActivityLine({ startedAt, elapsedMs, reducedMotion, runningTool, compacting, theme, caps, }) {
25
- const glyphs = pickGlyphs(caps);
24
+ /**
25
+ * The label ladder, MOST SPECIFIC FIRST: `Compacting context` outranks a tool name
26
+ * because compaction can be the only thing happening, and the tool name outranks
27
+ * the rotating phrase for the reason `runningTool` exists at all. One
28
+ * implementation, because the fixed bottom row and the run status row above the
29
+ * input must never disagree about what the agent is doing.
30
+ *
31
+ * `Running ${name}` rather than a per-tool verb table: a table would be a SECOND
32
+ * registry of tool names, and `toolGlyph` is already the one place that knows
33
+ * them. ASCII only, and the ellipsis comes from `pickGlyphs` like every other one.
34
+ */
35
+ export function resolveActivityLabel(activity, glyphs) {
36
+ if (activity.compacting)
37
+ return `Compacting context${glyphs.ellipsis}`;
38
+ if (activity.runningTool)
39
+ return `Running ${activity.runningTool}`;
26
40
  // PURE AND DERIVED FROM `(startedAt, now)`, so the sequence does not need to
27
41
  // keep rotating underneath the tool label: when the tool settles the phrase
28
42
  // resumes at the position the clock implies, with no state anywhere.
29
- const phrase = pickActivityPhrase(startedAt, startedAt + elapsedMs, !reducedMotion);
30
- // `Running ${name}` rather than a per-tool verb table: a table would be a
31
- // SECOND registry of tool names, and `toolGlyph` is already the one place that
32
- // knows them. Everything round 1 legislated for this row survives — one row,
33
- // no digits, no `esc abort`, no width ladder, ASCII only.
34
- // THREE RUNGS, MOST SPECIFIC FIRST. `Compacting context` outranks a tool name
35
- // because compaction can be the only thing happening, and it outranks the
36
- // phrase for the reason `runningTool` does. ASCII only, and the ellipsis comes
37
- // from `pickGlyphs` like every other one on this row.
38
- const label = compacting
39
- ? `Compacting context${glyphs.ellipsis}`
40
- : runningTool
41
- ? `Running ${runningTool}`
42
- : `${phrase}${glyphs.ellipsis}`;
43
+ const phrase = pickActivityPhrase(activity.startedAt, activity.startedAt + activity.elapsedMs, !activity.reducedMotion);
44
+ return `${phrase}${glyphs.ellipsis}`;
45
+ }
46
+ /** `<spinner> <label>` as ONE truncating `<Text>`; the only place that composes them. */
47
+ export function ActivityLabel({ spinnerLive = true, leadingSpace = false, theme, caps, ...activity }) {
48
+ const glyphs = pickGlyphs(caps);
49
+ const label = resolveActivityLabel(activity, glyphs);
43
50
  // The IDENTICAL branch `Transcript.tsx` uses for the assistant role marker, and
44
51
  // it must stay identical: braille dots are both an animation AND a Unicode-only
45
52
  // glyph, so reduced motion and an ASCII terminal fall back to the same marker.
46
53
  //
47
54
  // Through `liveSpinner` rather than inline, so this row and the glyph
48
55
  // `BottomStatusRow` composes beside a toast can never disagree about WHEN the
49
- // spinner is live — the same reason `App` names its mount condition once.
50
- const spinner = liveSpinner(reducedMotion, caps) ?? glyphs.spinnerStill;
51
- return (_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [' ', spinner, " ", label] }) }));
56
+ // spinner is live - the same reason `App` names its mount condition once.
57
+ const spinner = (spinnerLive ? liveSpinner(activity.reducedMotion, caps) : null) ??
58
+ glyphs.spinnerStill;
59
+ return (_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [leadingSpace ? ' ' : '', spinner, " ", label] }));
60
+ }
61
+ export function ActivityLine(props) {
62
+ return (_jsx(Box, { flexShrink: 0, children: _jsx(ActivityLabel, { ...props, leadingSpace: true }) }));
52
63
  }