@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
@@ -157,6 +157,55 @@ export interface ExecFastReviewEvent {
157
157
  model: string;
158
158
  text: string;
159
159
  }
160
+ /**
161
+ * Context compaction (context-auto-compaction §4.6).
162
+ *
163
+ * ONE ADDITIVE EVENT TYPE, AND `EXEC_SCHEMA_VERSION` DOES NOT MOVE — the rule
164
+ * this file's header states in the same words: adding a field or an event type
165
+ * never bumps it; renaming or removing one does. A consumer that ignores event
166
+ * types it does not know is unaffected.
167
+ *
168
+ * THE SUMMARY TEXT IS DELIBERATELY ABSENT (D-18). It can be tens of thousands of
169
+ * characters, no wrapper needs it, and the transcript card and the log both have
170
+ * it. What a wrapper does need is the SHAPE of what happened, which is here.
171
+ */
172
+ export interface ExecCompactionEvent {
173
+ type: 'compaction';
174
+ sessionId: string;
175
+ turn: number;
176
+ subtype: 'start' | 'end';
177
+ trigger: 'pressure' | 'overflow' | 'manual';
178
+ /** `end` only. */
179
+ applied?: boolean;
180
+ /**
181
+ * `relieved` means the RETAINED turns were clipped and nothing was dropped
182
+ * (context-auto-compaction-hardening §3.3 / W2).
183
+ *
184
+ * A NEW MEMBER OF AN EXISTING UNION, WHICH DOES NOT MOVE `EXEC_SCHEMA_VERSION`
185
+ * either - the rule this file's header states covers adding, not renaming or
186
+ * removing. A consumer switching on this field already has to handle a value it
187
+ * does not know, because `none` and `truncated` were both already possible.
188
+ */
189
+ mode?: 'summarized' | 'truncated' | 'relieved' | 'none';
190
+ reason?: string;
191
+ messagesBefore?: number;
192
+ messagesAfter?: number;
193
+ tokensBefore?: number;
194
+ tokensAfter?: number;
195
+ /**
196
+ * Present only when tail relief fired (§4.5).
197
+ *
198
+ * A BOUNDED, ANNOUNCED DATA LOSS INSIDE THE TURNS THE RUN CONTINUES FROM, and
199
+ * therefore something a wrapper is entitled to see. The clipped bodies
200
+ * themselves are not on the stream, for the same reason `summary` is not
201
+ * (D-18).
202
+ */
203
+ tailRelief?: {
204
+ messages: number;
205
+ charsRemoved: number;
206
+ };
207
+ durationMs?: number;
208
+ }
160
209
  export interface ExecRetryEvent {
161
210
  type: 'retry';
162
211
  sessionId: string;
@@ -202,7 +251,7 @@ export interface ExecResultEvent {
202
251
  todos: ExecTodoCounts | null;
203
252
  error: ExecErrorInfo | null;
204
253
  }
205
- export type ExecEvent = ExecSystemInitEvent | ExecUserEvent | ExecTextDeltaEvent | ExecThinkingEvent | ExecAssistantEvent | ExecToolCallEvent | ExecToolResultEvent | ExecTodoEvent | ExecTeamEvent | ExecFastReviewEvent | ExecRetryEvent | ExecErrorEvent | ExecResultEvent;
254
+ export type ExecEvent = ExecSystemInitEvent | ExecUserEvent | ExecTextDeltaEvent | ExecThinkingEvent | ExecAssistantEvent | ExecToolCallEvent | ExecToolResultEvent | ExecTodoEvent | ExecTeamEvent | ExecFastReviewEvent | ExecCompactionEvent | ExecRetryEvent | ExecErrorEvent | ExecResultEvent;
206
255
  /** Everything `system/init` needs that the runner cannot derive for itself. */
207
256
  export interface ExecInitParams {
208
257
  sessionId: string;
@@ -121,6 +121,48 @@ export class ExecRunner {
121
121
  });
122
122
  if (fast)
123
123
  this.unsubscribers.push(fast);
124
+ // Context compaction (context-auto-compaction §4.6). Usage is folded into the
125
+ // run's totals for the reason the team and fast branches above give: a
126
+ // summarization is real spend, and `result.usage` is the only cost readout a
127
+ // wrapper gets.
128
+ const compaction = controller.subscribeCompaction?.((event) => {
129
+ if (event.type === 'usage') {
130
+ this.usage.inputTokens += event.usage.inputTokens;
131
+ this.usage.outputTokens += event.usage.outputTokens;
132
+ return;
133
+ }
134
+ if (event.type === 'compaction_start') {
135
+ this.emit({
136
+ type: 'compaction',
137
+ sessionId: this.sessionId,
138
+ turn: this.turns,
139
+ subtype: 'start',
140
+ trigger: event.trigger,
141
+ });
142
+ return;
143
+ }
144
+ if (event.type !== 'compaction_end')
145
+ return;
146
+ const r = event.record;
147
+ this.emit({
148
+ type: 'compaction',
149
+ sessionId: this.sessionId,
150
+ turn: this.turns,
151
+ subtype: 'end',
152
+ trigger: r.trigger,
153
+ applied: r.applied,
154
+ mode: r.mode,
155
+ ...(r.reason !== undefined ? { reason: r.reason } : {}),
156
+ messagesBefore: r.messagesBefore,
157
+ messagesAfter: r.messagesAfter,
158
+ tokensBefore: r.tokensBefore,
159
+ tokensAfter: r.tokensAfter,
160
+ ...(r.tailRelief ? { tailRelief: r.tailRelief } : {}),
161
+ durationMs: r.durationMs,
162
+ });
163
+ });
164
+ if (compaction)
165
+ this.unsubscribers.push(compaction);
124
166
  this.installTerminator();
125
167
  }
