@aragon-agent/cli 0.6.12 → 0.6.14

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 (119) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +108 -31
  3. package/dist/agent/controller.d.ts +38 -2
  4. package/dist/agent/controller.js +185 -34
  5. package/dist/agent/model-profile-settings.d.ts +31 -0
  6. package/dist/agent/model-profile-settings.js +208 -0
  7. package/dist/agent/queued-messages.d.ts +12 -0
  8. package/dist/agent/queued-messages.js +18 -0
  9. package/dist/agent/reducer.d.ts +13 -1
  10. package/dist/agent/reducer.js +52 -12
  11. package/dist/boot/guard.d.ts +3 -3
  12. package/dist/boot/guard.js +3 -3
  13. package/dist/cli.js +48 -16
  14. package/dist/commands/builtins.js +66 -39
  15. package/dist/commands/queue-command.d.ts +3 -0
  16. package/dist/commands/queue-command.js +5 -0
  17. package/dist/commands/registry.d.ts +3 -0
  18. package/dist/compaction/child.d.ts +4 -2
  19. package/dist/compaction/child.js +8 -1
  20. package/dist/compaction/compactor.d.ts +7 -2
  21. package/dist/compaction/compactor.js +21 -16
  22. package/dist/compaction/context-command.js +10 -3
  23. package/dist/compaction/meter.js +2 -0
  24. package/dist/compaction/pressure.d.ts +1 -0
  25. package/dist/compaction/pressure.js +1 -0
  26. package/dist/compaction/types.d.ts +2 -0
  27. package/dist/compaction/wiring.d.ts +4 -3
  28. package/dist/compaction/wiring.js +10 -2
  29. package/dist/config/cli-commands.d.ts +2 -0
  30. package/dist/config/cli-commands.js +16 -4
  31. package/dist/config/load.d.ts +5 -2
  32. package/dist/config/load.js +23 -12
  33. package/dist/config/model-profile-resolution.d.ts +20 -0
  34. package/dist/config/model-profile-resolution.js +81 -0
  35. package/dist/config/model-profile-store.d.ts +67 -0
  36. package/dist/config/model-profile-store.js +153 -0
  37. package/dist/config/model-profiles.d.ts +44 -0
  38. package/dist/config/model-profiles.js +121 -0
  39. package/dist/config/schema.d.ts +11 -2
  40. package/dist/config/store.d.ts +11 -1
  41. package/dist/config/store.js +43 -27
  42. package/dist/fast/resolve.d.ts +2 -1
  43. package/dist/fast/resolve.js +6 -4
  44. package/dist/fast/wiring.d.ts +5 -3
  45. package/dist/fast/wiring.js +8 -4
  46. package/dist/input/stdin-filter.js +3 -2
  47. package/dist/logging/install.js +4 -2
  48. package/dist/logging/secret-registry.d.ts +2 -0
  49. package/dist/logging/secret-registry.js +18 -0
  50. package/dist/team/runtime.d.ts +2 -1
  51. package/dist/team/subagent.d.ts +3 -1
  52. package/dist/team/subagent.js +3 -1
  53. package/dist/ui/ActivityLine.d.ts +6 -70
  54. package/dist/ui/ActivityLine.js +12 -34
  55. package/dist/ui/App.d.ts +1 -0
  56. package/dist/ui/App.js +116 -215
  57. package/dist/ui/BottomStatusRow.d.ts +6 -86
  58. package/dist/ui/BottomStatusRow.js +11 -26
  59. package/dist/ui/Composer.d.ts +16 -83
  60. package/dist/ui/Composer.js +9 -100
  61. package/dist/ui/Logo.d.ts +3 -5
  62. package/dist/ui/Logo.js +4 -8
  63. package/dist/ui/PromptInput.d.ts +4 -1
  64. package/dist/ui/PromptInput.js +44 -35
  65. package/dist/ui/QueuePanel.d.ts +8 -0
  66. package/dist/ui/QueuePanel.js +9 -0
  67. package/dist/ui/QueueStatusRow.d.ts +2 -0
  68. package/dist/ui/QueueStatusRow.js +1 -1
  69. package/dist/ui/StatusBar.d.ts +13 -149
  70. package/dist/ui/StatusBar.js +27 -142
  71. package/dist/ui/capabilities.d.ts +2 -0
  72. package/dist/ui/capabilities.js +6 -0
  73. package/dist/ui/clipboard-task.d.ts +29 -0
  74. package/dist/ui/clipboard-task.js +66 -0
  75. package/dist/ui/clipboard.d.ts +29 -50
  76. package/dist/ui/clipboard.js +141 -75
  77. package/dist/ui/color-runtime.d.ts +5 -0
  78. package/dist/ui/color-runtime.js +29 -0
  79. package/dist/ui/composer-limits.d.ts +3 -4
  80. package/dist/ui/composer-limits.js +4 -9
  81. package/dist/ui/composer-rows.d.ts +1 -1
  82. package/dist/ui/composer-rows.js +24 -137
  83. package/dist/ui/editor-navigation.d.ts +45 -0
  84. package/dist/ui/editor-navigation.js +85 -0
  85. package/dist/ui/editor-reducer.d.ts +4 -0
  86. package/dist/ui/editor-reducer.js +29 -15
  87. package/dist/ui/glyphs.js +10 -8
  88. package/dist/ui/ink-runtime.d.ts +3 -0
  89. package/dist/ui/ink-runtime.js +30 -0
  90. package/dist/ui/interaction-copy.d.ts +77 -0
  91. package/dist/ui/interaction-copy.js +40 -0
  92. package/dist/ui/interaction-hints.d.ts +18 -0
  93. package/dist/ui/interaction-hints.js +92 -0
  94. package/dist/ui/layout/budget.js +1 -1
  95. package/dist/ui/layout/queue-layout.d.ts +19 -0
  96. package/dist/ui/layout/queue-layout.js +26 -0
  97. package/dist/ui/layout/status-layout.d.ts +55 -0
  98. package/dist/ui/layout/status-layout.js +110 -0
  99. package/dist/ui/layout/todo-layout.d.ts +1 -0
  100. package/dist/ui/layout/todo-layout.js +1 -1
  101. package/dist/ui/overlays/HelpOverlay.js +8 -8
  102. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  103. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  104. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  105. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  106. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  107. package/dist/ui/overlays/QueueOverlay.js +79 -0
  108. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  109. package/dist/ui/overlays/SettingsScreen.js +347 -3
  110. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  111. package/dist/ui/overlays/model-profile-state.js +119 -0
  112. package/dist/ui/render-cache.d.ts +2 -2
  113. package/dist/ui/render-cache.js +4 -3
  114. package/dist/ui/run-status-row.d.ts +7 -19
  115. package/dist/ui/run-status-row.js +2 -18
  116. package/dist/ui/selection/selection-controller.d.ts +2 -2
  117. package/dist/ui/selection/selection-controller.js +1 -0
  118. package/dist/ui/use-wheel-routing.js +2 -1
  119. package/package.json +2 -2
