@aragon-agent/cli 0.6.2 → 0.6.4

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 (167) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +297 -2
  3. package/dist/agent/controller.d.ts +193 -1
  4. package/dist/agent/controller.js +475 -2
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +189 -0
  8. package/dist/agent/reducer.js +301 -2
  9. package/dist/agent/system-prompt.d.ts +14 -0
  10. package/dist/agent/system-prompt.js +2 -0
  11. package/dist/cli.js +297 -16
  12. package/dist/commands/builtins.js +165 -24
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +118 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +355 -0
  20. package/dist/compaction/compactor.d.ts +302 -0
  21. package/dist/compaction/compactor.js +810 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +215 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +264 -0
  37. package/dist/compaction/wiring.js +577 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +5 -2
  43. package/dist/config/env.js +140 -3
  44. package/dist/config/load.d.ts +42 -0
  45. package/dist/config/load.js +101 -1
  46. package/dist/config/schema.d.ts +244 -0
  47. package/dist/config/schema.js +170 -0
  48. package/dist/config/store.js +37 -1
  49. package/dist/config/ui-state.d.ts +28 -1
  50. package/dist/config/ui-state.js +9 -0
  51. package/dist/exec/events.d.ts +50 -1
  52. package/dist/exec/runner.js +42 -0
  53. package/dist/input/limits.d.ts +72 -0
  54. package/dist/input/limits.js +63 -0
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/paste-parse.d.ts +71 -0
  58. package/dist/input/paste-parse.js +95 -0
  59. package/dist/input/stdin-filter.d.ts +81 -0
  60. package/dist/input/stdin-filter.js +379 -0
  61. package/dist/logging/install.d.ts +53 -1
  62. package/dist/logging/install.js +81 -1
  63. package/dist/logging/logger.d.ts +1 -1
  64. package/dist/proc/classify.d.ts +52 -0
  65. package/dist/proc/classify.js +143 -0
  66. package/dist/proc/kill-tree.d.ts +42 -0
  67. package/dist/proc/kill-tree.js +119 -0
  68. package/dist/proc/limits.d.ts +84 -0
  69. package/dist/proc/limits.js +84 -0
  70. package/dist/proc/log-ring.d.ts +68 -0
  71. package/dist/proc/log-ring.js +102 -0
  72. package/dist/proc/prompt.d.ts +24 -0
  73. package/dist/proc/prompt.js +49 -0
  74. package/dist/proc/readiness.d.ts +102 -0
  75. package/dist/proc/readiness.js +219 -0
  76. package/dist/proc/supervisor.d.ts +103 -0
  77. package/dist/proc/supervisor.js +465 -0
  78. package/dist/proc/types.d.ts +175 -0
  79. package/dist/proc/types.js +15 -0
  80. package/dist/session/persist.js +55 -1
  81. package/dist/team/report.js +7 -1
  82. package/dist/team/runtime.d.ts +10 -0
  83. package/dist/team/runtime.js +7 -0
  84. package/dist/team/subagent.d.ts +31 -1
  85. package/dist/team/subagent.js +42 -15
  86. package/dist/team/types.d.ts +8 -0
  87. package/dist/tools/bash-tool.d.ts +51 -2
  88. package/dist/tools/bash-tool.js +321 -51
  89. package/dist/tools/fs-tools.d.ts +17 -0
  90. package/dist/tools/index.d.ts +55 -2
  91. package/dist/tools/index.js +39 -1
  92. package/dist/tools/proc-tools.d.ts +25 -0
  93. package/dist/tools/proc-tools.js +102 -0
  94. package/dist/ui/ActivityLine.d.ts +19 -1
  95. package/dist/ui/ActivityLine.js +10 -2
  96. package/dist/ui/App.d.ts +62 -2
  97. package/dist/ui/App.js +546 -16
  98. package/dist/ui/Composer.d.ts +66 -1
  99. package/dist/ui/Composer.js +50 -6
  100. package/dist/ui/PromptInput.d.ts +82 -3
  101. package/dist/ui/PromptInput.js +232 -49
  102. package/dist/ui/StatusBar.d.ts +59 -0
  103. package/dist/ui/StatusBar.js +16 -4
  104. package/dist/ui/TeamPanel.js +7 -1
  105. package/dist/ui/Transcript.d.ts +21 -1
  106. package/dist/ui/Transcript.js +61 -3
  107. package/dist/ui/clipboard.d.ts +52 -0
  108. package/dist/ui/clipboard.js +91 -0
  109. package/dist/ui/composer-limits.d.ts +31 -0
  110. package/dist/ui/composer-limits.js +50 -0
  111. package/dist/ui/composer-rows.d.ts +73 -0
  112. package/dist/ui/composer-rows.js +201 -0
  113. package/dist/ui/editor-reducer.d.ts +37 -0
  114. package/dist/ui/editor-reducer.js +88 -12
  115. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  116. package/dist/ui/entries/CompactionCard.js +158 -0
  117. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  118. package/dist/ui/entries/ServiceCard.js +116 -0
  119. package/dist/ui/entries/UserEntry.d.ts +17 -3
  120. package/dist/ui/entries/UserEntry.js +34 -5
  121. package/dist/ui/frame-differ.d.ts +46 -0
  122. package/dist/ui/frame-differ.js +94 -10
  123. package/dist/ui/gauge.d.ts +16 -1
  124. package/dist/ui/gauge.js +25 -6
  125. package/dist/ui/glyphs.d.ts +11 -0
  126. package/dist/ui/glyphs.js +2 -0
  127. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  128. package/dist/ui/layout/ScrollViewport.js +128 -21
  129. package/dist/ui/layout/budget.d.ts +44 -9
  130. package/dist/ui/layout/budget.js +42 -10
  131. package/dist/ui/layout/follow-state.d.ts +137 -0
  132. package/dist/ui/layout/follow-state.js +150 -0
  133. package/dist/ui/layout/virtual-window.d.ts +11 -0
  134. package/dist/ui/layout/virtual-window.js +85 -1
  135. package/dist/ui/overlays/HelpOverlay.js +9 -1
  136. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  137. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  138. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  139. package/dist/ui/overlays/SettingsScreen.js +74 -2
  140. package/dist/ui/palettes.d.ts +16 -0
  141. package/dist/ui/palettes.js +6 -0
  142. package/dist/ui/paste-frames.d.ts +87 -0
  143. package/dist/ui/paste-frames.js +112 -0
  144. package/dist/ui/paste-tokens.d.ts +90 -0
  145. package/dist/ui/paste-tokens.js +147 -0
  146. package/dist/ui/screen.d.ts +38 -0
  147. package/dist/ui/screen.js +113 -8
  148. package/dist/ui/selection/highlight.d.ts +39 -0
  149. package/dist/ui/selection/highlight.js +64 -0
  150. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  151. package/dist/ui/selection/screen-mirror.js +35 -0
  152. package/dist/ui/selection/selection-controller.d.ts +88 -0
  153. package/dist/ui/selection/selection-controller.js +282 -0
  154. package/dist/ui/selection/selection.d.ts +90 -0
  155. package/dist/ui/selection/selection.js +223 -0
  156. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  157. package/dist/ui/stdout-frame-writer.js +15 -0
  158. package/dist/ui/theme.d.ts +11 -0
  159. package/dist/ui/theme.js +2 -0
  160. package/dist/ui/transcript-text.js +72 -0
  161. package/dist/ui/use-startup-notices.d.ts +27 -1
  162. package/dist/ui/use-startup-notices.js +43 -11
  163. package/dist/ui/use-wheel-routing.d.ts +1 -1
  164. package/dist/ui/use-wheel-routing.js +8 -0
  165. package/package.json +4 -2
  166. package/dist/input/stdin-mouse-filter.d.ts +0 -42
  167. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -88,6 +88,20 @@ export interface SystemPromptParams {
88
88
  * for a fixed `tools` array. Every existing caller passes nothing.
89
89
  */
90
90
  appendSystemPrompt?: string;
91
+ /**
92
+ * The rendered `<background_services>` block, or `''`
93
+ * (background-service-supervision §5.2).
94
+ *
95
+ * Spliced conditionally exactly as `skillsBlock`, the plan block, `teamBlock`,
96
+ * `todoBlock` and `fastBlock` are, which is what keeps I-2 true:
97
+ * `bash.background: false` produces the pre-feature prompt BYTE FOR BYTE for a
98
+ * fixed tool array.
99
+ *
100
+ * A CHILD'S PROMPT NEVER CARRIES IT: subagents build tools from the same
101
+ * factory but are given no supervisor, so telling one about `bash_output` /
102
+ * `bash_kill` would describe tools it does not have.
103
+ */
104
+ backgroundBlock?: string;
91
105
  }