126
168
  /**
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Structural bounds for paste handling (tui-paste-handling D-10 / section 8).
3
+ *
4
+ * THESE ARE STRUCTURAL, NOT POLICY. The package convention recorded in
5
+ * `.claude-index/index.md` is that `limits.ts` carries the numbers an
6
+ * implementation needs to stay bounded, while `config/schema.ts` carries the
7
+ * preferences a user is allowed to have. The only user-facing key this feature
8
+ * adds is the boolean `paste`; a tunable collapse threshold is a preference
9
+ * nobody has asked for, and a kill switch is what a bug report actually needs.
10
+ *
11
+ * The two framing constants below are the only bytes that cross from the stdin
12
+ * filter into the composer, so they are load-bearing in a way a threshold is
13
+ * not: see `PASTE_OPEN`.
14
+ */
15
+ /**
16
+ * Ceiling on ONE paste. Above it the body is dropped and the user is told,
17
+ * never truncated (D-11): a truncated paste loses data and looks like success.
18
+ */
19
+ export declare const PASTE_MAX_BYTES: number;
20
+ /** Ceiling on every live payload in one draft, summed. Above it a paste is refused. */
21
+ export declare const PASTE_DRAFT_MAX_BYTES: number;
22
+ /** Ceiling on collapsed blocks in one draft. Above it a paste is refused. */
23
+ export declare const PASTE_MAX_BLOCKS = 32;
24
+ /**
25
+ * Tier 2's single-line rule: a chunk of at least this many printable characters
26
+ * with no line break is a paste.
27
+ *
28
+ * Safe because coalescing a keyboard burst into 25 characters inside one
29
+ * event-loop turn needs roughly 1 500 WPM. It is a statement about what a
30
+ * KEYBOARD can emit, not about what text looks like.
31
+ */
32
+ export declare const PASTE_MIN_BURST_CHARS = 25;
33
+ /** Tier 2 coalescing window. Ordinary typing never opens it, so never pays it. */
34
+ export declare const PASTE_BURST_MS = 15;
35
+ /**
36
+ * How long an unterminated Tier 1 body is held before it is force-flushed.
37
+ *
38
+ * I-4: an unterminated `\x1b[200~` must never be able to wedge keyboard input,
39
+ * which is the rule `MAX_PENDING_MOUSE_CHARS` already encodes for mouse reports.
40
+ */
41
+ export declare const PASTE_ASSEMBLY_MAX_MS = 2000;
42
+ /**
43
+ * The inline frame the filter writes into the stream Ink reads (D-3 / I-5).
44
+ *
45
+ * ON THE SAME STREAM, NOT A SIDE CHANNEL. `wrapper.write(text)` is delivered
46
+ * asynchronously; a synchronous side-channel emit would apply the paste BEFORE
47
+ * text that preceded it in the same chunk. Framing preserves order by
48
+ * construction.
49
+ *
50
+ * NUL is the delimiter because no terminal delivers it for any key, and because
51
+ * `sanitisePaste` strips every NUL out of every payload -- so a payload can
52
+ * never forge a frame boundary.
53
+ */
54
+ export declare const PASTE_OPEN = "\0[";
55
+ export declare const PASTE_CLOSE = "\0]";
56
+ /**
57
+ * The one channel from the stdin filter to `App`'s notice dispatch (P1-3 / I-15).
58
+ *
59
+ * The filter is constructed in `cli.tsx` BEFORE `render()`, so it has no
60
+ * `dispatch`. Its only other outlet is the logger, and a limit that writes only
61
+ * to a log file is indistinguishable -- from the user's chair -- from a paste
62
+ * that silently did nothing, which is exactly what D-11 exists to rule out.
63
+ *
64
+ * Same late-bound shape as `selectionBridge` / `updateBridge` / `terminalBridge`:
65
+ * `cli.tsx` creates the object, `App` assigns `notify` on mount and clears it on
66
+ * unmount, and the filter calls `bridge.notify?.(...)`.
67
+ */
68
+ export interface PasteBridge {
69
+ notify: ((level: 'warn' | 'error', text: string) => void) | null;
70
+ }
71
+ /** Human-readable byte size for the refusal toast (`4.1 MB`, `812.0 KB`, `7 B`). */
72
+ export declare function formatPasteSize(bytes: number): string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Structural bounds for paste handling (tui-paste-handling D-10 / section 8).
3
+ *
4
+ * THESE ARE STRUCTURAL, NOT POLICY. The package convention recorded in
5
+ * `.claude-index/index.md` is that `limits.ts` carries the numbers an
6
+ * implementation needs to stay bounded, while `config/schema.ts` carries the
7
+ * preferences a user is allowed to have. The only user-facing key this feature
8
+ * adds is the boolean `paste`; a tunable collapse threshold is a preference
9
+ * nobody has asked for, and a kill switch is what a bug report actually needs.
10
+ *
11
+ * The two framing constants below are the only bytes that cross from the stdin
12
+ * filter into the composer, so they are load-bearing in a way a threshold is
13
+ * not: see `PASTE_OPEN`.
14
+ */
15
+ /**
16
+ * Ceiling on ONE paste. Above it the body is dropped and the user is told,
17
+ * never truncated (D-11): a truncated paste loses data and looks like success.
18
+ */
19
+ export const PASTE_MAX_BYTES = 2 * 1024 * 1024;
20
+ /** Ceiling on every live payload in one draft, summed. Above it a paste is refused. */
21
+ export const PASTE_DRAFT_MAX_BYTES = 8 * 1024 * 1024;
22
+ /** Ceiling on collapsed blocks in one draft. Above it a paste is refused. */
23
+ export const PASTE_MAX_BLOCKS = 32;
24
+ /**
25
+ * Tier 2's single-line rule: a chunk of at least this many printable characters
26
+ * with no line break is a paste.
27
+ *
28
+ * Safe because coalescing a keyboard burst into 25 characters inside one
29
+ * event-loop turn needs roughly 1 500 WPM. It is a statement about what a
30
+ * KEYBOARD can emit, not about what text looks like.
31
+ */
32
+ export const PASTE_MIN_BURST_CHARS = 25;
33
+ /** Tier 2 coalescing window. Ordinary typing never opens it, so never pays it. */
34
+ export const PASTE_BURST_MS = 15;
35
+ /**
36
+ * How long an unterminated Tier 1 body is held before it is force-flushed.
37
+ *
38
+ * I-4: an unterminated `\x1b[200~` must never be able to wedge keyboard input,
39
+ * which is the rule `MAX_PENDING_MOUSE_CHARS` already encodes for mouse reports.
40
+ */
41
+ export const PASTE_ASSEMBLY_MAX_MS = 2_000;
42
+ /**
43
+ * The inline frame the filter writes into the stream Ink reads (D-3 / I-5).
44
+ *
45
+ * ON THE SAME STREAM, NOT A SIDE CHANNEL. `wrapper.write(text)` is delivered
46
+ * asynchronously; a synchronous side-channel emit would apply the paste BEFORE
47
+ * text that preceded it in the same chunk. Framing preserves order by
48
+ * construction.
49
+ *
50
+ * NUL is the delimiter because no terminal delivers it for any key, and because
51
+ * `sanitisePaste` strips every NUL out of every payload -- so a payload can
52
+ * never forge a frame boundary.
53
+ */
54
+ export const PASTE_OPEN = '\u0000[';
55
+ export const PASTE_CLOSE = '\u0000]';
56
+ /** Human-readable byte size for the refusal toast (`4.1 MB`, `812.0 KB`, `7 B`). */
57
+ export function formatPasteSize(bytes) {
58
+ if (bytes >= 1024 * 1024)
59
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
60
+ if (bytes >= 1024)
61
+ return `${(bytes / 1024).toFixed(1)} KB`;
62
+ return `${bytes} B`;
63
+ }
@@ -15,8 +15,10 @@
15
15
  * module exists so `stdin-mouse-filter.ts` can remove every byte of every