@@ -0,0 +1,29 @@
1
+ import { type ClipboardOptions, type CopyResult } from './clipboard.js';
2
+ export interface CopyRequest {
3
+ text: string;
4
+ lines: number;
5
+ }
6
+ export type CopyRequestResult = CopyResult | {
7
+ status: 'busy';
8
+ };
9
+ export interface ClipboardState {
10
+ busy: boolean;
11
+ cleanupPending: boolean;
12
+ }
13
+ export interface ClipboardCoordinatorOptions {
14
+ clipboard?: ClipboardOptions;
15
+ /** Clears the previous Ctrl+C exit preparation for both copy entry points. */
16
+ onRequest?: () => void;
17
+ /** A repeated request is consumed; its selection must remain untouched. */
18
+ onBusy?: (state: ClipboardState) => void;
19
+ onResult?: (result: CopyResult, request: CopyRequest) => void;
20
+ onStateChange?: (state: ClipboardState) => void;
21
+ }
22
+ export interface ClipboardCoordinator {
23
+ requestCopy(request: CopyRequest): Promise<CopyRequestResult>;
24
+ isBusy(): boolean;
25
+ isCleanupPending(): boolean;
26
+ dispose(): void;
27
+ }
28
+ /** Create the App-owned copy port; use its live busy state before taking a selection. */
29
+ export declare function createClipboardCoordinator(options?: ClipboardCoordinatorOptions): ClipboardCoordinator;
@@ -0,0 +1,66 @@
1
+ import { startClipboardTask, } from './clipboard.js';
2
+ class SharedClipboardCoordinator {
3
+ options;
4
+ busy = false;
5
+ cleanupPending = false;
6
+ disposed = false;
7
+ abort = new AbortController();
8
+ constructor(options) {
9
+ this.options = options;
10
+ options.clipboard?.signal?.addEventListener('abort', this.dispose, { once: true });
11
+ if (options.clipboard?.signal?.aborted)
12
+ this.dispose();
13
+ }
14
+ isBusy() { return this.busy; }
15
+ isCleanupPending() { return this.cleanupPending; }
16
+ state() {
17
+ return { busy: this.busy, cleanupPending: this.cleanupPending };
18
+ }
19
+ notifyState() {
20
+ if (!this.disposed)
21
+ this.options.onStateChange?.(this.state());
22
+ }
23
+ requestCopy = (request) => {
24
+ if (this.disposed)
25
+ return Promise.resolve({ status: 'failed', reason: 'cancelled' });
26
+ if (this.busy) {
27
+ this.options.onRequest?.();
28
+ this.options.onBusy?.(this.state());
29
+ return Promise.resolve({ status: 'busy' });
30
+ }
31
+ // Claim synchronously, before callbacks or Promise work can re-enter this port.
32
+ this.busy = true;
33
+ this.options.onRequest?.();
34
+ this.notifyState();
35
+ const snapshot = { ...request };
36
+ const task = startClipboardTask(snapshot.text, {
37
+ ...this.options.clipboard, signal: this.abort.signal,
38
+ });
39
+ const result = task.result.then((value) => {
40
+ if (!this.disposed) {
41
+ this.cleanupPending = this.busy && value.status === 'failed';
42
+ this.notifyState();
43
+ this.options.onResult?.(value, snapshot);
44
+ }
45
+ return value;
46
+ });
47
+ // Failure feedback is not a release signal: the old native writer may still live.
48
+ void task.released.then(() => {
49
+ this.busy = false;
50
+ this.cleanupPending = false;
51
+ this.notifyState();
52
+ });
53
+ return result;
54
+ };
55
+ dispose = () => {
56
+ if (this.disposed)
57
+ return;
58
+ this.disposed = true;
59
+ this.options.clipboard?.signal?.removeEventListener('abort', this.dispose);
60
+ this.abort.abort();
61
+ };
62
+ }
63
+ /** Create the App-owned copy port; use its live busy state before taking a selection. */
64
+ export function createClipboardCoordinator(options = {}) {
65
+ return new SharedClipboardCoordinator(options);
66
+ }
@@ -1,52 +1,31 @@
1
- /**
2
- * Clipboard (tui-selection-and-scroll-follow §4.4.5) — the SINGLE clipboard path
3
- * for the package. `/copy` and the selection release both come through here.
4
- *
5
- * Two mechanisms, both attempted, and the return value names the better one so
6
- * the toast can say what was SENT rather than claiming a success neither one can
7
- * confirm:
8
- *
9
- * 1. **OSC 52.** It reaches the clipboard of the machine the USER is sitting
10
- * at, which is the only correct target over SSH, and it is the only mechanism
11
- * that works with no helper binary installed. `/copy` gained SSH support the
12
- * moment it moved onto this module.
13
- * 2. **The platform binary** (`clip` / `pbcopy` / `xclip`), moved here unchanged
14
- * from `commands/builtins.ts`, for the local case and for terminals that
15
- * ignore OSC 52 (tmux without `set -g set-clipboard on`, R-7).
16
- *
17
- * IT TAKES A WRITE *DOOR*, NOT A STREAM, and that is P1-6 rather than taste. In
18
- * full-screen mode stdout is a `Proxy` in front of the frame differ; an
19
- * unrecognised chunk there takes `passThrough(true)`, which increments
20
- * `fallbacks` and — on the first one — raises `onFirstFallback`, wired to
21
- * `console.warn(FRAME_FALLBACK_NOTICE)`. Sending OSC 52 through the proxy would
22
- * therefore print a diagnostic at a user who did nothing wrong the first time
23
- * they copied anything, and would permanently corrupt a counter documented to
24
- * mean "something wrote to stdout behind Ink's back". The caller supplies
25
- * `writeForeign` instead, and this module cannot pick the wrong door.
26
- */
27
- export type CopyVia = 'osc52' | 'native' | 'none';
28
- /**
29
- * Ceiling on an OSC 52 payload, in bytes of the ORIGINAL text.
30
- *
31
- * xterm's default limit is around 100 000 BASE64 characters and tmux / screen
32
- * are stricter, so 56 000 source bytes (~74 700 base64) stays inside all three
33
- * with room to spare. Past it the sequence is skipped rather than truncated: a
34
- * terminal that drops an over-long OSC 52 leaves the clipboard holding whatever
35
- * was there before, and half a selection is worse than none.
36
- */
1
+ export type CopyFailureReason = 'empty' | 'unavailable' | 'write' | 'timeout' | 'too-large' | 'cancelled';
2
+ export type CopyResult = {
3
+ status: 'confirmed';
4
+ via: 'native';
5
+ } | {
6
+ status: 'sent';
7
+ via: 'osc52';
8
+ fallbackReason?: CopyFailureReason;
9
+ } | {
10
+ status: 'failed';
11
+ reason: CopyFailureReason;
12
+ };
13
+ export interface ClipboardOptions {
14
+ /** Frame differ's foreign-write door, never the intercepted stdout stream. */
15
+ write?: (chunk: string) => void;
16
+ remote?: boolean;
17
+ timeoutMs?: number;
18
+ signal?: AbortSignal;
19
+ }
20
+ export interface ClipboardTask {
21
+ result: Promise<CopyResult>;
22
+ /** Resolves only after no writer can change the clipboard again. */
23
+ released: Promise<void>;
24
+ }
37
25
  export declare const MAX_OSC52_BYTES = 56000;