92
106
  /** Describe the active shell so the model does not emit incompatible syntax. */
93
107
  export declare function describeShell(): {
@@ -41,6 +41,7 @@ export function buildSystemPrompt(params) {
41
41
  const subagentBlock = params.subagentBlock ?? '';
42
42
  const todoBlock = params.todoBlock ?? '';
43
43
  const fastBlock = params.fastBlock ?? '';
44
+ const backgroundBlock = params.backgroundBlock ?? '';
44
45
  const appended = (params.appendSystemPrompt ?? '').trim();
45
46
  return [
46
47
  'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
@@ -59,6 +60,7 @@ export function buildSystemPrompt(params) {
59
60
  ...(subagentBlock ? ['', subagentBlock] : []),
60
61
  ...(todoBlock ? ['', todoBlock] : []),
61
62
  ...(fastBlock ? ['', fastBlock] : []),
63
+ ...(backgroundBlock ? ['', backgroundBlock] : []),
62
64
  '',
63
65
  'Operating guidance:',
64
66
  '- Prefer reading files and inspecting the workspace before making changes.',
package/dist/cli.js CHANGED
@@ -23,11 +23,11 @@ import { formatMigrationNotice, migrateLegacyState } from './config/migrate-lega
23
23
  import { formatHomeMigrationNotice, migrateToHome } from './config/migrate-home.js';
24
24
  import { migrateStateOutOfConfig } from './config/migrate-state-out-of-config.js';
25
25
  import { runHistoryCommand } from './config/history-commands.js';
26
- import { FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS, RETRY_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, UPDATE_CONFIG_SET_KEYS, applyFastConfigSet, applyLogConfigSet, applyRetryConfigSet, applyTeamConfigSet, applyUpdateConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
26
+ import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS, RETRY_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, UPDATE_CONFIG_SET_KEYS, applyCompactionConfigSet, applyFastConfigSet, applyLogConfigSet, applyRetryConfigSet, applyTeamConfigSet, applyUpdateConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
27
27
  import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
28
28
  import { getLogger } from './logging/logger.js';
29
29
  import { runLogsCommand } from './logging/cli-commands.js';
30
- import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampMaxRenderInterval, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
30
+ import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
31
31
  import { makeGetApiKey } from './config/load.js';
32
32
  import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
33
33
  import { runSkillsCommand } from './skills/cli-commands.js';
@@ -44,7 +44,7 @@ import { decideRenderMode } from './ui/layout/frame.js';
44
44
  import { createFrameDiffer } from './ui/frame-differ.js';
45
45
  import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
46
46
  import { setFrameStatsProvider } from './commands/perf.js';
47
- import { tryCreateMouseFilter } from './input/stdin-mouse-filter.js';
47
+ import { tryCreateStdinFilter } from './input/stdin-filter.js';
48
48
  import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
49
49
  import { supportsWindowsVtInput } from './ui/win-vt-input.js';
50
50
  import { forceWindowsVtInput } from './ui/win-vt-force.js';
@@ -52,6 +52,9 @@ import { readExitSnapshot } from './ui/exit-snapshot.js';
52
52
  import { renderTranscriptText } from './ui/transcript-text.js';
53
53
  import { detectCapabilities } from './ui/capabilities.js';
54
54
  import { pickGlyphs } from './ui/glyphs.js';
55
+ import { getTheme } from './ui/theme.js';
56
+ import { copyText } from './ui/clipboard.js';
57
+ import { createSelectionController, } from './ui/selection/selection-controller.js';
55
58
  import { DENY_ALL_HUMAN_INPUT } from './tools/human-input.js';
56
59
  // A VALUE import, and the one deliberate exception to the paragraph above
57
60
  // (cli-auto-update-hardening P2-7). `boot/guard.js` statically pulls
@@ -105,6 +108,16 @@ function toFlags(opts) {
105
108
  // Without this line the flag never reaches `CliFlags` and `--no-mouse` is
106
109
  // inert — a failure with no compile error and no runtime error (P1-2).
107
110
  mouse: opts.mouse,
111
+ // Site three of three for `--mouse-select` / `--no-mouse-select` — NINE
112
+ // features, one trap. A flag needs the `.option()` declaration below, a field
113
+ // on `CliFlags`, AND a line here; `config.test.ts` is the regression pin that
114
+ // exists because `--no-mouse` shipped inert once already.
115
+ mouseSelect: opts.mouseSelect,
116
+ // Site three of three for `--paste` / `--no-paste` -- TEN features, one
117
+ // trap. A flag needs the `.option()` declaration below, a field on
118
+ // `CliFlags`, AND a line here; `config.test.ts` is the regression pin that
119
+ // exists because `--no-mouse` shipped inert once already.
120
+ paste: opts.paste,
108
121
  skills: opts.skills,
109
122
  skill: opts.skill,
110
123
  skillsYes: opts.skillsYes,
@@ -171,6 +184,15 @@ function toFlags(opts) {
171
184
  // means `--no-update` reports nothing, does nothing, and leaves the updater
172
185
  // running.
173
186
  update: opts.update,
187
+ // Site three of three for `--compaction` / `--no-compaction` — NINE
188
+ // features, one trap. A flag needs the `.option()` declaration below, a field
189
+ // on `CliFlags`, AND a line here; miss this one and the flag is inert with no
190
+ // compile error and no runtime error. It bites here for the same reason it
191
+ // does one field up: this is the KILL SWITCH for a feature that spends the
192
+ // user's money on a summarizer, so a missing line means `--no-compaction`
193
+ // reports nothing, does nothing, and leaves compaction armed.
194
+ compaction: opts.compaction,
195
+ compactionThreshold: opts.compactionThreshold,
174
196
  };
175
197
  }
176
198
  // ---------------------------------------------------------------------------
@@ -399,7 +421,7 @@ function runInteractive(flags, extras = {}) {
399
421
  // before it reaches this process, so `\x1b[?1000h` is a pure no-op and the
400
422
  // filter has nothing to filter. Gating `wantMouse` — rather than the three
401
423
  // things downstream of it — is what keeps the three in agreement: the
402
- // `?1000h` write (`enterAltScreen(..., { mouse: filter !== null })`), the
424
+ // `?1000h` write (`enterAltScreen(..., { mouse: mouseOn })`), the
403
425
  // filter itself, and the `mouseSource` prop that decides whether the user is
404
426
  // advised about a mode that is not running. Wiring any of them separately
405
427
  // re-opens the gap where the app claims a capability it does not have.
@@ -417,10 +439,50 @@ function runInteractive(flags, extras = {}) {
417
439
  !!process.stdout.isTTY &&
418
440
  !!process.stdin.isTTY &&
419
441
  vtInputSupported;
420
- const mouseFilter = wantMouse
421
- ? tryCreateMouseFilter(process.stdin, (reason) => logger.warn('cli', 'mouse_filter_failed', { reason }))
442
+ // --- Paste handling (tui-paste-handling section 5.1.1 / D-13 / D-17). ----
443
+ //
444
+ // THE GATE IS WIDER THAN THE MOUSE'S, AND `paste` DEFAULTS TO TRUE. Paste
445
+ // needs no `vtInputSupported` and no full-screen: Tier 2 is a heuristic on
446
+ // chunk shape and needs no terminal cooperation at all, which is exactly why
447
+ // it exists for the consoles that never deliver `\x1b[200~` (R-11).
448
+ //
449
+ // WHAT THIS WIDENING BREAKS IF IT IS NOT PAIRED WITH `mouseOn` BELOW (P0-1).
450
+ // `filter !== null` used to be the answer to FIVE questions, only one of which
451
+ // is really about the stream. With paste able to build a filter on its own,
452
+ // `aragon --no-mouse` would get a non-null handle and every one of those five
453
+ // would flip true: `?1000h`/`?1006h`/`?1002h` written, terminal selection taken
454
+ // away (the ONLY reason anyone passes `--no-mouse`), and a startup notice
455
+ // advising the user about a mode they just disabled.
456
+ const wantPaste = config.paste && !!process.stdin.isTTY;
457
+ const pasteBridge = { notify: null };
458
+ const filter = wantMouse || wantPaste
459
+ ? tryCreateStdinFilter(process.stdin, { mouse: wantMouse, paste: wantPaste }, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
422
460
  : null;
423
- const disposeMouse = () => mouseFilter?.dispose();
461
+ /**
462
+ * THE ONE EXPRESSION EVERY MOUSE SITE READS (I-11 / D-17).
463
+ *
464
+ * `filter !== null` now answers exactly one question -- "is a stream wrapped?"
465
+ * -- and the only site allowed to keep reading it is the `stdin` handed to
466
+ * `render()`.
467
+ */
468
+ const mouseOn = wantMouse && filter !== null;
469
+ /** Same discipline for DEC 2004: already decided here, never re-derived. */
470
+ const pasteOn = wantPaste && filter !== null;
471
+ const disposeStdinFilter = () => filter?.dispose();
472
+ // --- Drag-select (tui-selection-and-scroll-follow §4.4). ------------------
473
+ //
474
+ // ONE EXPRESSION, READ BY FOUR THINGS, and that is the same discipline
475
+ // `wantMouse` above follows for the same reason: the `?1002h` write, the
476
+ // controller, the differ's `decorate` hook and the startup notice must never
477
+ // disagree about whether drag-select is running. With it false the session is
478
+ // byte-identical to a pre-feature build — no extra escape on the wire (AC-8),
479
+ // no controller, no `decorate`, no mirror.
480
+ const wantSelect = mouseOn && config.mouseSelect;
481
+ const selectionBridge = {
482
+ controller: null,
483
+ onCopied: null,
484
+ requestRedraw: null,
485
+ };
424
486
  // --- Frame differ (tui-input-flicker-fix §3.2 / §4.2). -------------------
425
487
  //
426
488
  // FULL-SCREEN ONLY, and that is invariant I-3 rather than caution: inline mode
@@ -443,6 +505,25 @@ function runInteractive(flags, extras = {}) {
443
505
  // edge inside the differ: a notice per fallback would be a repaint
444
506
  // storm triggered by a repaint problem (§5.6).
445
507
  onFirstFallback: () => console.warn(FRAME_FALLBACK_NOTICE),
508
+ // ABSENT ENTIRELY without drag-select, not a no-op function: the
509
+ // differ's identity path then allocates nothing and copies nothing,
510
+ // which is what makes `mouseSelect: false` a statement about the code
511
+ // rather than about how cheap an empty function is.
512
+ ...(wantSelect
513
+ ? {
514
+ decorate: (lines) => selectionBridge.controller?.decorate(lines) ?? lines,
515
+ // P1-7: a pass-through frame writes the RAW Ink frame verbatim,
516
+ // so the highlight is wiped off the terminal while the
517
+ // controller still believes it is painted. Deferred out of the
518
+ // current stack because this fires from inside `transform`,
519
+ // which is inside Ink's own `stdout.write`.
520
+ onInvalidate: () => {
521
+ const controller = selectionBridge.controller;
522
+ if (controller)
523
+ setImmediate(() => controller.clear());
524
+ },
525
+ }
526
+ : {}),
446
527
  }))
447
528
  : null;
448
529
  // `null` when no writer was built, so `/perf` can say "diff render off" rather
@@ -476,10 +557,21 @@ function runInteractive(flags, extras = {}) {
476
557
  }));
477
558
  };
478
559
  if (mode === 'fullscreen') {
479
- // `mouse` here means "a filter is installed", NOT "the user wants mouse
480
- // support" — see `AltScreenOptions`. `screen.ts` must never derive it for
481
- // itself, or a later refactor re-opens the gap I-8 closes.
482
- screen = enterAltScreen(process.stdout, { mouse: mouseFilter !== null });
560
+ // `mouse` here means "a mouse-parsing filter is installed", NOT "the user
561
+ // wants mouse support" — see `AltScreenOptions`. `screen.ts` must never
562
+ // derive it for itself, or a later refactor re-opens the gap I-8 closes.
563
+ // `motion` carries the same discipline for `?1002h`, and `bracketedPaste`
564
+ // for `?2004h`: all three are already decided, none is re-derived.
565
+ //
566
+ // IT IS `mouseOn`, NOT `filter !== null` (P0-1). A `--no-mouse` session with
567
+ // paste at its default `true` still builds a filter, and passing the handle
568
+ // here would write `?1000h` at a user who disabled the mouse precisely to
569
+ // keep their terminal's own selection.
570
+ screen = enterAltScreen(process.stdout, {
571
+ mouse: mouseOn,
572
+ motion: wantSelect,
573
+ bracketedPaste: pasteOn && mode === 'fullscreen',
574
+ });
483
575
  const restore = () => screen?.restore();
484
576
  // Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
485
577
  // - `waitUntilExit().then()` is a microtask and may never be reached when
@@ -516,19 +608,136 @@ function runInteractive(flags, extras = {}) {
516
608
  // `CI` is excluded on top of the two TTY tests because a CI runner with a pty
517
609
  // allocated is otherwise a machine that silently mutates its own global
518
610
  // toolchain mid-pipeline (D-11).
611
+ // --- The foreign-write door (§4.4.5). ------------------------------------
612
+ //
613
+ // Where bytes that are OURS but are NOT a frame go — today, the OSC 52
614
+ // clipboard sequence. THREE SHAPES, ONE DOOR, and the third one is the reason
615
+ // this is a resolved value rather than an inline arrow:
616
+ //
617
+ // * with the differ in front of stdout it MUST be `frameWriter.writeForeign`,
618
+ // which invalidates the cache and skips the fallback accounting — through
619
+ // the proxy the same bytes are an unrecognised chunk, which is
620
+ // `passThrough(true)`, which prints `FRAME_FALLBACK_NOTICE` at a user who
621
+ // did nothing but copy something (P1-6);
622
+ // * without one — `--no-diff-render`, and inline mode, which never builds a
623
+ // differ at all — stdout IS the real stream, so a direct write is both safe
624
+ // and what §4.4.5 prescribes. OSC 52 emits no newline, so Ink's own line
625
+ // accounting is untouched and there is nothing to invalidate;
626
+ // * on a non-TTY there is no terminal to accept it, so the door is ABSENT.
627
+ //
628
+ // It must never be a function that accepts a chunk and writes NOWHERE.
629
+ // `copyText` reports `'osc52'` whenever its door took the text, so a silent
630
+ // no-op door — which `frameWriter?.writeForeign(chunk)` is under
631
+ // `--no-diff-render` — makes the toast name a mechanism that was never
632
+ // attempted. Naming the mechanism honestly is the entire reason that return
633
+ // value exists (R-7), and a door that lies is worse than no door.
634
+ const writeForeign = frameWriter
635
+ ? (text) => frameWriter.writeForeign(text)
636
+ : process.stdout.isTTY
637
+ ? (text) => {
638
+ try {
639
+ process.stdout.write(text);
640
+ }
641
+ catch {
642
+ // A closed or broken stdout must never turn a copy into a crash.
643
+ }
644
+ }
645
+ : null;
646
+ // --- The selection controller (§4.4). ------------------------------------
647
+ //
648
+ // BUILT AFTER `enterAltScreen` AND BEFORE `render()`. After, because it must
649
+ // not exist on a session that never asked the terminal to report motion;
650
+ // before, because `decorate` is read by the differ from the very first frame
651
+ // Ink produces.
652
+ //
653
+ // `mouseCaptured` is the run-time answer for `/mouse`, and it lives here rather
654
+ // than inside `screen.ts`'s closure because `App` needs to READ it and the
655
+ // handle's own flag is private (it has to be — `restore()` reads it, and
656
+ // exposing a setter would be a second way to get the two out of step).
657
+ let mouseCaptured = mouseOn;
658
+ if (wantSelect && filter) {
659
+ selectionBridge.controller = createSelectionController({
660
+ source: filter.source,
661
+ // The frame writer is absent under `--no-diff-render`, where there is no
662
+ // cache to repaint from at all; the controller then falls back to asking
663
+ // `App` for a React redraw on every drag flush, which is slower and
664
+ // correct. That is rung 2 of the fail-safe ladder doing its job.
665
+ repaint: () => frameWriter?.repaint() ?? false,
666
+ requestRedraw: () => selectionBridge.requestRedraw?.(),
667
+ // `?? undefined` and not a wrapper: `copyText` skips OSC 52 entirely when
668
+ // it has no door, and reports `'native'`/`'none'` accordingly.
669
+ copy: (text) => copyText(text, writeForeign ?? undefined),
670
+ onCopied: (via, lines, chars) => selectionBridge.onCopied?.(via, lines, chars),
671
+ // Read at PAINT time, not captured: `/theme` rebuilds the theme mid-session.
672
+ theme: () => getTheme(config.theme, detectCapabilities(process.env, process.stdout)),
673
+ caps: detectCapabilities(process.env, process.stdout),
674
+ cols: () => process.stdout.columns ?? 80,
675
+ // The STATIC half of the gate. The dynamic half — "no overlay is open" —
676
+ // belongs to `App`, which owns that state and calls `setEnabled`.
677
+ isSelectable: () => mouseCaptured,
678
+ });
679
+ }
680
+ const disposeSelection = () => {
681
+ selectionBridge.controller?.dispose();
682
+ selectionBridge.controller = null;
683
+ };
684
+ /**
685
+ * The terminal-facing services `App` cannot build for itself (§4.4).
686
+ *
687
+ * PRESENT IN INLINE MODE TOO, and only for the foreign-write door: §4.4.5
688
+ * routes `/copy` through `ui/clipboard.ts` precisely so it gains OSC 52 — the
689
+ * one mechanism that reaches the clipboard of the machine the USER is sitting
690
+ * at over SSH — and gating the whole bridge on full-screen would have handed
691
+ * that back only to full-screen users while the command is available in both.
692
+ * Every OTHER field stays absent there, so the inline tree is unchanged: there
693
+ * is no filter, hence no `setMouseCapture` and no `ctx.mouse`; `mouseSelect`
694
+ * is `wantSelect`, which is already false without one; and there is no
695
+ * controller, so no `selection`.
696
+ */
697
+ const terminalBridge = mode === 'fullscreen' || writeForeign
698
+ ? {
699
+ mouseSelect: wantSelect,
700
+ ...(mouseOn
701
+ ? {
702
+ setMouseCapture: (on) => {
703
+ screen?.setMouseCapture(on);
704
+ mouseCaptured = on;
705
+ // A released mouse cannot be dragging, and a selection left
706
+ // painted over a screen the terminal is now selecting on its
707
+ // own would be two highlights claiming the same rows.
708
+ if (!on)
709
+ selectionBridge.controller?.clear();
710
+ },
711
+ isMouseCaptured: () => mouseCaptured,
712
+ }
713
+ : {}),
714
+ // The same door the controller copies through, so `/copy` and a
715
+ // drag-release can never disagree about which stream OSC 52 goes to.
716
+ ...(writeForeign ? { writeForeign } : {}),
717
+ ...(selectionBridge.controller ? { selection: selectionBridge } : {}),
718
+ }
719
+ : undefined;
519
720
  const updateBridge = { service: null, onAttach: null };
520
721
  let updateService = null;
521
722
  const updateEligible = !!process.stdout.isTTY &&
522
723
  !!process.stdin.isTTY &&
523
724
  !process.env.CI &&
524
725
  config.update.mode !== 'off';
525
- const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay, mouseSource: mouseFilter?.source,
726
+ const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay,
727
+ // `mouseOn`, NOT the handle (I-11). `App.tsx:186-188` documents this
728
+ // prop's contract as "absent under `--no-mouse`, inline, or non-TTY", and
729
+ // it feeds `enabled: !!mouseSource` on a wheel subscription that would
730
+ // otherwise be a channel no event can ever arrive on.
731
+ mouseSource: mouseOn && filter ? filter.source : undefined,
526
732
  // Present ONLY when this console swallows `CSI Z` and mouse reports.
527
733
  // Passed in rather than probed inside `App` so that every test renders
528
734
  // the same tree on every machine — reading `process.versions.node` from
529
735
  // a component would make the notice appear or vanish according to which
530
736
  // Node the developer happened to have installed.
531
- vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined }),
737
+ vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined,
738
+ // Absent when nothing can refuse a paste, so a `--no-paste` tree is
739
+ // byte-identical to a build without this feature.
740
+ pasteBridge: pasteOn ? pasteBridge : undefined, terminal: terminalBridge }),
532
741
  // Full-screen owns console.* itself (I-4): Ink's patchConsole writes
533
742
  // straight to stdout and permanently shifts the fixed frame's accounting.
534
743
  {
@@ -536,7 +745,7 @@ function runInteractive(flags, extras = {}) {
536
745
  patchConsole: mode === 'inline',
537
746
  // I-5: with mouse support off the REAL stdin is handed over unwrapped, so
538
747
  // every existing path is provably unchanged.
539
- stdin: mouseFilter?.stdin ?? process.stdin,
748
+ stdin: filter?.stdin ?? process.stdin,
540
749
  // SPREAD, not `frameWriter?.stdout ?? process.stdout`. This call passed no
541
750
  // `stdout` at all before, and Ink defaults it to `process.stdout`; omitting
542
751
  // the key entirely when the writer is off keeps the options object the same
@@ -597,7 +806,12 @@ function runInteractive(flags, extras = {}) {
597
806
  .waitUntilExit()
598
807
  .then(() => {
599
808
  screen?.restore();
600
- disposeMouse();
809
+ // BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
810
+ // and after `restore()` for the reason that call records: the terminal must
811
+ // stop reporting first, and anything already in flight is still consumed by
812
+ // a live filter.
813
+ disposeSelection();
814
+ disposeStdinFilter();
601
815
  // BOTH BRANCHES, the same discipline `controller.dispose()` and
602
816
  // `updateService?.dispose()` already follow. It drops the `'resize'`
603
817
  // listener of I-8 and detaches the `/perf` provider so a later `/perf` in a
@@ -621,7 +835,12 @@ function runInteractive(flags, extras = {}) {
621
835
  })
622
836
  .catch(() => {
623
837
  screen?.restore();
624
- disposeMouse();
838
+ // BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
839
+ // and after `restore()` for the reason that call records: the terminal must
840
+ // stop reporting first, and anything already in flight is still consumed by
841
+ // a live filter.
842
+ disposeSelection();
843
+ disposeStdinFilter();
625
844
  disposeFrameWriter();
626
845
  controller.dispose();
627
846
  updateService?.dispose();
@@ -737,6 +956,15 @@ const CONFIG_SET_KEYS = new Set([
737
956
  // falls through every case, writes nothing, and still prints
738
957
  // `Set mouse = false` (P1-2) -- exactly what happened to `density`/`hints`.
739
958
  'mouse',
959
+ // Both cased in the switch below, for the reason the note above `mouse`
960
+ // records: a key listed here but absent there writes nothing and still prints
961
+ // `Set mouseSelect = false`.
962
+ 'mouseSelect',
963
+ // Listed here AND cased in the switch below, for the reason the note above
964
+ // `mouse` records: a key present here but absent there falls through every
965
+ // case, writes nothing, and still prints `Set paste = false`.
966
+ 'paste',
967
+ 'scrollResumeMs',
740
968
  'historyEnabled',
741
969
  'planModeDefault',
742
970
  'planModeMaxAskRounds',
@@ -763,6 +991,11 @@ const CONFIG_SET_KEYS = new Set([
763
991
  ...FAST_CONFIG_SET_KEYS,
764
992
  // `update.*` keys are handled by `applyUpdateConfigSet`, same reasoning again.
765
993
  ...UPDATE_CONFIG_SET_KEYS,
994
+ // `compaction.*` keys are handled by `applyCompactionConfigSet`, same
995
+ // reasoning again — and this set is what makes the kill switch reachable from
996
+ // a script at all: `config list` enumerates the section dynamically and so
997
+ // shows these keys whether they are here or not, while `config set` does not.
998
+ ...COMPACTION_CONFIG_SET_KEYS,
766
999
  ]);
767
1000
  /**
768
1001
  * `aragon config set maxTokens <n|auto|default>`.
@@ -849,6 +1082,18 @@ function runConfigSet(key, value) {
849
1082
  process.stdout.write(`Set ${key} = ${String(merged.update[field])}\n`);
850
1083
  return;
851
1084
  }
1085
+ const compactionPatch = applyCompactionConfigSet(key, value);
1086
+ if (compactionPatch) {
1087
+ // Echoed from the STORED value, so `config set compaction.threshold 0.2`
1088
+ // prints the clamped 0.5 rather than reporting a number that was never
1089
+ // written — the rule `team.*`, `retry.*`, `fast.*` and `update.*` each state
1090
+ // above. It matters most for `warnThreshold`, which is clamped against the
1091
+ // stored `threshold` and so can move for a reason the caller cannot see.
1092
+ const merged = updatePersistedConfig(compactionPatch);
1093
+ const field = key.slice('compaction.'.length);
1094
+ process.stdout.write(`Set ${key} = ${String(merged.compaction[field])}\n`);
1095
+ return;
1096
+ }
852
1097
  const patch = {};
853
1098
  switch (key) {
854
1099
  case 'provider':
@@ -914,6 +1159,19 @@ function runConfigSet(key, value) {
914
1159
  case 'mouse':
915
1160
  patch.mouse = value === 'true' || value === '1';
916
1161
  break;
1162
+ case 'mouseSelect':
1163
+ patch.mouseSelect = value === 'true' || value === '1';
1164
+ break;
1165
+ case 'paste':
1166
+ patch.paste = value === 'true' || value === '1';
1167
+ break;
1168
+ // Clamped rather than rejected, for the same reason `planModeDefault` below
1169
+ // records: hardening only the READ path leaves a bad value on disk that
1170
+ // reverts to the default on every launch, which presents as "my setting
1171
+ // won't stick". `0` is a legal value here and its documented off switch.
1172
+ case 'scrollResumeMs':
1173
+ patch.scrollResumeMs = clampScrollResumeMs(value, DEFAULT_SCROLL_RESUME_MS);
1174
+ break;
917
1175
  // Turning this off stops new prompts being recorded; it does NOT delete
918
1176
  // what is already in `prompt-history.jsonl` (that is `aragon history clear`).
919
1177
  case 'historyEnabled':
@@ -1172,6 +1430,19 @@ function buildProgram() {
1172
1430
  // Positive form first, same tri-state reason as `--fullscreen` above.
1173
1431
  .option('--mouse', 'Wheel scrolls the transcript (full-screen mode)')
1174
1432
  .option('--no-mouse', 'Leave the mouse to the terminal (the wheel does nothing)')
1433
+ // Positive form FIRST, for the tri-state reason `--fullscreen` records
1434
+ // below: `mouseSelect` is persisted and defaults to `true`, so a lone
1435
+ // `--no-mouse-select` would make commander default `opts.mouseSelect` to
1436
+ // `true` — indistinguishable from silence, and silently overriding a stored
1437
+ // `false` on every run that passes no flag at all.
1438
+ .option('--mouse-select', 'Drag with the mouse to select text; releasing copies it')
1439
+ .option('--no-mouse-select', 'Keep wheel scrolling, but leave drag-select off')
1440
+ // Positive form FIRST, same tri-state reason as `--mouse-select` above:
1441
+ // `paste` is persisted and defaults to `true`, so a lone `--no-paste` would
1442
+ // make commander default `opts.paste` to `true` -- indistinguishable from
1443
+ // silence, and silently overriding a stored `false` on every run.
1444
+ .option('--paste', 'Collapse large pastes into a placeholder (the default)')
1445
+ .option('--no-paste', 'Treat pasted bytes as keystrokes (pre-0.6.3 behavior)')
1175
1446
  // Declaring `--fullscreen` BEFORE `--no-fullscreen` keeps the default
1176
1447
  // `undefined` instead of `true`, which is what makes the tri-state work:
1177
1448
  // "no opinion" must stay distinguishable from "force it".
@@ -1228,6 +1499,16 @@ function buildProgram() {
1228
1499
  // feature that installs software would quietly un-set itself (C-11).
1229
1500
  .option('--update', 'Check for and install updates in the background (the default)')
1230
1501
  .option('--no-update', 'Disable auto-update for this session')
1502
+ // BOTH FORMS, positive first, for the reason recorded eight times above —
1503
+ // and this pair shares `--update`'s polarity, which is the worst one.
1504
+ // `compaction.enabled` is PERSISTED and defaults to `true`, so a lone
1505
+ // `--no-compaction` would make commander default `opts.compaction` to `true`
1506
+ // and silently overwrite a stored `false` on EVERY run that passed no flag at
1507
+ // all: the kill switch for a feature that spends the user's money on a
1508
+ // summarizer would quietly un-set itself (C-13 / P1-5).
1509
+ .option('--compaction', 'Compact the conversation when the context window fills (the default)')
1510
+ .option('--no-compaction', 'Never compact; a full context window ends the run as before')
1511
+ .option('--compaction-threshold <n>', 'Occupancy that triggers compaction for this run: 0.9 or 90% (0.5-0.95)')
1231
1512
  .option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
1232
1513
  .option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
1233
1514
  .option('--skills-yes', 'Approve skill installs for this run (CI / headless)')