@aragon-agent/cli 0.6.1 → 0.6.3
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.
- package/CHANGELOG.md +216 -0
- package/README.md +241 -18
- package/dist/agent/agent-mode.d.ts +39 -0
- package/dist/agent/agent-mode.js +36 -0
- package/dist/agent/controller.d.ts +74 -1
- package/dist/agent/controller.js +200 -1
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +120 -0
- package/dist/agent/reducer.js +201 -2
- package/dist/cli.js +297 -6
- package/dist/commands/builtins.js +73 -23
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +113 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +342 -0
- package/dist/compaction/compactor.d.ts +268 -0
- package/dist/compaction/compactor.js +689 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +179 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +255 -0
- package/dist/compaction/wiring.js +556 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +92 -2
- package/dist/config/load.d.ts +31 -0
- package/dist/config/load.js +63 -1
- package/dist/config/schema.d.ts +159 -0
- package/dist/config/schema.js +137 -0
- package/dist/config/store.js +17 -1
- package/dist/config/ui-state.d.ts +44 -6
- package/dist/config/ui-state.js +21 -12
- package/dist/diagnostics/doctor.js +41 -1
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/index.js +8 -4
- package/dist/exec/runner.js +44 -2
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/stdin-mouse-filter.d.ts +8 -3
- package/dist/logging/install.d.ts +51 -1
- package/dist/logging/install.js +28 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/cli-commands.js +23 -1
- package/dist/session/persist.js +19 -0
- package/dist/session/store.d.ts +18 -0
- package/dist/session/store.js +59 -2
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +48 -1
- package/dist/ui/App.js +317 -17
- package/dist/ui/Composer.d.ts +26 -1
- package/dist/ui/Composer.js +6 -6
- package/dist/ui/PromptInput.d.ts +34 -1
- package/dist/ui/PromptInput.js +54 -3
- package/dist/ui/StatusBar.d.ts +40 -0
- package/dist/ui/StatusBar.js +13 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +32 -2
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +10 -3
- package/dist/ui/layout/budget.js +10 -3
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +34 -0
- package/dist/ui/overlays/HelpOverlay.js +18 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +60 -1
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/screen.d.ts +22 -0
- package/dist/ui/screen.js +70 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +23 -0
- package/dist/ui/use-startup-notices.d.ts +62 -5
- package/dist/ui/use-startup-notices.js +81 -17
- package/dist/ui/use-wheel-routing.js +8 -0
- package/dist/ui/win-vt-force.d.ts +136 -0
- package/dist/ui/win-vt-force.js +237 -0
- package/package.json +4 -2
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';
|
|
@@ -47,10 +47,14 @@ import { setFrameStatsProvider } from './commands/perf.js';
|
|
|
47
47
|
import { tryCreateMouseFilter } from './input/stdin-mouse-filter.js';
|
|
48
48
|
import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
|
|
49
49
|
import { supportsWindowsVtInput } from './ui/win-vt-input.js';
|
|
50
|
+
import { forceWindowsVtInput } from './ui/win-vt-force.js';
|
|
50
51
|
import { readExitSnapshot } from './ui/exit-snapshot.js';
|
|
51
52
|
import { renderTranscriptText } from './ui/transcript-text.js';
|
|
52
53
|
import { detectCapabilities } from './ui/capabilities.js';
|
|
53
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';
|
|
54
58
|
import { DENY_ALL_HUMAN_INPUT } from './tools/human-input.js';
|
|
55
59
|
// A VALUE import, and the one deliberate exception to the paragraph above
|
|
56
60
|
// (cli-auto-update-hardening P2-7). `boot/guard.js` statically pulls
|
|
@@ -104,6 +108,11 @@ function toFlags(opts) {
|
|
|
104
108
|
// Without this line the flag never reaches `CliFlags` and `--no-mouse` is
|
|
105
109
|
// inert — a failure with no compile error and no runtime error (P1-2).
|
|
106
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,
|
|
107
116
|
skills: opts.skills,
|
|
108
117
|
skill: opts.skill,
|
|
109
118
|
skillsYes: opts.skillsYes,
|
|
@@ -170,6 +179,15 @@ function toFlags(opts) {
|
|
|
170
179
|
// means `--no-update` reports nothing, does nothing, and leaves the updater
|
|
171
180
|
// running.
|
|
172
181
|
update: opts.update,
|
|
182
|
+
// Site three of three for `--compaction` / `--no-compaction` — NINE
|
|
183
|
+
// features, one trap. A flag needs the `.option()` declaration below, a field
|
|
184
|
+
// on `CliFlags`, AND a line here; miss this one and the flag is inert with no
|
|
185
|
+
// compile error and no runtime error. It bites here for the same reason it
|
|
186
|
+
// does one field up: this is the KILL SWITCH for a feature that spends the
|
|
187
|
+
// user's money on a summarizer, so a missing line means `--no-compaction`
|
|
188
|
+
// reports nothing, does nothing, and leaves compaction armed.
|
|
189
|
+
compaction: opts.compaction,
|
|
190
|
+
compactionThreshold: opts.compactionThreshold,
|
|
173
191
|
};
|
|
174
192
|
}
|
|
175
193
|
// ---------------------------------------------------------------------------
|
|
@@ -285,6 +303,69 @@ function makeExecController(flags, deps) {
|
|
|
285
303
|
*/
|
|
286
304
|
const FRAME_FALLBACK_NOTICE = 'Frame diffing lost sync with the terminal and fell back to a full repaint. ' +
|
|
287
305
|
'If the display looks wrong, restart with `--no-diff-render`.';
|
|
306
|
+
/**
|
|
307
|
+
* Make this console deliver `CSI Z`, on a Node that will not do it for us
|
|
308
|
+
* (shift-tab-mode-toggle-still-dead-on-windows, C1).
|
|
309
|
+
*
|
|
310
|
+
* Returns whether the console now reports; never throws. `false` means nothing
|
|
311
|
+
* changed and the session runs exactly as it did before this feature existed,
|
|
312
|
+
* with `MODE_TOGGLE_KEYS.fallback` carrying the mode toggle instead.
|
|
313
|
+
*
|
|
314
|
+
* RAW MODE IS RAISED HERE, AND THE ORDER IS THE ENTIRE MECHANISM. libuv rewrites
|
|
315
|
+
* the whole console mode on every raw-mode TRANSITION, and its pre-22.17 path
|
|
316
|
+
* does not know about this bit - so setting the bit first and letting Ink raise
|
|
317
|
+
* raw mode afterwards clears it again, measured (analysis section 3.3). Raising
|
|
318
|
+
* it first inverts that: `uv_tty_set_mode` returns early when the mode is
|
|
319
|
+
* unchanged, so Ink's own `setRawMode(true)` at mount is a no-op and the bit
|
|
320
|
+
* survives to the first keypress.
|
|
321
|
+
*
|
|
322
|
+
* That same asymmetry is why there is no restore call anywhere. Ink lowers raw
|
|
323
|
+
* mode when it unmounts, and that transition IS the restore - it is the only
|
|
324
|
+
* thing that ever clears the bit, since neither Windows nor the console does
|
|
325
|
+
* (section 3.5b). The `exit` hook exists for the paths that never unmount
|
|
326
|
+
* (`taskkill`, a closed terminal window, a crash), which are exactly the
|
|
327
|
+
* unattended ones where a console left in VT-input mode has nobody to notice.
|
|
328
|
+
*
|
|
329
|
+
* WHY THIS RUNS BEFORE `render()` rather than from an effect inside `App`: the
|
|
330
|
+
* answer gates `wantMouse`, which decides the `?1000h` write and the filter
|
|
331
|
+
* BEFORE Ink mounts. Learning the truth one tick too late would mean the wheel
|
|
332
|
+
* stays off for the whole session on a console we just repaired.
|
|
333
|
+
*/
|
|
334
|
+
function forceVtInputForThisConsole() {
|
|
335
|
+
if (process.platform !== 'win32' || !process.stdin.isTTY)
|
|
336
|
+
return false;
|
|
337
|
+
const logger = getLogger();
|
|
338
|
+
try {
|
|
339
|
+
process.stdin.setRawMode(true);
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
// A TTY that refuses raw mode is not a console we can repair; C2 carries it.
|
|
343
|
+
logger.warn('cli', 'vt_input_rawmode_failed', { error: err.message });
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
process.on('exit', () => {
|
|
347
|
+
try {
|
|
348
|
+
process.stdin.setRawMode(false);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
/* Stream already torn down — nothing left to restore. */
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
const result = forceWindowsVtInput();
|
|
355
|
+
if (result.ok) {
|
|
356
|
+
logger.info('cli', 'vt_input_forced', { before: result.before, after: result.after });
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
// Constrained Language Mode kills `Add-Type`, an old conhost drops the bit it
|
|
360
|
+
// does not implement, and `wrong_console` means we configured somebody else's
|
|
361
|
+
// console. All three are ordinary on the fleets this bug keeps landing on, so
|
|
362
|
+
// they degrade in silence and only the log says which one happened.
|
|
363
|
+
logger.warn('cli', 'vt_input_force_failed', {
|
|
364
|
+
reason: result.reason,
|
|
365
|
+
...(result.detail ? { detail: result.detail } : {}),
|
|
366
|
+
});
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
288
369
|
/**
|
|
289
370
|
* The interactive TUI — and the ONLY place that may touch the screen.
|
|
290
371
|
*
|
|
@@ -339,7 +420,15 @@ function runInteractive(flags, extras = {}) {
|
|
|
339
420
|
// filter itself, and the `mouseSource` prop that decides whether the user is
|
|
340
421
|
// advised about a mode that is not running. Wiring any of them separately
|
|
341
422
|
// re-opens the gap where the app claims a capability it does not have.
|
|
342
|
-
|
|
423
|
+
//
|
|
424
|
+
// AND `vtInputSupported` IS NOW A MEASUREMENT, NOT A PREDICTION
|
|
425
|
+
// (shift-tab-mode-toggle-still-dead-on-windows, C1). It used to be exactly
|
|
426
|
+
// `supportsWindowsVtInput(...)` - "is this Node new enough to turn the bit on
|
|
427
|
+
// for us?". We now turn it on ourselves when it is not, so continuing to gate
|
|
428
|
+
// on the Node version would switch the wheel off, and keep the warning up, on
|
|
429
|
+
// a console this process just repaired.
|
|
430
|
+
const vtInputByNode = supportsWindowsVtInput(process.platform, process.versions.node);
|
|
431
|
+
const vtInputSupported = vtInputByNode || forceVtInputForThisConsole();
|
|
343
432
|
const wantMouse = mode === 'fullscreen' &&
|
|
344
433
|
config.mouse &&
|
|
345
434
|
!!process.stdout.isTTY &&
|
|
@@ -349,6 +438,20 @@ function runInteractive(flags, extras = {}) {
|
|
|
349
438
|
? tryCreateMouseFilter(process.stdin, (reason) => logger.warn('cli', 'mouse_filter_failed', { reason }))
|
|
350
439
|
: null;
|
|
351
440
|
const disposeMouse = () => mouseFilter?.dispose();
|
|
441
|
+
// --- Drag-select (tui-selection-and-scroll-follow §4.4). ------------------
|
|
442
|
+
//
|
|
443
|
+
// ONE EXPRESSION, READ BY FOUR THINGS, and that is the same discipline
|
|
444
|
+
// `wantMouse` above follows for the same reason: the `?1002h` write, the
|
|
445
|
+
// controller, the differ's `decorate` hook and the startup notice must never
|
|
446
|
+
// disagree about whether drag-select is running. With it false the session is
|
|
447
|
+
// byte-identical to a pre-feature build — no extra escape on the wire (AC-8),
|
|
448
|
+
// no controller, no `decorate`, no mirror.
|
|
449
|
+
const wantSelect = mouseFilter !== null && config.mouseSelect;
|
|
450
|
+
const selectionBridge = {
|
|
451
|
+
controller: null,
|
|
452
|
+
onCopied: null,
|
|
453
|
+
requestRedraw: null,
|
|
454
|
+
};
|
|
352
455
|
// --- Frame differ (tui-input-flicker-fix §3.2 / §4.2). -------------------
|
|
353
456
|
//
|
|
354
457
|
// FULL-SCREEN ONLY, and that is invariant I-3 rather than caution: inline mode
|
|
@@ -371,6 +474,25 @@ function runInteractive(flags, extras = {}) {
|
|
|
371
474
|
// edge inside the differ: a notice per fallback would be a repaint
|
|
372
475
|
// storm triggered by a repaint problem (§5.6).
|
|
373
476
|
onFirstFallback: () => console.warn(FRAME_FALLBACK_NOTICE),
|
|
477
|
+
// ABSENT ENTIRELY without drag-select, not a no-op function: the
|
|
478
|
+
// differ's identity path then allocates nothing and copies nothing,
|
|
479
|
+
// which is what makes `mouseSelect: false` a statement about the code
|
|
480
|
+
// rather than about how cheap an empty function is.
|
|
481
|
+
...(wantSelect
|
|
482
|
+
? {
|
|
483
|
+
decorate: (lines) => selectionBridge.controller?.decorate(lines) ?? lines,
|
|
484
|
+
// P1-7: a pass-through frame writes the RAW Ink frame verbatim,
|
|
485
|
+
// so the highlight is wiped off the terminal while the
|
|
486
|
+
// controller still believes it is painted. Deferred out of the
|
|
487
|
+
// current stack because this fires from inside `transform`,
|
|
488
|
+
// which is inside Ink's own `stdout.write`.
|
|
489
|
+
onInvalidate: () => {
|
|
490
|
+
const controller = selectionBridge.controller;
|
|
491
|
+
if (controller)
|
|
492
|
+
setImmediate(() => controller.clear());
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
: {}),
|
|
374
496
|
}))
|
|
375
497
|
: null;
|
|
376
498
|
// `null` when no writer was built, so `/perf` can say "diff render off" rather
|
|
@@ -406,8 +528,9 @@ function runInteractive(flags, extras = {}) {
|
|
|
406
528
|
if (mode === 'fullscreen') {
|
|
407
529
|
// `mouse` here means "a filter is installed", NOT "the user wants mouse
|
|
408
530
|
// support" — see `AltScreenOptions`. `screen.ts` must never derive it for
|
|
409
|
-
// itself, or a later refactor re-opens the gap I-8 closes.
|
|
410
|
-
|
|
531
|
+
// itself, or a later refactor re-opens the gap I-8 closes. `motion` carries
|
|
532
|
+
// the same discipline for `?1002h`: already decided, never re-derived.
|
|
533
|
+
screen = enterAltScreen(process.stdout, { mouse: mouseFilter !== null, motion: wantSelect });
|
|
411
534
|
const restore = () => screen?.restore();
|
|
412
535
|
// Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
|
|
413
536
|
// - `waitUntilExit().then()` is a microtask and may never be reached when
|
|
@@ -444,6 +567,115 @@ function runInteractive(flags, extras = {}) {
|
|
|
444
567
|
// `CI` is excluded on top of the two TTY tests because a CI runner with a pty
|
|
445
568
|
// allocated is otherwise a machine that silently mutates its own global
|
|
446
569
|
// toolchain mid-pipeline (D-11).
|
|
570
|
+
// --- The foreign-write door (§4.4.5). ------------------------------------
|
|
571
|
+
//
|
|
572
|
+
// Where bytes that are OURS but are NOT a frame go — today, the OSC 52
|
|
573
|
+
// clipboard sequence. THREE SHAPES, ONE DOOR, and the third one is the reason
|
|
574
|
+
// this is a resolved value rather than an inline arrow:
|
|
575
|
+
//
|
|
576
|
+
// * with the differ in front of stdout it MUST be `frameWriter.writeForeign`,
|
|
577
|
+
// which invalidates the cache and skips the fallback accounting — through
|
|
578
|
+
// the proxy the same bytes are an unrecognised chunk, which is
|
|
579
|
+
// `passThrough(true)`, which prints `FRAME_FALLBACK_NOTICE` at a user who
|
|
580
|
+
// did nothing but copy something (P1-6);
|
|
581
|
+
// * without one — `--no-diff-render`, and inline mode, which never builds a
|
|
582
|
+
// differ at all — stdout IS the real stream, so a direct write is both safe
|
|
583
|
+
// and what §4.4.5 prescribes. OSC 52 emits no newline, so Ink's own line
|
|
584
|
+
// accounting is untouched and there is nothing to invalidate;
|
|
585
|
+
// * on a non-TTY there is no terminal to accept it, so the door is ABSENT.
|
|
586
|
+
//
|
|
587
|
+
// It must never be a function that accepts a chunk and writes NOWHERE.
|
|
588
|
+
// `copyText` reports `'osc52'` whenever its door took the text, so a silent
|
|
589
|
+
// no-op door — which `frameWriter?.writeForeign(chunk)` is under
|
|
590
|
+
// `--no-diff-render` — makes the toast name a mechanism that was never
|
|
591
|
+
// attempted. Naming the mechanism honestly is the entire reason that return
|
|
592
|
+
// value exists (R-7), and a door that lies is worse than no door.
|
|
593
|
+
const writeForeign = frameWriter
|
|
594
|
+
? (text) => frameWriter.writeForeign(text)
|
|
595
|
+
: process.stdout.isTTY
|
|
596
|
+
? (text) => {
|
|
597
|
+
try {
|
|
598
|
+
process.stdout.write(text);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
// A closed or broken stdout must never turn a copy into a crash.
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
: null;
|
|
605
|
+
// --- The selection controller (§4.4). ------------------------------------
|
|
606
|
+
//
|
|
607
|
+
// BUILT AFTER `enterAltScreen` AND BEFORE `render()`. After, because it must
|
|
608
|
+
// not exist on a session that never asked the terminal to report motion;
|
|
609
|
+
// before, because `decorate` is read by the differ from the very first frame
|
|
610
|
+
// Ink produces.
|
|
611
|
+
//
|
|
612
|
+
// `mouseCaptured` is the run-time answer for `/mouse`, and it lives here rather
|
|
613
|
+
// than inside `screen.ts`'s closure because `App` needs to READ it and the
|
|
614
|
+
// handle's own flag is private (it has to be — `restore()` reads it, and
|
|
615
|
+
// exposing a setter would be a second way to get the two out of step).
|
|
616
|
+
let mouseCaptured = mouseFilter !== null;
|
|
617
|
+
if (wantSelect && mouseFilter) {
|
|
618
|
+
selectionBridge.controller = createSelectionController({
|
|
619
|
+
source: mouseFilter.source,
|
|
620
|
+
// The frame writer is absent under `--no-diff-render`, where there is no
|
|
621
|
+
// cache to repaint from at all; the controller then falls back to asking
|
|
622
|
+
// `App` for a React redraw on every drag flush, which is slower and
|
|
623
|
+
// correct. That is rung 2 of the fail-safe ladder doing its job.
|
|
624
|
+
repaint: () => frameWriter?.repaint() ?? false,
|
|
625
|
+
requestRedraw: () => selectionBridge.requestRedraw?.(),
|
|
626
|
+
// `?? undefined` and not a wrapper: `copyText` skips OSC 52 entirely when
|
|
627
|
+
// it has no door, and reports `'native'`/`'none'` accordingly.
|
|
628
|
+
copy: (text) => copyText(text, writeForeign ?? undefined),
|
|
629
|
+
onCopied: (via, lines, chars) => selectionBridge.onCopied?.(via, lines, chars),
|
|
630
|
+
// Read at PAINT time, not captured: `/theme` rebuilds the theme mid-session.
|
|
631
|
+
theme: () => getTheme(config.theme, detectCapabilities(process.env, process.stdout)),
|
|
632
|
+
caps: detectCapabilities(process.env, process.stdout),
|
|
633
|
+
cols: () => process.stdout.columns ?? 80,
|
|
634
|
+
// The STATIC half of the gate. The dynamic half — "no overlay is open" —
|
|
635
|
+
// belongs to `App`, which owns that state and calls `setEnabled`.
|
|
636
|
+
isSelectable: () => mouseCaptured,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const disposeSelection = () => {
|
|
640
|
+
selectionBridge.controller?.dispose();
|
|
641
|
+
selectionBridge.controller = null;
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* The terminal-facing services `App` cannot build for itself (§4.4).
|
|
645
|
+
*
|
|
646
|
+
* PRESENT IN INLINE MODE TOO, and only for the foreign-write door: §4.4.5
|
|
647
|
+
* routes `/copy` through `ui/clipboard.ts` precisely so it gains OSC 52 — the
|
|
648
|
+
* one mechanism that reaches the clipboard of the machine the USER is sitting
|
|
649
|
+
* at over SSH — and gating the whole bridge on full-screen would have handed
|
|
650
|
+
* that back only to full-screen users while the command is available in both.
|
|
651
|
+
* Every OTHER field stays absent there, so the inline tree is unchanged: there
|
|
652
|
+
* is no filter, hence no `setMouseCapture` and no `ctx.mouse`; `mouseSelect`
|
|
653
|
+
* is `wantSelect`, which is already false without one; and there is no
|
|
654
|
+
* controller, so no `selection`.
|
|
655
|
+
*/
|
|
656
|
+
const terminalBridge = mode === 'fullscreen' || writeForeign
|
|
657
|
+
? {
|
|
658
|
+
mouseSelect: wantSelect,
|
|
659
|
+
...(mouseFilter
|
|
660
|
+
? {
|
|
661
|
+
setMouseCapture: (on) => {
|
|
662
|
+
screen?.setMouseCapture(on);
|
|
663
|
+
mouseCaptured = on;
|
|
664
|
+
// A released mouse cannot be dragging, and a selection left
|
|
665
|
+
// painted over a screen the terminal is now selecting on its
|
|
666
|
+
// own would be two highlights claiming the same rows.
|
|
667
|
+
if (!on)
|
|
668
|
+
selectionBridge.controller?.clear();
|
|
669
|
+
},
|
|
670
|
+
isMouseCaptured: () => mouseCaptured,
|
|
671
|
+
}
|
|
672
|
+
: {}),
|
|
673
|
+
// The same door the controller copies through, so `/copy` and a
|
|
674
|
+
// drag-release can never disagree about which stream OSC 52 goes to.
|
|
675
|
+
...(writeForeign ? { writeForeign } : {}),
|
|
676
|
+
...(selectionBridge.controller ? { selection: selectionBridge } : {}),
|
|
677
|
+
}
|
|
678
|
+
: undefined;
|
|
447
679
|
const updateBridge = { service: null, onAttach: null };
|
|
448
680
|
let updateService = null;
|
|
449
681
|
const updateEligible = !!process.stdout.isTTY &&
|
|
@@ -456,7 +688,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
456
688
|
// the same tree on every machine — reading `process.versions.node` from
|
|
457
689
|
// a component would make the notice appear or vanish according to which
|
|
458
690
|
// Node the developer happened to have installed.
|
|
459
|
-
vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined }),
|
|
691
|
+
vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined, terminal: terminalBridge }),
|
|
460
692
|
// Full-screen owns console.* itself (I-4): Ink's patchConsole writes
|
|
461
693
|
// straight to stdout and permanently shifts the fixed frame's accounting.
|
|
462
694
|
{
|
|
@@ -525,6 +757,11 @@ function runInteractive(flags, extras = {}) {
|
|
|
525
757
|
.waitUntilExit()
|
|
526
758
|
.then(() => {
|
|
527
759
|
screen?.restore();
|
|
760
|
+
// BEFORE `disposeMouse()`, which clears the listener set it subscribed to,
|
|
761
|
+
// and after `restore()` for the reason that call records: the terminal must
|
|
762
|
+
// stop reporting first, and anything already in flight is still consumed by
|
|
763
|
+
// a live filter.
|
|
764
|
+
disposeSelection();
|
|
528
765
|
disposeMouse();
|
|
529
766
|
// BOTH BRANCHES, the same discipline `controller.dispose()` and
|
|
530
767
|
// `updateService?.dispose()` already follow. It drops the `'resize'`
|
|
@@ -549,6 +786,11 @@ function runInteractive(flags, extras = {}) {
|
|
|
549
786
|
})
|
|
550
787
|
.catch(() => {
|
|
551
788
|
screen?.restore();
|
|
789
|
+
// BEFORE `disposeMouse()`, which clears the listener set it subscribed to,
|
|
790
|
+
// and after `restore()` for the reason that call records: the terminal must
|
|
791
|
+
// stop reporting first, and anything already in flight is still consumed by
|
|
792
|
+
// a live filter.
|
|
793
|
+
disposeSelection();
|
|
552
794
|
disposeMouse();
|
|
553
795
|
disposeFrameWriter();
|
|
554
796
|
controller.dispose();
|
|
@@ -665,6 +907,11 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
665
907
|
// falls through every case, writes nothing, and still prints
|
|
666
908
|
// `Set mouse = false` (P1-2) -- exactly what happened to `density`/`hints`.
|
|
667
909
|
'mouse',
|
|
910
|
+
// Both cased in the switch below, for the reason the note above `mouse`
|
|
911
|
+
// records: a key listed here but absent there writes nothing and still prints
|
|
912
|
+
// `Set mouseSelect = false`.
|
|
913
|
+
'mouseSelect',
|
|
914
|
+
'scrollResumeMs',
|
|
668
915
|
'historyEnabled',
|
|
669
916
|
'planModeDefault',
|
|
670
917
|
'planModeMaxAskRounds',
|
|
@@ -691,6 +938,11 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
691
938
|
...FAST_CONFIG_SET_KEYS,
|
|
692
939
|
// `update.*` keys are handled by `applyUpdateConfigSet`, same reasoning again.
|
|
693
940
|
...UPDATE_CONFIG_SET_KEYS,
|
|
941
|
+
// `compaction.*` keys are handled by `applyCompactionConfigSet`, same
|
|
942
|
+
// reasoning again — and this set is what makes the kill switch reachable from
|
|
943
|
+
// a script at all: `config list` enumerates the section dynamically and so
|
|
944
|
+
// shows these keys whether they are here or not, while `config set` does not.
|
|
945
|
+
...COMPACTION_CONFIG_SET_KEYS,
|
|
694
946
|
]);
|
|
695
947
|
/**
|
|
696
948
|
* `aragon config set maxTokens <n|auto|default>`.
|
|
@@ -777,6 +1029,18 @@ function runConfigSet(key, value) {
|
|
|
777
1029
|
process.stdout.write(`Set ${key} = ${String(merged.update[field])}\n`);
|
|
778
1030
|
return;
|
|
779
1031
|
}
|
|
1032
|
+
const compactionPatch = applyCompactionConfigSet(key, value);
|
|
1033
|
+
if (compactionPatch) {
|
|
1034
|
+
// Echoed from the STORED value, so `config set compaction.threshold 0.2`
|
|
1035
|
+
// prints the clamped 0.5 rather than reporting a number that was never
|
|
1036
|
+
// written — the rule `team.*`, `retry.*`, `fast.*` and `update.*` each state
|
|
1037
|
+
// above. It matters most for `warnThreshold`, which is clamped against the
|
|
1038
|
+
// stored `threshold` and so can move for a reason the caller cannot see.
|
|
1039
|
+
const merged = updatePersistedConfig(compactionPatch);
|
|
1040
|
+
const field = key.slice('compaction.'.length);
|
|
1041
|
+
process.stdout.write(`Set ${key} = ${String(merged.compaction[field])}\n`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
780
1044
|
const patch = {};
|
|
781
1045
|
switch (key) {
|
|
782
1046
|
case 'provider':
|
|
@@ -842,6 +1106,16 @@ function runConfigSet(key, value) {
|
|
|
842
1106
|
case 'mouse':
|
|
843
1107
|
patch.mouse = value === 'true' || value === '1';
|
|
844
1108
|
break;
|
|
1109
|
+
case 'mouseSelect':
|
|
1110
|
+
patch.mouseSelect = value === 'true' || value === '1';
|
|
1111
|
+
break;
|
|
1112
|
+
// Clamped rather than rejected, for the same reason `planModeDefault` below
|
|
1113
|
+
// records: hardening only the READ path leaves a bad value on disk that
|
|
1114
|
+
// reverts to the default on every launch, which presents as "my setting
|
|
1115
|
+
// won't stick". `0` is a legal value here and its documented off switch.
|
|
1116
|
+
case 'scrollResumeMs':
|
|
1117
|
+
patch.scrollResumeMs = clampScrollResumeMs(value, DEFAULT_SCROLL_RESUME_MS);
|
|
1118
|
+
break;
|
|
845
1119
|
// Turning this off stops new prompts being recorded; it does NOT delete
|
|
846
1120
|
// what is already in `prompt-history.jsonl` (that is `aragon history clear`).
|
|
847
1121
|
case 'historyEnabled':
|
|
@@ -1100,6 +1374,13 @@ function buildProgram() {
|
|
|
1100
1374
|
// Positive form first, same tri-state reason as `--fullscreen` above.
|
|
1101
1375
|
.option('--mouse', 'Wheel scrolls the transcript (full-screen mode)')
|
|
1102
1376
|
.option('--no-mouse', 'Leave the mouse to the terminal (the wheel does nothing)')
|
|
1377
|
+
// Positive form FIRST, for the tri-state reason `--fullscreen` records
|
|
1378
|
+
// below: `mouseSelect` is persisted and defaults to `true`, so a lone
|
|
1379
|
+
// `--no-mouse-select` would make commander default `opts.mouseSelect` to
|
|
1380
|
+
// `true` — indistinguishable from silence, and silently overriding a stored
|
|
1381
|
+
// `false` on every run that passes no flag at all.
|
|
1382
|
+
.option('--mouse-select', 'Drag with the mouse to select text; releasing copies it')
|
|
1383
|
+
.option('--no-mouse-select', 'Keep wheel scrolling, but leave drag-select off')
|
|
1103
1384
|
// Declaring `--fullscreen` BEFORE `--no-fullscreen` keeps the default
|
|
1104
1385
|
// `undefined` instead of `true`, which is what makes the tri-state work:
|
|
1105
1386
|
// "no opinion" must stay distinguishable from "force it".
|
|
@@ -1156,6 +1437,16 @@ function buildProgram() {
|
|
|
1156
1437
|
// feature that installs software would quietly un-set itself (C-11).
|
|
1157
1438
|
.option('--update', 'Check for and install updates in the background (the default)')
|
|
1158
1439
|
.option('--no-update', 'Disable auto-update for this session')
|
|
1440
|
+
// BOTH FORMS, positive first, for the reason recorded eight times above —
|
|
1441
|
+
// and this pair shares `--update`'s polarity, which is the worst one.
|
|
1442
|
+
// `compaction.enabled` is PERSISTED and defaults to `true`, so a lone
|
|
1443
|
+
// `--no-compaction` would make commander default `opts.compaction` to `true`
|
|
1444
|
+
// and silently overwrite a stored `false` on EVERY run that passed no flag at
|
|
1445
|
+
// all: the kill switch for a feature that spends the user's money on a
|
|
1446
|
+
// summarizer would quietly un-set itself (C-13 / P1-5).
|
|
1447
|
+
.option('--compaction', 'Compact the conversation when the context window fills (the default)')
|
|
1448
|
+
.option('--no-compaction', 'Never compact; a full context window ends the run as before')
|
|
1449
|
+
.option('--compaction-threshold <n>', 'Occupancy that triggers compaction for this run: 0.9 or 90% (0.5-0.95)')
|
|
1159
1450
|
.option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
|
|
1160
1451
|
.option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
|
|
1161
1452
|
.option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
* /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
|
|
4
4
|
* /resume /copy /exit (/quit), plus the /team, /fast and /todo subsystems.
|
|
5
5
|
*/
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
6
|
import { existsSync, statSync } from 'node:fs';
|
|
8
7
|
import { isAbsolute, resolve } from 'node:path';
|
|
9
|
-
import process from 'node:process';
|
|
10
8
|
import { resolveOutputTokens } from '@aragon-agent/core';
|
|
11
9
|
import { clampThinkingLevel, clampTheme, isAdapterProvider, isThemeName, parseMaxTokensInput, ADAPTER_PROVIDERS, DEFAULT_MAX_TOKENS, HARD_MAX_RETRIES, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
|
|
12
10
|
import { computeCost, formatCost, formatTokens } from '../agent/usage.js';
|
|
@@ -16,7 +14,9 @@ import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
|
16
14
|
import { loadConfig } from '../config/load.js';
|
|
17
15
|
import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
|
|
18
16
|
import { getLogger } from '../logging/logger.js';
|
|
17
|
+
import { copyText } from '../ui/clipboard.js';
|
|
19
18
|
import { perfCommand } from './perf.js';
|
|
19
|
+
import { runCompactCommand } from '../compaction/command.js';
|
|
20
20
|
/**
|
|
21
21
|
* `/fast status` — THE GUARANTEED REPORTING SURFACE for the tier (§4.4 / R-8).
|
|
22
22
|
*
|
|
@@ -801,8 +801,21 @@ const COMMANDS = [
|
|
|
801
801
|
ctx.notify('warn', `Unknown argument "${verb}" - use /retry [show|on|off|max <n>].`);
|
|
802
802
|
},
|
|
803
803
|
},
|
|
804
|
+
{
|
|
805
|
+
// REGISTRATION ONLY. The body lives in `compaction/command.ts` because this
|
|
806
|
+
// file is already past the 1000-line guideline and `/compact` has six forms
|
|
807
|
+
// plus the longest status report in the package (C-16 / P1-12).
|
|
808
|
+
name: 'compact',
|
|
809
|
+
description: 'Context compaction: status | on | off | threshold <n> | keep <n> | <instructions>',
|
|
810
|
+
run: (ctx) => runCompactCommand(ctx),
|
|
811
|
+
},
|
|
804
812
|
{
|
|
805
813
|
name: 'expand',
|
|
814
|
+
// DELIBERATELY LEFT TOOL-ONLY, and the asymmetry with Ctrl+O is a decision
|
|
815
|
+
// rather than an oversight (§6.3). Its description says "a recent tool card",
|
|
816
|
+
// it takes an N-from-last index over tool cards specifically, and widening it
|
|
817
|
+
// would change what `/expand 3` means for every existing user. Ctrl+O is the
|
|
818
|
+
// general expansion surface — that is where `'compaction'` was added.
|
|
806
819
|
description: 'Expand / collapse a recent tool card (default: last)',
|
|
807
820
|
run: (ctx) => {
|
|
808
821
|
const toolEntries = ctx.state.entries.filter((e) => e.kind === 'tool');
|
|
@@ -942,8 +955,60 @@ const COMMANDS = [
|
|
|
942
955
|
ctx.notify('warn', 'No assistant message to copy.');
|
|
943
956
|
return;
|
|
944
957
|
}
|
|
945
|
-
|
|
946
|
-
|
|
958
|
+
// ROUTED THROUGH `ui/clipboard.ts` (§4.4.5). The duplicate spawn helper this
|
|
959
|
+
// command used to own is gone, and the command gained SSH support on the
|
|
960
|
+
// way: OSC 52 reaches the clipboard of the machine the USER is sitting at,
|
|
961
|
+
// which the local `pbcopy` never could.
|
|
962
|
+
//
|
|
963
|
+
// `ctx.writeForeign` and not `stdout.write` — see the field's comment.
|
|
964
|
+
const via = copyText(last.text, ctx.writeForeign);
|
|
965
|
+
if (via === 'none') {
|
|
966
|
+
ctx.toast('warn', 'Clipboard not available.');
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
// NAMES THE MECHANISM RATHER THAN CLAIMING SUCCESS: neither path is
|
|
970
|
+
// detectable, and tmux without `set -g set-clipboard on` swallows OSC 52
|
|
971
|
+
// silently (R-7).
|
|
972
|
+
ctx.toast('success', via === 'osc52' ? 'Sent to the terminal clipboard.' : 'Sent to the system clipboard.');
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: 'mouse',
|
|
977
|
+
description: 'Release the mouse to the terminal, or take it back',
|
|
978
|
+
run: (ctx) => {
|
|
979
|
+
const port = ctx.mouse;
|
|
980
|
+
if (!port) {
|
|
981
|
+
// Inline mode, `--no-mouse`, a non-TTY, or a console that cannot deliver
|
|
982
|
+
// reports. Saying so beats pretending to toggle something that is not on.
|
|
983
|
+
ctx.notify('info', 'The mouse is not captured in this session, so there is nothing to release.');
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const arg = ctx.args.trim().toLowerCase();
|
|
987
|
+
if (arg === 'off') {
|
|
988
|
+
port.setCapture(false);
|
|
989
|
+
// NAMING THE COST IS THE HONEST THING TO DO, and it is one clause. The
|
|
990
|
+
// wheel goes INERT rather than back to scrolling: alternate scroll is
|
|
991
|
+
// saved and disabled alongside the capture, because otherwise every notch
|
|
992
|
+
// becomes a burst of arrow keys that `PromptInput` reads as prompt-history
|
|
993
|
+
// recall (I-10 / P1-1).
|
|
994
|
+
ctx.toast('info', 'Mouse released to the terminal - the wheel will not scroll. /mouse on gives it back.');
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (arg === 'on') {
|
|
998
|
+
port.setCapture(true);
|
|
999
|
+
ctx.toast('info', 'Mouse captured - the wheel scrolls the transcript again.');
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const captured = port.captured();
|
|
1003
|
+
const lines = [
|
|
1004
|
+
`Mouse capture: ${captured ? 'on' : 'off'}`,
|
|
1005
|
+
` drag-select ${port.selectEnabled() ? 'on' : 'off (mouseSelect is false)'}`,
|
|
1006
|
+
' /mouse off hand the mouse to your terminal for this session',
|
|
1007
|
+
' /mouse on take it back',
|
|
1008
|
+
' --no-mouse never capture it (also ARAGON_MOUSE=0)',
|
|
1009
|
+
' aragon config set mouseSelect false keep the wheel, drop drag-select',
|
|
1010
|
+
];
|
|
1011
|
+
ctx.notify('info', lines.join('\n'));
|
|
947
1012
|
},
|
|
948
1013
|
},
|
|
949
1014
|
{
|
|
@@ -1006,22 +1071,7 @@ export const BUILTIN_COMMAND_NAMES = COMMANDS.flatMap((c) => [
|
|
|
1006
1071
|
c.name,
|
|
1007
1072
|
...(c.aliases ?? []),
|
|
1008
1073
|
]).sort();
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
: process.platform === 'darwin'
|
|
1014
|
-
? { file: 'pbcopy', args: [] }
|
|
1015
|
-
: { file: 'xclip', args: ['-selection', 'clipboard'] };
|
|
1016
|
-
try {
|
|
1017
|
-
const child = spawn(cmd.file, cmd.args, { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
1018
|
-
child.on('error', () => {
|
|
1019
|
-
/* swallow — clipboard is best-effort */
|
|
1020
|
-
});
|
|
1021
|
-
child.stdin?.end(text);
|
|
1022
|
-
return true;
|
|
1023
|
-
}
|
|
1024
|
-
catch {
|
|
1025
|
-
return false;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1074
|
+
// The `copyToClipboard` helper that used to live here moved into
|
|
1075
|
+
// `ui/clipboard.ts` (tui-selection-and-scroll-follow §4.4.5), unchanged, so that
|
|
1076
|
+
// `/copy` and the drag-select release share ONE clipboard path instead of two
|
|
1077
|
+
// that can drift.
|
|
@@ -87,6 +87,33 @@ export interface CommandContext {
|
|
|
87
87
|
effective: AgentMode;
|
|
88
88
|
pending: AgentMode | null;
|
|
89
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* Mouse capture, for `/mouse` (tui-selection-and-scroll-follow §6.2 / G3).
|
|
92
|
+
*
|
|
93
|
+
* OPTIONAL, and legitimately absent: inline mode, `--no-mouse`, a non-TTY, a
|
|
94
|
+
* Windows console that cannot deliver reports, and every test that builds this
|
|
95
|
+
* context by literal. `/mouse` has to have something honest to say when it is
|
|
96
|
+
* missing, which is the same reason `update` above it is optional.
|
|
97
|
+
*/
|
|
98
|
+
mouse?: MouseCommandPort;
|
|
99
|
+
/**
|
|
100
|
+
* The foreign-write door, for `/copy`'s OSC 52 half (§4.4.5).
|
|
101
|
+
*
|
|
102
|
+
* NOT `process.stdout.write`. In full-screen mode stdout is a `Proxy` in front
|
|
103
|
+
* of the frame differ, and an unrecognised chunk there is counted as a foreign
|
|
104
|
+
* write and prints a diagnostic at the user the first time they copy anything
|
|
105
|
+
* (P1-6). Absent in inline mode and with `--no-diff-render`, where the real
|
|
106
|
+
* stream is handed over directly.
|
|
107
|
+
*/
|
|
108
|
+
writeForeign?: (text: string) => void;
|
|
109
|
+
}
|
|
110
|
+
/** What `/mouse` can see and do. Read at command time, never captured. */
|
|
111
|
+
export interface MouseCommandPort {
|
|
112
|
+
/** Whether the CLI is holding the mouse right now. */
|
|
113
|
+
captured(): boolean;
|
|
114
|
+
/** Whether drag-select is on for this session (`mouseSelect`). */
|
|
115
|
+
selectEnabled(): boolean;
|
|
116
|
+
setCapture(on: boolean): void;
|
|
90
117
|
}
|
|
91
118
|
export interface SlashCommand {
|
|
92
119
|
name: string;
|