16
16
  * report before Ink ever sees the chunk (invariant I-1).
17
17
  *
18
- * Non-wheel reports (press / release / drag) are recognized and DISCARDED
19
- * rather than passed through, for the same reason.
18
+ * Every mouse report is CONSUMED here whatever it turns out to be; what changed
19
+ * with tui-selection-and-scroll-follow is that press / drag / release are now
20
+ * EMITTED as well as consumed, so `SelectionController` can build a selection
21
+ * out of them (§4.4.2). They were previously recognized and thrown away.
20
22
  */
21
23
  export interface WheelEvent {
22
24
  readonly kind: 'wheel';
@@ -29,14 +31,33 @@ export interface WheelEvent {
29
31
  readonly alt: boolean;
30
32
  readonly ctrl: boolean;
31
33
  }
34
+ export interface ButtonEvent {
35
+ readonly kind: 'press' | 'drag' | 'release';
36
+ /** 0 = left, 1 = middle, 2 = right. Never 3 — see `decodeReport`. */
37
+ readonly button: 0 | 1 | 2;
38
+ /** 1-based terminal column, as reported. */
39
+ readonly x: number;
40
+ /** 1-based terminal row, as reported. */
41
+ readonly y: number;
42
+ readonly shift: boolean;
43
+ readonly alt: boolean;
44
+ readonly ctrl: boolean;
45
+ }
46
+ export type MouseEvent = WheelEvent | ButtonEvent;
32
47
  export interface MouseSplit {
33
- /** Wheel events, in arrival order. Non-wheel reports are dropped, not returned. */
34
- readonly events: WheelEvent[];
48
+ /** Mouse events, in arrival order. Unrecognized reports are dropped, not returned. */
49
+ readonly events: MouseEvent[];
35
50
  /** Everything that was not a mouse report — forward this to Ink verbatim. */
36
51
  readonly text: string;
37
52
  /** Trailing strict prefix of a possible report; feed it back in with the next chunk. */
38
53
  readonly pending: string;
39
54
  }
55
+ /**
56
+ * Which wire encoding a report arrived in. It is a PARAMETER rather than a
57
+ * remembered rule because the rule it expresses is invariant I-11 and the cost
58
+ * of forgetting it is a permanently frozen viewport (see `decodeReport`).
59
+ */
60
+ export type MouseEncoding = 'sgr' | 'x10';
40
61
  /**
41
62
  * Ceiling on the held-back tail. A malformed sequence that looks like a prefix
42
63
  * forever must never be able to wedge input: past this length the tail is
@@ -51,11 +72,16 @@ export declare const MAX_PENDING_MOUSE_CHARS = 32;
51
72
  */
52
73
  export declare function isMousePrefix(s: string): boolean;
53
74
  /**
54
- * Split a decoded stdin chunk into wheel events, passthrough text, and a
75
+ * Split a decoded stdin chunk into mouse events, passthrough text, and a
55
76
  * trailing prefix to carry into the next chunk.
56
77
  *
57
78
  * The caller is expected to feed `pending + nextChunk` back in; ConPTY does
58
79
  * split writes mid-sequence, and a report torn across two chunks would
59
80
  * otherwise leak its tail into the composer (R-6).
81
+ *
82
+ * ONE CHANNEL, NOT TWO (§4.4.2). Widening this list to carry buttons alongside
83
+ * wheel notches is what keeps ARRIVAL ORDER intact between them; a second
84
+ * channel would let a press overtake the notch that preceded it, and the
85
+ * selection would then be anchored against rows that had already scrolled.
60
86
  */
61
87
  export declare function splitMouseEvents(buffer: string): MouseSplit;
@@ -15,8 +15,10 @@
15
15
  * module exists so `stdin-mouse-filter.ts` can remove every byte of every
16
16
  * report before Ink ever sees the chunk (invariant I-1).
17
17
  *
18
- * Non-wheel reports (press / release / drag) are recognized and DISCARDED
19
- * rather than passed through, for the same reason.
18
+ * Every mouse report is CONSUMED here whatever it turns out to be; what changed
19
+ * with tui-selection-and-scroll-follow is that press / drag / release are now
20
+ * EMITTED as well as consumed, so `SelectionController` can build a selection
21
+ * out of them (§4.4.2). They were previously recognized and thrown away.
20
22
  */
21
23
  /**
22
24
  * Ceiling on the held-back tail. A malformed sequence that looks like a prefix
@@ -38,34 +40,80 @@ const SGR_PREFIX = /^\x1b\[<[\d;]*$/;
38
40
  export function isMousePrefix(s) {
39
41
  return /^\x1B(\[(<[\d;]*|M[\s\S]{0,2})?)?$/.test(s);
40
42
  }
43
+ function modifiers(b) {
44
+ return { shift: (b & 4) !== 0, alt: (b & 8) !== 0, ctrl: (b & 16) !== 0 };
45
+ }
41
46
  /**
42
- * Decode an SGR button byte into a wheel event, or `null` when the report is
43
- * not a wheel notch we act on.
47
+ * Decode one report, or `null` when it is a report we consume without acting on.
44
48
  *
45
49
  * THE ORDER OF THESE TESTS IS THE SPECIFICATION, not a presentation choice.
46
50
  * Button 66 (wheel-left) has `b & 1 === 0` and would decode as `'up'`, so the
47
- * horizontal test has to precede the direction decode.
51
+ * horizontal test has to precede the direction decode; and `b & 32` (motion) has
52
+ * to precede the `final` test, because a drag is reported with `M` exactly as a
53
+ * press is.
54
+ *
55
+ * ═══════════════════════════════════════════════════════════════════════════
56
+ * I-11 — BUTTON EVENTS COME FROM THE SGR PATH ONLY. X10 stays wheel-only, which
57
+ * is exactly the contract it has today.
58
+ *
59
+ * `matchX10` reconstructs a legacy report and hard-codes `final = 'M'`, because
60
+ * X10 HAS NO SEPARATE RELEASE FORM AT ALL — it encodes a release as button 3 of
61
+ * a press. Fed through the table below, an X10 release would become a `press`
62
+ * that is never followed by a `release`, whose `button` is 3 while the type says
63
+ * `0 | 1 | 2`, and whose consequence is a `hold` that is set and never cleared:
64
+ * content anchoring never lets go, the resume timer's third condition never
65
+ * passes, and the transcript is frozen for the rest of the session with nothing
66
+ * raised anywhere.
67
+ *
68
+ * This costs nothing real (D-16): we never REQUEST X10 — `?1006h` is over a
69
+ * decade old and every terminal that implements `?1000` implements it — and a
70
+ * terminal that ignored the SGR request also has no usable drag coordinates past
71
+ * column 223 (R-2). X10 exists in this parser only so a stray report cannot
72
+ * reach the user's draft.
73
+ * ═══════════════════════════════════════════════════════════════════════════
48
74
  */
49
- function decodeWheel(b, x, y, final) {
50
- // 1. Not a wheel report at all (press / release / drag) — consume, emit nothing.
51
- if ((b & 64) === 0)
52
- return null;
53
- // 2. Horizontal wheel (66 = left, 67 = right). Consumed and dropped (non-goal).
54
- if ((b & 3) >= 2)
55
- return null;
56
- // 3. Wheel buttons are press-only; a terminal that ever sent a release for one
57
- // must not produce a second step for the same physical notch.
58
- if (final !== 'M')
75
+ function decodeReport(b, x, y, final, encoding) {
76
+ // 1. Wheel. Buttons 64-67 in both encodings.
77
+ if ((b & 64) !== 0) {
78
+ // Horizontal wheel (66 = left, 67 = right). Consumed and dropped (non-goal).
79
+ if ((b & 3) >= 2)
80
+ return null;
81
+ // Wheel buttons are press-only; a terminal that ever sent a release for one
82
+ // must not produce a second step for the same physical notch.
83
+ if (final !== 'M')
84
+ return null;
85
+ return { kind: 'wheel', dir: (b & 1) === 1 ? 'down' : 'up', x, y, ...modifiers(b) };
86
+ }
87
+ if (encoding !== 'sgr')
88
+ return null; // I-11
89
+ // 2. Motion while a button is held (`?1002h`). `3` is "no button", which
90
+ // `?1002` should never send and `?1003` sends constantly — dropping it is
91
+ // what keeps an any-event terminal from painting a selection with no button
92
+ // down at all.
93
+ if ((b & 32) !== 0) {
94
+ const button = b & 3;
95
+ if (button === 3)
96
+ return null;
97
+ return { kind: 'drag', button: button, x, y, ...modifiers(b) };
98
+ }
99
+ // 3. Release. In SGR the final byte carries the transition, so this is
100
+ // unambiguous — unlike X10, which is why step 1's guard exists.
101
+ //
102
+ // A release whose button field reads `3` is a terminal saying "some button
103
+ // came up" without naming it. It is mapped to button 0 rather than dropped:
104
+ // the ONE thing a release must always do is let go of `hold`, and dropping
105
+ // it would leave the viewport frozen — the exact failure I-11 is about. A
106
+ // spurious button-0 release is harmless, because a selection can only exist
107
+ // if button 0 was pressed in the first place.
108
+ if (final === 'm') {
109
+ const button = b & 3;
110
+ return { kind: 'release', button: (button === 3 ? 0 : button), x, y, ...modifiers(b) };
111
+ }
112
+ // 4. Press.
113
+ const button = b & 3;
114
+ if (button === 3)
59
115
  return null;
60
- return {
61
- kind: 'wheel',
62
- dir: (b & 1) === 1 ? 'down' : 'up',
63
- x,
64
- y,
65
- shift: (b & 4) !== 0,
66
- alt: (b & 8) !== 0,
67
- ctrl: (b & 16) !== 0,
68
- };
116
+ return { kind: 'press', button: button, x, y, ...modifiers(b) };
69
117
  }
70
118
  /**
71
119
  * X10 legacy reports (`\x1b[M` + three chars, each `code - 32`).
@@ -84,7 +132,13 @@ function matchX10(s) {
84
132
  if (codes.some((c) => c < 32 || c > 255))
85
133
  return { kind: 'complete', length: 6, event: null };
86
134
  const [b, x, y] = codes;
87
- return { kind: 'complete', length: 6, event: decodeWheel(b - 32, x - 32, y - 32, 'M') };
135
+ return {
136
+ kind: 'complete',
137
+ length: 6,
138
+ // `'x10'` is what makes I-11 a property of the code rather than of somebody's
139
+ // memory: this encoding yields wheel events or nothing, ever.
140
+ event: decodeReport(b - 32, x - 32, y - 32, 'M', 'x10'),
141
+ };
88
142
  }
89
143
  /** Classify the escape sequence starting at `s[0] === ESC`. */
90
144
  function matchReport(s) {
@@ -100,7 +154,7 @@ function matchReport(s) {
100
154
  const b = Number(m[1]);
101
155
  const x = Number(m[2]);
102
156
  const y = Number(m[3]);
103
- return { kind: 'complete', length: m[0].length, event: decodeWheel(b, x, y, m[4]) };
157
+ return { kind: 'complete', length: m[0].length, event: decodeReport(b, x, y, m[4], 'sgr') };
104
158
  }
105
159
  return SGR_PREFIX.test(s) ? { kind: 'partial' } : { kind: 'none' };
106
160
  }
@@ -109,12 +163,17 @@ function matchReport(s) {
109
163
  return { kind: 'none' };
110
164
  }
111
165
  /**
112
- * Split a decoded stdin chunk into wheel events, passthrough text, and a
166
+ * Split a decoded stdin chunk into mouse events, passthrough text, and a
113
167
  * trailing prefix to carry into the next chunk.
114
168
  *
115
169
  * The caller is expected to feed `pending + nextChunk` back in; ConPTY does
116
170
  * split writes mid-sequence, and a report torn across two chunks would
117
171
  * otherwise leak its tail into the composer (R-6).
172
+ *
173
+ * ONE CHANNEL, NOT TWO (§4.4.2). Widening this list to carry buttons alongside
174
+ * wheel notches is what keeps ARRIVAL ORDER intact between them; a second
175
+ * channel would let a press overtake the notch that preceded it, and the
176
+ * selection would then be anchored against rows that had already scrolled.
118
177
  */
119
178
  export function splitMouseEvents(buffer) {
120
179
  const events = [];
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Paste recognition primitives (tui-paste-handling section 5.1) -- pure,
3
+ * stream-free, no timers.
4
+ *
5
+ * WHY THIS LIVES BELOW INK. `ink/build/components/App.js:131-138` reads stdin on
6
+ * `'readable'` and emits each chunk as one `'input'` event;
7
+ * `parse-keypress.js:145-148` maps the exact string `'\r'` to `name: 'return'`,
8
+ * which `PromptInput` reads as "send the message". A 15-line paste on a Windows
9
+ * console routinely arrives as a dozen chunks that are exactly `"\r"`, and each
10
+ * one SUBMITS THE DRAFT AS IT STOOD. No component-level heuristic can undo a
11
+ * `key.return` that has already fired, so recognition has to happen on the byte
12
+ * stream, in front of Ink -- the same lesson `mouse-events.ts` records.
13
+ *
14
+ * Two tiers feed one framing (D-4). Tier 1 is bracketed paste and is exact;
15
+ * Tier 2 is a heuristic on chunk SHAPE for the terminals that ignore
16
+ * `DECSET 2004`. Both are about what a keyboard can physically emit.
17
+ */
18
+ /** `DECSET 2004` wraps a paste in these two markers. */
19
+ export declare const PASTE_BEGIN_MARK = "\u001B[200~";
20
+ export declare const PASTE_END_MARK = "\u001B[201~";
21
+ /** The longest tail worth holding back while a marker might still complete. */
22
+ export declare const PASTE_MARK_LENGTH: number;
23
+ /**
24
+ * True when `s` is a PROPER prefix of either paste marker (never a whole one).
25
+ *
26
+ * Both markers are held, in every mode, whether or not THIS process enabled DEC
27
+ * 2004 (I-16): the user's shell, `tmux` or a wrapper script may have left it on,
28
+ * and an unconsumed marker is a literal `[200~` typed into the draft -- the
29
+ * exact failure the feature exists to prevent.
30
+ */
31
+ export declare function isPastePrefix(s: string): boolean;
32
+ /**
33
+ * Length of the trailing run of `text` that is a proper prefix of a paste
34
+ * marker, or 0. Shortest match wins, so only the bytes that could still become a
35
+ * marker are held back.
36
+ */
37
+ export declare function trailingPastePrefixLength(text: string): number;
38
+ /**
39
+ * Tier 2: is this chunk a paste, or keystrokes?
40
+ *
41
+ * The PRIMARY rule -- "longer than one character AND contains a line break" --
42
+ * is exactly the shape defect A needs, so the two coincide: any chunk that could
43
+ * submit the draft by accident is classified as a paste. A lone `\r` is the
44
+ * Enter key and stays `'keys'`.
45
+ *
46
+ * The SECONDARY rule needs `PASTE_MIN_BURST_CHARS` printable characters in one
47
+ * chunk. It excludes chunks carrying any C0 byte but TAB, which is what keeps an
48
+ * arrow-key burst (`\x1b[B\x1b[B...`, and every wheel notch translated by
49
+ * alternate scroll) out of the paste path.
50
+ */
51
+ export declare function classifyChunk(text: string, minBurstChars?: number): 'paste' | 'keys';
52
+ /**
53
+ * Make a payload safe to put in a buffer and draw through `<Text>` (G2).
54
+ *
55
+ * 1. CRLF and lone CR both become exactly ONE `\n` -- terminals send CR for a
56
+ * newline in pasted text, and a lone LF is dropped by Ink's key parser
57
+ * entirely, so both have to be normalised here or a pasted line break either
58
+ * submits the message or silently vanishes.
59
+ * 2. Every C0 byte except `\n` and `\t` is removed. Ink writes `<Text>` children
60
+ * to the terminal without escaping anything, so a surviving `\r` overwrites
61
+ * the row it is drawn on and a surviving `\x1b` repaints the frame's colours.
62
+ * 3. DEL and every C1 byte (U+0080..U+009F) go too, and that is NOT padding:
63
+ * U+009B is the single-byte CSI and U+0085 is NEL, and a terminal that decodes
64
+ * them acts on them exactly as it acts on `\x1b[`. A file pasted out of a
65
+ * non-UTF-8 editor walks straight through a "strip C0" rule.
66
+ * 4. TAB is KEPT. Indentation is content, and it is what the model receives.
67
+ *
68
+ * Applied to PAYLOAD ONLY, never to keystrokes, so `Ctrl+A` and Escape keep
69
+ * working. Non-ASCII text (CJK, emoji) is untouched.
70
+ */
71
+ export declare function sanitisePaste(raw: string): string;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Paste recognition primitives (tui-paste-handling section 5.1) -- pure,
3
+ * stream-free, no timers.
4
+ *
5
+ * WHY THIS LIVES BELOW INK. `ink/build/components/App.js:131-138` reads stdin on
6
+ * `'readable'` and emits each chunk as one `'input'` event;
7
+ * `parse-keypress.js:145-148` maps the exact string `'\r'` to `name: 'return'`,
8
+ * which `PromptInput` reads as "send the message". A 15-line paste on a Windows
9
+ * console routinely arrives as a dozen chunks that are exactly `"\r"`, and each
10
+ * one SUBMITS THE DRAFT AS IT STOOD. No component-level heuristic can undo a
11
+ * `key.return` that has already fired, so recognition has to happen on the byte
12
+ * stream, in front of Ink -- the same lesson `mouse-events.ts` records.
13
+ *
14
+ * Two tiers feed one framing (D-4). Tier 1 is bracketed paste and is exact;
15
+ * Tier 2 is a heuristic on chunk SHAPE for the terminals that ignore
16
+ * `DECSET 2004`. Both are about what a keyboard can physically emit.
17
+ */
18
+ import { PASTE_MIN_BURST_CHARS } from './limits.js';
19
+ /** `DECSET 2004` wraps a paste in these two markers. */
20
+ export const PASTE_BEGIN_MARK = '\x1b[200~';
21
+ export const PASTE_END_MARK = '\x1b[201~';
22
+ /** The longest tail worth holding back while a marker might still complete. */
23
+ export const PASTE_MARK_LENGTH = PASTE_BEGIN_MARK.length;
24
+ /**
25
+ * True when `s` is a PROPER prefix of either paste marker (never a whole one).
26
+ *
27
+ * Both markers are held, in every mode, whether or not THIS process enabled DEC
28
+ * 2004 (I-16): the user's shell, `tmux` or a wrapper script may have left it on,
29
+ * and an unconsumed marker is a literal `[200~` typed into the draft -- the
30
+ * exact failure the feature exists to prevent.
31
+ */
32
+ export function isPastePrefix(s) {
33
+ if (s.length === 0 || s.length >= PASTE_MARK_LENGTH)
34
+ return false;
35
+ return PASTE_BEGIN_MARK.startsWith(s) || PASTE_END_MARK.startsWith(s);
36
+ }
37
+ /**
38
+ * Length of the trailing run of `text` that is a proper prefix of a paste
39
+ * marker, or 0. Shortest match wins, so only the bytes that could still become a
40
+ * marker are held back.
41
+ */
42
+ export function trailingPastePrefixLength(text) {
43
+ const max = Math.min(text.length, PASTE_MARK_LENGTH - 1);
44
+ for (let k = 1; k <= max; k += 1) {
45
+ if (isPastePrefix(text.slice(text.length - k)))
46
+ return k;
47
+ }
48
+ return 0;
49
+ }
50
+ /** Any C0 control byte other than TAB -- ESC, BEL, NUL, CR, LF included. */
51
+ const HAS_CONTROL_EXCEPT_TAB = /[\u0000-\u0008\u000A-\u001F]/;
52
+ /**
53
+ * Tier 2: is this chunk a paste, or keystrokes?
54
+ *
55
+ * The PRIMARY rule -- "longer than one character AND contains a line break" --
56
+ * is exactly the shape defect A needs, so the two coincide: any chunk that could
57
+ * submit the draft by accident is classified as a paste. A lone `\r` is the
58
+ * Enter key and stays `'keys'`.
59
+ *
60
+ * The SECONDARY rule needs `PASTE_MIN_BURST_CHARS` printable characters in one
61
+ * chunk. It excludes chunks carrying any C0 byte but TAB, which is what keeps an
62
+ * arrow-key burst (`\x1b[B\x1b[B...`, and every wheel notch translated by
63
+ * alternate scroll) out of the paste path.
64
+ */
65
+ export function classifyChunk(text, minBurstChars = PASTE_MIN_BURST_CHARS) {
66
+ if (text.length > 1 && /[\r\n]/.test(text))
67
+ return 'paste';
68
+ if (text.length >= minBurstChars && !HAS_CONTROL_EXCEPT_TAB.test(text))
69
+ return 'paste';
70
+ return 'keys';
71
+ }
72
+ /**
73
+ * Make a payload safe to put in a buffer and draw through `<Text>` (G2).
74
+ *
75
+ * 1. CRLF and lone CR both become exactly ONE `\n` -- terminals send CR for a
76
+ * newline in pasted text, and a lone LF is dropped by Ink's key parser
77
+ * entirely, so both have to be normalised here or a pasted line break either
78
+ * submits the message or silently vanishes.
79
+ * 2. Every C0 byte except `\n` and `\t` is removed. Ink writes `<Text>` children
80
+ * to the terminal without escaping anything, so a surviving `\r` overwrites
81
+ * the row it is drawn on and a surviving `\x1b` repaints the frame's colours.
82
+ * 3. DEL and every C1 byte (U+0080..U+009F) go too, and that is NOT padding:
83
+ * U+009B is the single-byte CSI and U+0085 is NEL, and a terminal that decodes
84
+ * them acts on them exactly as it acts on `\x1b[`. A file pasted out of a
85
+ * non-UTF-8 editor walks straight through a "strip C0" rule.
86
+ * 4. TAB is KEPT. Indentation is content, and it is what the model receives.
87
+ *
88
+ * Applied to PAYLOAD ONLY, never to keystrokes, so `Ctrl+A` and Escape keep
89
+ * working. Non-ASCII text (CJK, emoji) is untouched.
90
+ */
91
+ export function sanitisePaste(raw) {
92
+ return raw
93
+ .replace(/\r\n?/g, '\n')
94
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
95
+ }