38
- /** `\x1b]52;c;<base64>\x07` — `c` is the CLIPBOARD selection, not PRIMARY. */
26
+ /** Encode the full clipboard selection as UTF-8, without truncating its payload. */
39
27
  export declare function osc52(text: string): string;
40
- /**
41
- * Put `text` on the clipboard and report the best mechanism that was reached.
42
- *
43
- * `write` is the foreign-write door (see the module header). Omit it and OSC 52
44
- * is skipped entirely, which is the correct behaviour for a caller that has no
45
- * terminal to write to.
46
- *
47
- * NEITHER MECHANISM IS DETECTABLE. `spawn` returning without throwing does not
48
- * mean `xclip` exists, and an OSC 52 sequence a terminal ignores is
49
- * indistinguishable from one it honours. The return value therefore names what
50
- * was ATTEMPTED, and every caller's user-facing text is worded accordingly.
51
- */
52
- export declare function copyText(text: string, write?: (chunk: string) => void): CopyVia;
28
+ /** Start a copy without rejecting; result and writer release are separate signals. */
29
+ export declare function startClipboardTask(text: string, options?: ClipboardOptions): ClipboardTask;
30
+ /** Promise convenience wrapper; interactive callers must use the shared coordinator. */
31
+ export declare function copyText(text: string, options?: ClipboardOptions): Promise<CopyResult>;
@@ -1,91 +1,157 @@
1
- /**
2
- * Clipboard (tui-selection-and-scroll-follow §4.4.5) — the SINGLE clipboard path
3
- * for the package. `/copy` and the selection release both come through here.
4
- *
5
- * Two mechanisms, both attempted, and the return value names the better one so
6
- * the toast can say what was SENT rather than claiming a success neither one can
7
- * confirm:
8
- *
9
- * 1. **OSC 52.** It reaches the clipboard of the machine the USER is sitting
10
- * at, which is the only correct target over SSH, and it is the only mechanism
11
- * that works with no helper binary installed. `/copy` gained SSH support the
12
- * moment it moved onto this module.
13
- * 2. **The platform binary** (`clip` / `pbcopy` / `xclip`), moved here unchanged
14
- * from `commands/builtins.ts`, for the local case and for terminals that
15
- * ignore OSC 52 (tmux without `set -g set-clipboard on`, R-7).
16
- *
17
- * IT TAKES A WRITE *DOOR*, NOT A STREAM, and that is P1-6 rather than taste. In
18
- * full-screen mode stdout is a `Proxy` in front of the frame differ; an
19
- * unrecognised chunk there takes `passThrough(true)`, which increments
20
- * `fallbacks` and — on the first one — raises `onFirstFallback`, wired to
21
- * `console.warn(FRAME_FALLBACK_NOTICE)`. Sending OSC 52 through the proxy would
22
- * therefore print a diagnostic at a user who did nothing wrong the first time
23
- * they copied anything, and would permanently corrupt a counter documented to
24
- * mean "something wrote to stdout behind Ink's back". The caller supplies
25
- * `writeForeign` instead, and this module cannot pick the wrong door.
26
- */
1
+ /** The only clipboard writer. Interaction callers share clipboard-task's coordinator. */
27
2
  import { spawn } from 'node:child_process';
28
3
  import process from 'node:process';
29
- /**
30
- * Ceiling on an OSC 52 payload, in bytes of the ORIGINAL text.
31
- *
32
- * xterm's default limit is around 100 000 BASE64 characters and tmux / screen
33
- * are stricter, so 56 000 source bytes (~74 700 base64) stays inside all three
34
- * with room to spare. Past it the sequence is skipped rather than truncated: a
35
- * terminal that drops an over-long OSC 52 leaves the clipboard holding whatever
36
- * was there before, and half a selection is worse than none.
37
- */
38
4
  export const MAX_OSC52_BYTES = 56_000;
39
- /** `\x1b]52;c;<base64>\x07` — `c` is the CLIPBOARD selection, not PRIMARY. */
5
+ const CLEANUP_GRACE_MS = 500;
6
+ // clip.exe preserves a supplied BOM and guesses BOM-less UTF-16 incorrectly for
7
+ // short CJK text. Decode stdin explicitly; never interpolate clipboard content.
8
+ const WINDOWS_CLIPBOARD_COMMAND = "$ErrorActionPreference='Stop'; "
9
+ + '$reader=[IO.StreamReader]::new([Console]::OpenStandardInput(),'
10
+ + '[Text.UTF8Encoding]::new($false),$false); Set-Clipboard -Value $reader.ReadToEnd()';
11
+ /** Encode the full clipboard selection as UTF-8, without truncating its payload. */
40
12
  export function osc52(text) {
41
- return `\x1b]52;c;${Buffer.from(text, 'utf-8').toString('base64')}\x07`;
13
+ return `\x1b]52;c;${Buffer.from(text, 'utf8').toString('base64')}\x07`;
42
14
  }
43
- /** Best-effort clipboard copy via the platform's clipboard CLI. */
44
- function copyNative(text) {
45
- const cmd = process.platform === 'win32'
46
- ? { file: 'clip', args: [] }
47
- : process.platform === 'darwin'
48
- ? { file: 'pbcopy', args: [] }
49
- : { file: 'xclip', args: ['-selection', 'clipboard'] };
15
+ function sendOsc52(text, options, fallbackReason) {
16
+ if (options.signal?.aborted)
17
+ return { status: 'failed', reason: 'cancelled' };
18
+ if (Buffer.byteLength(text, 'utf8') > MAX_OSC52_BYTES) {
19
+ return { status: 'failed', reason: 'too-large' };
20
+ }
21
+ if (!options.write)
22
+ return { status: 'failed', reason: fallbackReason ?? 'unavailable' };
50
23
  try {
51
- const child = spawn(cmd.file, cmd.args, { stdio: ['pipe', 'ignore', 'ignore'] });
52
- child.on('error', () => {
53
- /* swallow — clipboard is best-effort, and a missing xclip is ordinary */
54
- });
55
- child.stdin?.end(text);
56
- return true;
24
+ options.write(osc52(text));
25
+ return { status: 'sent', via: 'osc52', ...(fallbackReason ? { fallbackReason } : {}) };
57
26
  }
58
27
  catch {
59
- return false;
28
+ return { status: 'failed', reason: 'write' };
60
29
  }
61
30
  }
62
- /**
63
- * Put `text` on the clipboard and report the best mechanism that was reached.
64
- *
65
- * `write` is the foreign-write door (see the module header). Omit it and OSC 52
66
- * is skipped entirely, which is the correct behaviour for a caller that has no
67
- * terminal to write to.
68
- *
69
- * NEITHER MECHANISM IS DETECTABLE. `spawn` returning without throwing does not
70
- * mean `xclip` exists, and an OSC 52 sequence a terminal ignores is
71
- * indistinguishable from one it honours. The return value therefore names what
72
- * was ATTEMPTED, and every caller's user-facing text is worded accordingly.
73
- */
74
- export function copyText(text, write) {
75
- if (text.length === 0)
76
- return 'none';
77
- let sentOsc52 = false;
78
- if (write && Buffer.byteLength(text, 'utf-8') <= MAX_OSC52_BYTES) {
31
+ function settledTask(result) {
32
+ return { result: Promise.resolve(result), released: Promise.resolve() };
33
+ }
34
+ function spawnNative() {
35
+ const windows = process.platform === 'win32';
36
+ const file = windows ? 'powershell.exe' : process.platform === 'darwin' ? 'pbcopy' : 'xclip';
37
+ const args = windows
38
+ ? ['-NoProfile', '-NonInteractive', '-STA', '-Command', WINDOWS_CLIPBOARD_COMMAND]
39
+ : file === 'xclip' ? ['-selection', 'clipboard'] : [];
40
+ return spawn(file, args, {
41
+ stdio: ['pipe', 'ignore', 'ignore'], shell: false, windowsHide: true,
42
+ });
43
+ }
44
+ /** Owns a writer through close, even when its user-facing result timed out earlier. */
45
+ class NativeClipboardTask {
46
+ child;
47
+ text;
48
+ options;
49
+ result;
50
+ released;
51
+ resolveResult;
52
+ resolveReleased;
53
+ settled = false;
54
+ closed = false;
55
+ stopping = false;
56
+ failure;
57
+ timer;
58
+ cleanupTimer;
59
+ constructor(child, text, options) {
60
+ this.child = child;
61
+ this.text = text;
62
+ this.options = options;
63
+ this.result = new Promise((resolve) => { this.resolveResult = resolve; });
64
+ this.released = new Promise((resolve) => { this.resolveReleased = resolve; });
65
+ child.on('error', this.onError);
66
+ child.once('close', this.onClose);
67
+ child.stdin?.on('error', this.onStdinError);
68
+ options.signal?.addEventListener('abort', this.onAbort, { once: true });
69
+ this.timer = setTimeout(() => this.stop('timeout'), options.timeoutMs ?? 1500);
70
+ if (options.signal?.aborted) {
71
+ this.onAbort();
72
+ return;
73
+ }
74
+ if (!child.stdin) {
75
+ this.stop('write');
76
+ return;
77
+ }
79
78
  try {
80
- write(osc52(text));
81
- sentOsc52 = true;
79
+ child.stdin.end(Buffer.from(text, 'utf8'));
82
80
  }
83
81
  catch {
84
- // A closed or broken stdout must never turn a copy into a crash.
82
+ this.stop('write');
85
83
  }
86
84
  }
87
- const sentNative = copyNative(text);
88
- if (sentOsc52)
89
- return 'osc52';
90
- return sentNative ? 'native' : 'none';
85
+ settle(result) {
86
+ if (this.settled)
87
+ return;
88
+ this.settled = true;
89
+ clearTimeout(this.timer);
90
+ clearTimeout(this.cleanupTimer);
91
+ this.resolveResult(result);
92
+ }
93
+ stop(reason) {
94
+ if (this.closed || this.stopping)
95
+ return;
96
+ this.stopping = true;
97
+ this.failure = reason;
98
+ clearTimeout(this.timer);
99
+ // No fallback before close: even a successful kill request is not proof of exit.
100
+ if (!this.settled) {
101
+ this.cleanupTimer = setTimeout(() => {
102
+ this.settle({ status: 'failed', reason });
103
+ }, CLEANUP_GRACE_MS);
104
+ }
105
+ try {
106
+ this.child.stdin?.destroy();
107
+ }
108
+ catch { /* Keep error listeners until close. */ }
109
+ try {
110
+ this.child.kill();
111
+ }
112
+ catch { /* A failed kill must not release the writer lock. */ }
113
+ }
114
+ onAbort = () => {
115
+ this.settle({ status: 'failed', reason: 'cancelled' });
116
+ this.stop('cancelled');
117
+ };
118
+ onError = () => { this.stop('unavailable'); };
119
+ onStdinError = () => { this.stop('write'); };
120
+ onClose = (code) => {
121
+ this.closed = true;
122
+ clearTimeout(this.timer);
123
+ clearTimeout(this.cleanupTimer);
124
+ this.options.signal?.removeEventListener('abort', this.onAbort);
125
+ if (!this.settled) {
126
+ const result = code === 0 && !this.failure
127
+ ? { status: 'confirmed', via: 'native' }
128
+ : sendOsc52(this.text, this.options, this.failure ?? 'write');
129
+ this.settle(result);
130
+ }
131
+ this.child.removeListener('error', this.onError);
132
+ this.child.stdin?.removeListener('error', this.onStdinError);
133
+ this.resolveReleased();
134
+ };
135
+ }
136
+ /** Start a copy without rejecting; result and writer release are separate signals. */
137
+ export function startClipboardTask(text, options = {}) {
138
+ if (options.signal?.aborted)
139
+ return settledTask({ status: 'failed', reason: 'cancelled' });
140
+ if (text.length === 0)
141
+ return settledTask({ status: 'failed', reason: 'empty' });
142
+ const remote = options.remote ?? Boolean(process.env.SSH_CONNECTION || process.env.SSH_TTY);
143
+ if (remote)
144
+ return settledTask(sendOsc52(text, options));
145
+ let child;
146
+ try {
147
+ child = spawnNative();
148
+ }
149
+ catch {
150
+ return settledTask(sendOsc52(text, options, 'unavailable'));
151
+ }
152
+ return new NativeClipboardTask(child, text, options);
153
+ }
154
+ /** Promise convenience wrapper; interactive callers must use the shared coordinator. */
155
+ export function copyText(text, options = {}) {
156
+ return startClipboardTask(text, options).result;
91
157
  }
@@ -0,0 +1,5 @@
1
+ import type { TermCapabilities } from './capabilities.js';
2
+ /** Highlight cache entries must not cross color preferences between sessions. */
3
+ export declare function syntaxColorLevel(): number;
4
+ /** Apply session colors without changing environment inherited by agent tools. */
5
+ export declare function useTuiColors(level: TermCapabilities['colorLevel']): () => void;
@@ -0,0 +1,29 @@
1
+ import { createRequire } from 'node:module';
2
+ import { pathToFileURL } from 'node:url';
3
+ import chalk from 'chalk';
4
+ const require = createRequire(import.meta.url);
5
+ /** Resolve the actual dependency instance even when npm nests its Chalk. */
6
+ async function dependencyChalk(packageName) {
7
+ const dependency = createRequire(require.resolve(packageName));
8
+ return (await import(pathToFileURL(dependency.resolve('chalk')).href)).default;
9
+ }
10
+ const [inkChalk, highlightChalk] = await Promise.all([
11
+ dependencyChalk('ink'), dependencyChalk('cli-highlight'),
12
+ ]);
13
+ /** Highlight cache entries must not cross color preferences between sessions. */
14
+ export function syntaxColorLevel() { return highlightChalk.level; }
15
+ /** Apply session colors without changing environment inherited by agent tools. */
16
+ export function useTuiColors(level) {
17
+ const renderers = new Set([chalk, inkChalk, highlightChalk]);
18
+ const previous = new Map([...renderers].map(renderer => [renderer, renderer.level]));
19
+ for (const renderer of renderers)
20
+ renderer.level = level;
21
+ let restored = false;
22
+ return () => {
23
+ if (restored)
24
+ return;
25
+ restored = true;
26
+ for (const [renderer, original] of previous)
27
+ renderer.level = original;
28
+ };
29
+ }
@@ -22,10 +22,9 @@ export declare const USER_ENTRY_MAX_ROWS = 40;
22
22
  /**
23
23
  * Ceiling on the composer's DRAFT rows at a given terminal height.
24
24
  *
25
- * Non-decreasing in `terminalRows`. Returning `Infinity` reduces the whole
26
- * height-bound layer to the rung below it (section 12, S4), which is what makes
27
- * that stage revertible on its own.
25
+ * Non-decreasing in `terminalRows`, with one quarter of the terminal available
26
+ * to the draft and an absolute six-row ceiling shared by rendering and budgets.
28
27
  */
29
28
  export declare function draftMaxRows(terminalRows: number): number;
30
- /** The terminal height at which `draftMaxRows` steps from 3 to 6. Pinned by test. */
29
+ /** Compatibility export for the shared hint visibility threshold. */
31
30
  export declare const DRAFT_MAX_ROWS_HINT_TIER = 20;
@@ -33,18 +33,13 @@ const DRAFT_TIER_MIN_ROWS = 20;
33
33
  /**
34
34
  * Ceiling on the composer's DRAFT rows at a given terminal height.
35
35
  *
36
- * Non-decreasing in `terminalRows`. Returning `Infinity` reduces the whole
37
- * height-bound layer to the rung below it (section 12, S4), which is what makes
38
- * that stage revertible on its own.
36
+ * Non-decreasing in `terminalRows`, with one quarter of the terminal available
37
+ * to the draft and an absolute six-row ceiling shared by rendering and budgets.
39
38
  */
40
39
  export function draftMaxRows(terminalRows) {
41
40
  if (!Number.isFinite(terminalRows))
42
41
  return 3;
43
- if (terminalRows >= 30)
44
- return 10;
45
- if (terminalRows >= DRAFT_TIER_MIN_ROWS)
46
- return 6;
47
- return 3;
42
+ return Math.max(1, Math.min(6, Math.floor(terminalRows / 4)));
48
43
  }
49
- /** The terminal height at which `draftMaxRows` steps from 3 to 6. Pinned by test. */
44
+ /** Compatibility export for the shared hint visibility threshold. */
50
45
  export const DRAFT_MAX_ROWS_HINT_TIER = DRAFT_TIER_MIN_ROWS;
@@ -44,7 +44,7 @@ export interface ComposerLayoutInput {
44
44
  readonly maxRows: number;
45
45
  readonly active: boolean;
46
46
  }
47
- /** Total display width of a run, using the same table the layout used. */
47
+ /** Width of a displayed run, measured by complete graphemes. */
48
48
  export declare function displayWidth(text: string): number;
49
49
  /**
50
50
  * Wrap `buffer` into rows, locate the caret, and select a window of at most