lonny-agent 0.1.2 → 0.1.8

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 (216) hide show
  1. package/.kilo/plans/1780064105789-mighty-pixel.md +171 -0
  2. package/.lonny/fix-new-command-session-cleanup.md +65 -0
  3. package/.lonny/tui-autocomplete-above.md +62 -0
  4. package/dist/agent/__tests__/compaction.test.js +6 -4
  5. package/dist/agent/__tests__/compaction.test.js.map +1 -1
  6. package/dist/agent/llm.d.ts +1 -1
  7. package/dist/agent/llm.d.ts.map +1 -1
  8. package/dist/agent/prompt-builder.d.ts.map +1 -1
  9. package/dist/agent/prompt-builder.js +26 -8
  10. package/dist/agent/prompt-builder.js.map +1 -1
  11. package/dist/agent/providers/anthropic.d.ts +1 -1
  12. package/dist/agent/providers/anthropic.d.ts.map +1 -1
  13. package/dist/agent/providers/anthropic.js +2 -2
  14. package/dist/agent/providers/anthropic.js.map +1 -1
  15. package/dist/agent/providers/google.d.ts +1 -1
  16. package/dist/agent/providers/google.d.ts.map +1 -1
  17. package/dist/agent/providers/google.js +2 -1
  18. package/dist/agent/providers/google.js.map +1 -1
  19. package/dist/agent/providers/ollama.d.ts +1 -1
  20. package/dist/agent/providers/ollama.d.ts.map +1 -1
  21. package/dist/agent/providers/ollama.js +2 -1
  22. package/dist/agent/providers/ollama.js.map +1 -1
  23. package/dist/agent/providers/openai.d.ts +1 -1
  24. package/dist/agent/providers/openai.d.ts.map +1 -1
  25. package/dist/agent/providers/openai.js +2 -2
  26. package/dist/agent/providers/openai.js.map +1 -1
  27. package/dist/agent/session.d.ts +1 -0
  28. package/dist/agent/session.d.ts.map +1 -1
  29. package/dist/agent/session.js +8 -2
  30. package/dist/agent/session.js.map +1 -1
  31. package/dist/pi-tui/autocomplete.d.ts +54 -0
  32. package/dist/pi-tui/autocomplete.d.ts.map +1 -0
  33. package/dist/pi-tui/autocomplete.js +636 -0
  34. package/dist/pi-tui/autocomplete.js.map +1 -0
  35. package/dist/pi-tui/components/box.d.ts +22 -0
  36. package/dist/pi-tui/components/box.d.ts.map +1 -0
  37. package/dist/pi-tui/components/box.js +104 -0
  38. package/dist/pi-tui/components/box.js.map +1 -0
  39. package/dist/pi-tui/components/cancellable-loader.d.ts +22 -0
  40. package/dist/pi-tui/components/cancellable-loader.d.ts.map +1 -0
  41. package/dist/pi-tui/components/cancellable-loader.js +35 -0
  42. package/dist/pi-tui/components/cancellable-loader.js.map +1 -0
  43. package/dist/pi-tui/components/editor.d.ts +249 -0
  44. package/dist/pi-tui/components/editor.d.ts.map +1 -0
  45. package/dist/pi-tui/components/editor.js +1883 -0
  46. package/dist/pi-tui/components/editor.js.map +1 -0
  47. package/dist/pi-tui/components/image.d.ts +28 -0
  48. package/dist/pi-tui/components/image.d.ts.map +1 -0
  49. package/dist/pi-tui/components/image.js +90 -0
  50. package/dist/pi-tui/components/image.js.map +1 -0
  51. package/dist/pi-tui/components/input.d.ts +37 -0
  52. package/dist/pi-tui/components/input.d.ts.map +1 -0
  53. package/dist/pi-tui/components/input.js +382 -0
  54. package/dist/pi-tui/components/input.js.map +1 -0
  55. package/dist/pi-tui/components/loader.d.ts +31 -0
  56. package/dist/pi-tui/components/loader.d.ts.map +1 -0
  57. package/dist/pi-tui/components/loader.js +70 -0
  58. package/dist/pi-tui/components/loader.js.map +1 -0
  59. package/dist/pi-tui/components/markdown.d.ts +96 -0
  60. package/dist/pi-tui/components/markdown.d.ts.map +1 -0
  61. package/dist/pi-tui/components/markdown.js +647 -0
  62. package/dist/pi-tui/components/markdown.js.map +1 -0
  63. package/dist/pi-tui/components/select-list.d.ts +50 -0
  64. package/dist/pi-tui/components/select-list.d.ts.map +1 -0
  65. package/dist/pi-tui/components/select-list.js +167 -0
  66. package/dist/pi-tui/components/select-list.js.map +1 -0
  67. package/dist/pi-tui/components/settings-list.d.ts +50 -0
  68. package/dist/pi-tui/components/settings-list.d.ts.map +1 -0
  69. package/dist/pi-tui/components/settings-list.js +189 -0
  70. package/dist/pi-tui/components/settings-list.js.map +1 -0
  71. package/dist/pi-tui/components/spacer.d.ts +12 -0
  72. package/dist/pi-tui/components/spacer.d.ts.map +1 -0
  73. package/dist/pi-tui/components/spacer.js +23 -0
  74. package/dist/pi-tui/components/spacer.js.map +1 -0
  75. package/dist/pi-tui/components/text.d.ts +19 -0
  76. package/dist/pi-tui/components/text.d.ts.map +1 -0
  77. package/dist/pi-tui/components/text.js +91 -0
  78. package/dist/pi-tui/components/text.js.map +1 -0
  79. package/dist/pi-tui/components/truncated-text.d.ts +13 -0
  80. package/dist/pi-tui/components/truncated-text.d.ts.map +1 -0
  81. package/dist/pi-tui/components/truncated-text.js +51 -0
  82. package/dist/pi-tui/components/truncated-text.js.map +1 -0
  83. package/dist/pi-tui/editor-component.d.ts +39 -0
  84. package/dist/pi-tui/editor-component.d.ts.map +1 -0
  85. package/dist/pi-tui/editor-component.js +2 -0
  86. package/dist/pi-tui/editor-component.js.map +1 -0
  87. package/dist/pi-tui/fuzzy.d.ts +16 -0
  88. package/dist/pi-tui/fuzzy.d.ts.map +1 -0
  89. package/dist/pi-tui/fuzzy.js +110 -0
  90. package/dist/pi-tui/fuzzy.js.map +1 -0
  91. package/dist/pi-tui/index.d.ts +23 -0
  92. package/dist/pi-tui/index.d.ts.map +1 -0
  93. package/dist/pi-tui/index.js +32 -0
  94. package/dist/pi-tui/index.js.map +1 -0
  95. package/dist/pi-tui/keybindings.d.ts +193 -0
  96. package/dist/pi-tui/keybindings.d.ts.map +1 -0
  97. package/dist/pi-tui/keybindings.js +174 -0
  98. package/dist/pi-tui/keybindings.js.map +1 -0
  99. package/dist/pi-tui/keys.d.ts +184 -0
  100. package/dist/pi-tui/keys.d.ts.map +1 -0
  101. package/dist/pi-tui/keys.js +1182 -0
  102. package/dist/pi-tui/keys.js.map +1 -0
  103. package/dist/pi-tui/kill-ring.d.ts +28 -0
  104. package/dist/pi-tui/kill-ring.d.ts.map +1 -0
  105. package/dist/pi-tui/kill-ring.js +44 -0
  106. package/dist/pi-tui/kill-ring.js.map +1 -0
  107. package/dist/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  108. package/dist/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  109. package/dist/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  110. package/dist/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  111. package/dist/pi-tui/native-modifiers.d.ts +3 -0
  112. package/dist/pi-tui/native-modifiers.d.ts.map +1 -0
  113. package/dist/pi-tui/native-modifiers.js +53 -0
  114. package/dist/pi-tui/native-modifiers.js.map +1 -0
  115. package/dist/pi-tui/stdin-buffer.d.ts +50 -0
  116. package/dist/pi-tui/stdin-buffer.d.ts.map +1 -0
  117. package/dist/pi-tui/stdin-buffer.js +361 -0
  118. package/dist/pi-tui/stdin-buffer.js.map +1 -0
  119. package/dist/pi-tui/terminal-image.d.ts +90 -0
  120. package/dist/pi-tui/terminal-image.d.ts.map +1 -0
  121. package/dist/pi-tui/terminal-image.js +343 -0
  122. package/dist/pi-tui/terminal-image.js.map +1 -0
  123. package/dist/pi-tui/terminal.d.ts +113 -0
  124. package/dist/pi-tui/terminal.d.ts.map +1 -0
  125. package/dist/pi-tui/terminal.js +478 -0
  126. package/dist/pi-tui/terminal.js.map +1 -0
  127. package/dist/pi-tui/tui.d.ts +227 -0
  128. package/dist/pi-tui/tui.d.ts.map +1 -0
  129. package/dist/pi-tui/tui.js +1091 -0
  130. package/dist/pi-tui/tui.js.map +1 -0
  131. package/dist/pi-tui/undo-stack.d.ts +17 -0
  132. package/dist/pi-tui/undo-stack.d.ts.map +1 -0
  133. package/dist/pi-tui/undo-stack.js +25 -0
  134. package/dist/pi-tui/undo-stack.js.map +1 -0
  135. package/dist/pi-tui/utils.d.ts +84 -0
  136. package/dist/pi-tui/utils.d.ts.map +1 -0
  137. package/dist/pi-tui/utils.js +1024 -0
  138. package/dist/pi-tui/utils.js.map +1 -0
  139. package/dist/pi-tui/word-navigation.d.ts +25 -0
  140. package/dist/pi-tui/word-navigation.d.ts.map +1 -0
  141. package/dist/pi-tui/word-navigation.js +98 -0
  142. package/dist/pi-tui/word-navigation.js.map +1 -0
  143. package/dist/tools/__tests__/edit.test.js +85 -0
  144. package/dist/tools/__tests__/edit.test.js.map +1 -1
  145. package/dist/tools/edit.d.ts.map +1 -1
  146. package/dist/tools/edit.js +23 -1
  147. package/dist/tools/edit.js.map +1 -1
  148. package/dist/tui/components.d.ts +1 -1
  149. package/dist/tui/components.d.ts.map +1 -1
  150. package/dist/tui/components.js +1 -1
  151. package/dist/tui/components.js.map +1 -1
  152. package/dist/tui/editor-patch.d.ts +13 -0
  153. package/dist/tui/editor-patch.d.ts.map +1 -0
  154. package/dist/tui/editor-patch.js +41 -0
  155. package/dist/tui/editor-patch.js.map +1 -0
  156. package/dist/tui/index.d.ts.map +1 -1
  157. package/dist/tui/index.js +15 -3
  158. package/dist/tui/index.js.map +1 -1
  159. package/dist/web/index.d.ts.map +1 -1
  160. package/dist/web/index.js +9 -2
  161. package/dist/web/index.js.map +1 -1
  162. package/dist/web/public/app.js +65 -4
  163. package/dist/web/public/style.css +49 -0
  164. package/dist/web/session-bridge.d.ts +0 -4
  165. package/dist/web/session-bridge.d.ts.map +1 -1
  166. package/dist/web/session-bridge.js +16 -2
  167. package/dist/web/session-bridge.js.map +1 -1
  168. package/package.json +5 -3
  169. package/src/agent/__tests__/compaction.test.ts +6 -4
  170. package/src/agent/llm.ts +5 -1
  171. package/src/agent/prompt-builder.ts +27 -8
  172. package/src/agent/providers/anthropic.ts +15 -8
  173. package/src/agent/providers/google.ts +6 -1
  174. package/src/agent/providers/ollama.ts +6 -1
  175. package/src/agent/providers/openai.ts +21 -12
  176. package/src/agent/session.ts +12 -2
  177. package/src/pi-tui/autocomplete.ts +808 -0
  178. package/src/pi-tui/components/box.ts +137 -0
  179. package/src/pi-tui/components/cancellable-loader.ts +40 -0
  180. package/src/pi-tui/components/editor.ts +2321 -0
  181. package/src/pi-tui/components/image.ts +130 -0
  182. package/src/pi-tui/components/input.ts +456 -0
  183. package/src/pi-tui/components/loader.ts +93 -0
  184. package/src/pi-tui/components/markdown.ts +839 -0
  185. package/src/pi-tui/components/select-list.ts +263 -0
  186. package/src/pi-tui/components/settings-list.ts +264 -0
  187. package/src/pi-tui/components/spacer.ts +28 -0
  188. package/src/pi-tui/components/text.ts +113 -0
  189. package/src/pi-tui/components/truncated-text.ts +65 -0
  190. package/src/pi-tui/editor-component.ts +74 -0
  191. package/src/pi-tui/fuzzy.ts +137 -0
  192. package/src/pi-tui/index.ts +115 -0
  193. package/src/pi-tui/keybindings.ts +245 -0
  194. package/src/pi-tui/keys.ts +1440 -0
  195. package/src/pi-tui/kill-ring.ts +46 -0
  196. package/src/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  197. package/src/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  198. package/src/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  199. package/src/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  200. package/src/pi-tui/native-modifiers.ts +65 -0
  201. package/src/pi-tui/stdin-buffer.ts +434 -0
  202. package/src/pi-tui/terminal-image.ts +469 -0
  203. package/src/pi-tui/terminal.ts +594 -0
  204. package/src/pi-tui/tui.ts +1376 -0
  205. package/src/pi-tui/undo-stack.ts +28 -0
  206. package/src/pi-tui/utils.ts +1176 -0
  207. package/src/pi-tui/word-navigation.ts +127 -0
  208. package/src/tools/__tests__/edit.test.ts +91 -0
  209. package/src/tools/edit.ts +27 -1
  210. package/src/tui/components.ts +2 -2
  211. package/src/tui/index.ts +24 -10
  212. package/src/web/index.ts +9 -2
  213. package/src/web/public/app.js +65 -4
  214. package/src/web/public/style.css +49 -0
  215. package/src/web/session-bridge.ts +13 -2
  216. package/tsconfig.json +2 -2
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Ring buffer for Emacs-style kill/yank operations.
3
+ *
4
+ * Tracks killed (deleted) text entries. Consecutive kills can accumulate
5
+ * into a single entry. Supports yank (paste most recent) and yank-pop
6
+ * (cycle through older entries).
7
+ */
8
+ export class KillRing {
9
+ private ring: string[] = []
10
+
11
+ /**
12
+ * Add text to the kill ring.
13
+ *
14
+ * @param text - The killed text to add
15
+ * @param opts - Push options
16
+ * @param opts.prepend - If accumulating, prepend (backward deletion) or append (forward deletion)
17
+ * @param opts.accumulate - Merge with the most recent entry instead of creating a new one
18
+ */
19
+ push(text: string, opts: { prepend: boolean; accumulate?: boolean }): void {
20
+ if (!text) return
21
+
22
+ if (opts.accumulate && this.ring.length > 0) {
23
+ const last = this.ring.pop()!
24
+ this.ring.push(opts.prepend ? text + last : last + text)
25
+ } else {
26
+ this.ring.push(text)
27
+ }
28
+ }
29
+
30
+ /** Get most recent entry without modifying the ring. */
31
+ peek(): string | undefined {
32
+ return this.ring.length > 0 ? this.ring[this.ring.length - 1] : undefined
33
+ }
34
+
35
+ /** Move last entry to front (for yank-pop cycling). */
36
+ rotate(): void {
37
+ if (this.ring.length > 1) {
38
+ const last = this.ring.pop()!
39
+ this.ring.unshift(last)
40
+ }
41
+ }
42
+
43
+ get length(): number {
44
+ return this.ring.length
45
+ }
46
+ }
@@ -0,0 +1,65 @@
1
+ import { createRequire } from 'node:module'
2
+ import * as path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const cjsRequire = createRequire(import.meta.url)
6
+
7
+ export type ModifierKey = 'shift' | 'command' | 'control' | 'option'
8
+
9
+ type NativeModifiersHelper = {
10
+ isModifierPressed: (name: ModifierKey) => boolean
11
+ }
12
+
13
+ let nativeModifiersHelper: NativeModifiersHelper | null | undefined
14
+
15
+ function isNativeModifiersHelper(value: unknown): value is NativeModifiersHelper {
16
+ if (typeof value !== 'object' || value === null) return false
17
+ const candidate = (value as { isModifierPressed?: unknown }).isModifierPressed
18
+ return typeof candidate === 'function'
19
+ }
20
+
21
+ function loadNativeModifiersHelper(): NativeModifiersHelper | undefined {
22
+ if (nativeModifiersHelper !== undefined) return nativeModifiersHelper ?? undefined
23
+ nativeModifiersHelper = null
24
+ if (process.platform !== 'darwin') return undefined
25
+ const arch = process.arch
26
+ if (arch !== 'x64' && arch !== 'arm64') return undefined
27
+
28
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url))
29
+ const nativePath = path.join(
30
+ 'native',
31
+ 'darwin',
32
+ 'prebuilds',
33
+ `darwin-${arch}`,
34
+ 'darwin-modifiers.node',
35
+ )
36
+ const candidates = [
37
+ path.join(moduleDir, '..', nativePath),
38
+ path.join(moduleDir, nativePath),
39
+ path.join(path.dirname(process.execPath), nativePath),
40
+ ]
41
+
42
+ for (const modulePath of candidates) {
43
+ try {
44
+ const helper = cjsRequire(modulePath) as unknown
45
+ if (isNativeModifiersHelper(helper)) {
46
+ nativeModifiersHelper = helper
47
+ return helper
48
+ }
49
+ } catch {
50
+ // Try the next possible packaging location.
51
+ }
52
+ }
53
+
54
+ return undefined
55
+ }
56
+
57
+ export function isNativeModifierPressed(key: ModifierKey): boolean {
58
+ const helper = loadNativeModifiersHelper()
59
+ if (!helper) return false
60
+ try {
61
+ return helper.isModifierPressed(key) === true
62
+ } catch {
63
+ return false
64
+ }
65
+ }
@@ -0,0 +1,434 @@
1
+ /**
2
+ * StdinBuffer buffers input and emits complete sequences.
3
+ *
4
+ * This is necessary because stdin data events can arrive in partial chunks,
5
+ * especially for escape sequences like mouse events. Without buffering,
6
+ * partial sequences can be misinterpreted as regular keypresses.
7
+ *
8
+ * For example, the mouse SGR sequence `\x1b[<35;20;5m` might arrive as:
9
+ * - Event 1: `\x1b`
10
+ * - Event 2: `[<35`
11
+ * - Event 3: `;20;5m`
12
+ *
13
+ * The buffer accumulates these until a complete sequence is detected.
14
+ * Call the `process()` method to feed input data.
15
+ *
16
+ * Based on code from OpenTUI (https://github.com/anomalyco/opentui)
17
+ * MIT License - Copyright (c) 2025 opentui
18
+ */
19
+
20
+ import { EventEmitter } from 'events'
21
+
22
+ const ESC = '\x1b'
23
+ const BRACKETED_PASTE_START = '\x1b[200~'
24
+ const BRACKETED_PASTE_END = '\x1b[201~'
25
+
26
+ /**
27
+ * Check if a string is a complete escape sequence or needs more data
28
+ */
29
+ function isCompleteSequence(data: string): 'complete' | 'incomplete' | 'not-escape' {
30
+ if (!data.startsWith(ESC)) {
31
+ return 'not-escape'
32
+ }
33
+
34
+ if (data.length === 1) {
35
+ return 'incomplete'
36
+ }
37
+
38
+ const afterEsc = data.slice(1)
39
+
40
+ // CSI sequences: ESC [
41
+ if (afterEsc.startsWith('[')) {
42
+ // Check for old-style mouse sequence: ESC[M + 3 bytes
43
+ if (afterEsc.startsWith('[M')) {
44
+ // Old-style mouse needs ESC[M + 3 bytes = 6 total
45
+ return data.length >= 6 ? 'complete' : 'incomplete'
46
+ }
47
+ return isCompleteCsiSequence(data)
48
+ }
49
+
50
+ // OSC sequences: ESC ]
51
+ if (afterEsc.startsWith(']')) {
52
+ return isCompleteOscSequence(data)
53
+ }
54
+
55
+ // DCS sequences: ESC P ... ESC \ (includes XTVersion responses)
56
+ if (afterEsc.startsWith('P')) {
57
+ return isCompleteDcsSequence(data)
58
+ }
59
+
60
+ // APC sequences: ESC _ ... ESC \ (includes Kitty graphics responses)
61
+ if (afterEsc.startsWith('_')) {
62
+ return isCompleteApcSequence(data)
63
+ }
64
+
65
+ // SS3 sequences: ESC O
66
+ if (afterEsc.startsWith('O')) {
67
+ // ESC O followed by a single character
68
+ return afterEsc.length >= 2 ? 'complete' : 'incomplete'
69
+ }
70
+
71
+ // Meta key sequences: ESC followed by a single character
72
+ if (afterEsc.length === 1) {
73
+ return 'complete'
74
+ }
75
+
76
+ // Unknown escape sequence - treat as complete
77
+ return 'complete'
78
+ }
79
+
80
+ /**
81
+ * Check if CSI sequence is complete
82
+ * CSI sequences: ESC [ ... followed by a final byte (0x40-0x7E)
83
+ */
84
+ function isCompleteCsiSequence(data: string): 'complete' | 'incomplete' {
85
+ if (!data.startsWith(`${ESC}[`)) {
86
+ return 'complete'
87
+ }
88
+
89
+ // Need at least ESC [ and one more character
90
+ if (data.length < 3) {
91
+ return 'incomplete'
92
+ }
93
+
94
+ const payload = data.slice(2)
95
+
96
+ // CSI sequences end with a byte in the range 0x40-0x7E (@-~)
97
+ // This includes all letters and several special characters
98
+ const lastChar = payload[payload.length - 1]
99
+ const lastCharCode = lastChar.charCodeAt(0)
100
+
101
+ if (lastCharCode >= 0x40 && lastCharCode <= 0x7e) {
102
+ // Special handling for SGR mouse sequences
103
+ // Format: ESC[<B;X;Ym or ESC[<B;X;YM
104
+ if (payload.startsWith('<')) {
105
+ // Must have format: <digits;digits;digits[Mm]
106
+ const mouseMatch = /^<\d+;\d+;\d+[Mm]$/.test(payload)
107
+ if (mouseMatch) {
108
+ return 'complete'
109
+ }
110
+ // If it ends with M or m but doesn't match the pattern, still incomplete
111
+ if (lastChar === 'M' || lastChar === 'm') {
112
+ // Check if we have the right structure
113
+ const parts = payload.slice(1, -1).split(';')
114
+ if (parts.length === 3 && parts.every(p => /^\d+$/.test(p))) {
115
+ return 'complete'
116
+ }
117
+ }
118
+
119
+ return 'incomplete'
120
+ }
121
+
122
+ return 'complete'
123
+ }
124
+
125
+ return 'incomplete'
126
+ }
127
+
128
+ /**
129
+ * Check if OSC sequence is complete
130
+ * OSC sequences: ESC ] ... ST (where ST is ESC \ or BEL)
131
+ */
132
+ function isCompleteOscSequence(data: string): 'complete' | 'incomplete' {
133
+ if (!data.startsWith(`${ESC}]`)) {
134
+ return 'complete'
135
+ }
136
+
137
+ // OSC sequences end with ST (ESC \) or BEL (\x07)
138
+ if (data.endsWith(`${ESC}\\`) || data.endsWith('\x07')) {
139
+ return 'complete'
140
+ }
141
+
142
+ return 'incomplete'
143
+ }
144
+
145
+ /**
146
+ * Check if DCS (Device Control String) sequence is complete
147
+ * DCS sequences: ESC P ... ST (where ST is ESC \)
148
+ * Used for XTVersion responses like ESC P >| ... ESC \
149
+ */
150
+ function isCompleteDcsSequence(data: string): 'complete' | 'incomplete' {
151
+ if (!data.startsWith(`${ESC}P`)) {
152
+ return 'complete'
153
+ }
154
+
155
+ // DCS sequences end with ST (ESC \)
156
+ if (data.endsWith(`${ESC}\\`)) {
157
+ return 'complete'
158
+ }
159
+
160
+ return 'incomplete'
161
+ }
162
+
163
+ /**
164
+ * Check if APC (Application Program Command) sequence is complete
165
+ * APC sequences: ESC _ ... ST (where ST is ESC \)
166
+ * Used for Kitty graphics responses like ESC _ G ... ESC \
167
+ */
168
+ function isCompleteApcSequence(data: string): 'complete' | 'incomplete' {
169
+ if (!data.startsWith(`${ESC}_`)) {
170
+ return 'complete'
171
+ }
172
+
173
+ // APC sequences end with ST (ESC \)
174
+ if (data.endsWith(`${ESC}\\`)) {
175
+ return 'complete'
176
+ }
177
+
178
+ return 'incomplete'
179
+ }
180
+
181
+ /**
182
+ * Split accumulated buffer into complete sequences
183
+ */
184
+ function parseUnmodifiedKittyPrintableCodepoint(sequence: string): number | undefined {
185
+ const match = sequence.match(/^\x1b\[(\d+)(?::\d*)?(?::\d+)?u$/)
186
+ if (!match) return undefined
187
+
188
+ const codepoint = parseInt(match[1]!, 10)
189
+ return codepoint >= 32 ? codepoint : undefined
190
+ }
191
+
192
+ function extractCompleteSequences(buffer: string): { sequences: string[]; remainder: string } {
193
+ const sequences: string[] = []
194
+ let pos = 0
195
+
196
+ while (pos < buffer.length) {
197
+ const remaining = buffer.slice(pos)
198
+
199
+ // Try to extract a sequence starting at this position
200
+ if (remaining.startsWith(ESC)) {
201
+ // Find the end of this escape sequence
202
+ let seqEnd = 1
203
+ while (seqEnd <= remaining.length) {
204
+ const candidate = remaining.slice(0, seqEnd)
205
+ const status = isCompleteSequence(candidate)
206
+
207
+ if (status === 'complete') {
208
+ // WezTerm with enable_kitty_keyboard sends the Escape key press as a
209
+ // raw '\x1b' byte (simple text path in encode_kitty, ignoring
210
+ // DISAMBIGUATE_ESCAPE_CODES) and the release as a full Kitty CSI-u
211
+ // sequence. These arrive concatenated as '\x1b\x1b[27;...u'.
212
+ // The buffer would normally treat '\x1b\x1b' as a complete meta-key
213
+ // sequence (ESC + single char), leaving '[27;...u' to be typed as
214
+ // plain text. If the character immediately following '\x1b\x1b'
215
+ // would begin a new escape sequence, emit only the first ESC and
216
+ // restart from the second.
217
+ if (candidate === '\x1b\x1b') {
218
+ const nextChar = remaining[seqEnd]
219
+ if (
220
+ nextChar === '[' || // CSI
221
+ nextChar === ']' || // OSC
222
+ nextChar === 'O' || // SS3
223
+ nextChar === 'P' || // DCS
224
+ nextChar === '_' // APC
225
+ ) {
226
+ sequences.push(ESC)
227
+ pos += 1
228
+ break
229
+ }
230
+ }
231
+ sequences.push(candidate)
232
+ pos += seqEnd
233
+ break
234
+ } else if (status === 'incomplete') {
235
+ seqEnd++
236
+ } else {
237
+ // Should not happen when starting with ESC
238
+ sequences.push(candidate)
239
+ pos += seqEnd
240
+ break
241
+ }
242
+ }
243
+
244
+ if (seqEnd > remaining.length) {
245
+ return { sequences, remainder: remaining }
246
+ }
247
+ } else {
248
+ // Not an escape sequence - take a single character
249
+ sequences.push(remaining[0]!)
250
+ pos++
251
+ }
252
+ }
253
+
254
+ return { sequences, remainder: '' }
255
+ }
256
+
257
+ export type StdinBufferOptions = {
258
+ /**
259
+ * Maximum time to wait for sequence completion (default: 10ms)
260
+ * After this time, the buffer is flushed even if incomplete
261
+ */
262
+ timeout?: number
263
+ }
264
+
265
+ export type StdinBufferEventMap = {
266
+ data: [string]
267
+ paste: [string]
268
+ }
269
+
270
+ /**
271
+ * Buffers stdin input and emits complete sequences via the 'data' event.
272
+ * Handles partial escape sequences that arrive across multiple chunks.
273
+ */
274
+ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
275
+ private buffer: string = ''
276
+ private timeout: ReturnType<typeof setTimeout> | null = null
277
+ private readonly timeoutMs: number
278
+ private pasteMode: boolean = false
279
+ private pasteBuffer: string = ''
280
+ private pendingKittyPrintableCodepoint: number | undefined
281
+
282
+ constructor(options: StdinBufferOptions = {}) {
283
+ super()
284
+ this.timeoutMs = options.timeout ?? 10
285
+ }
286
+
287
+ public process(data: string | Buffer): void {
288
+ // Clear any pending timeout
289
+ if (this.timeout) {
290
+ clearTimeout(this.timeout)
291
+ this.timeout = null
292
+ }
293
+
294
+ // Handle high-byte conversion (for compatibility with parseKeypress)
295
+ // If buffer has single byte > 127, convert to ESC + (byte - 128)
296
+ let str: string
297
+ if (Buffer.isBuffer(data)) {
298
+ if (data.length === 1 && data[0]! > 127) {
299
+ const byte = data[0]! - 128
300
+ str = `\x1b${String.fromCharCode(byte)}`
301
+ } else {
302
+ str = data.toString()
303
+ }
304
+ } else {
305
+ str = data
306
+ }
307
+
308
+ if (str.length === 0 && this.buffer.length === 0) {
309
+ this.emitDataSequence('')
310
+ return
311
+ }
312
+
313
+ this.buffer += str
314
+
315
+ if (this.pasteMode) {
316
+ this.pasteBuffer += this.buffer
317
+ this.buffer = ''
318
+
319
+ const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END)
320
+ if (endIndex !== -1) {
321
+ const pastedContent = this.pasteBuffer.slice(0, endIndex)
322
+ const remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length)
323
+
324
+ this.pasteMode = false
325
+ this.pasteBuffer = ''
326
+ this.pendingKittyPrintableCodepoint = undefined
327
+
328
+ this.emit('paste', pastedContent)
329
+
330
+ if (remaining.length > 0) {
331
+ this.process(remaining)
332
+ }
333
+ }
334
+ return
335
+ }
336
+
337
+ const startIndex = this.buffer.indexOf(BRACKETED_PASTE_START)
338
+ if (startIndex !== -1) {
339
+ if (startIndex > 0) {
340
+ const beforePaste = this.buffer.slice(0, startIndex)
341
+ const result = extractCompleteSequences(beforePaste)
342
+ for (const sequence of result.sequences) {
343
+ this.emitDataSequence(sequence)
344
+ }
345
+ }
346
+
347
+ this.pendingKittyPrintableCodepoint = undefined
348
+ this.buffer = this.buffer.slice(startIndex + BRACKETED_PASTE_START.length)
349
+ this.pasteMode = true
350
+ this.pasteBuffer = this.buffer
351
+ this.buffer = ''
352
+
353
+ const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END)
354
+ if (endIndex !== -1) {
355
+ const pastedContent = this.pasteBuffer.slice(0, endIndex)
356
+ const remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length)
357
+
358
+ this.pasteMode = false
359
+ this.pasteBuffer = ''
360
+ this.pendingKittyPrintableCodepoint = undefined
361
+
362
+ this.emit('paste', pastedContent)
363
+
364
+ if (remaining.length > 0) {
365
+ this.process(remaining)
366
+ }
367
+ }
368
+ return
369
+ }
370
+
371
+ const result = extractCompleteSequences(this.buffer)
372
+ this.buffer = result.remainder
373
+
374
+ for (const sequence of result.sequences) {
375
+ this.emitDataSequence(sequence)
376
+ }
377
+
378
+ if (this.buffer.length > 0) {
379
+ this.timeout = setTimeout(() => {
380
+ const flushed = this.flush()
381
+
382
+ for (const sequence of flushed) {
383
+ this.emitDataSequence(sequence)
384
+ }
385
+ }, this.timeoutMs)
386
+ }
387
+ }
388
+
389
+ private emitDataSequence(sequence: string): void {
390
+ const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined
391
+ if (rawCodepoint !== undefined && rawCodepoint === this.pendingKittyPrintableCodepoint) {
392
+ this.pendingKittyPrintableCodepoint = undefined
393
+ return
394
+ }
395
+
396
+ this.pendingKittyPrintableCodepoint = parseUnmodifiedKittyPrintableCodepoint(sequence)
397
+ this.emit('data', sequence)
398
+ }
399
+
400
+ flush(): string[] {
401
+ if (this.timeout) {
402
+ clearTimeout(this.timeout)
403
+ this.timeout = null
404
+ }
405
+
406
+ if (this.buffer.length === 0) {
407
+ return []
408
+ }
409
+
410
+ const sequences = [this.buffer]
411
+ this.buffer = ''
412
+ this.pendingKittyPrintableCodepoint = undefined
413
+ return sequences
414
+ }
415
+
416
+ clear(): void {
417
+ if (this.timeout) {
418
+ clearTimeout(this.timeout)
419
+ this.timeout = null
420
+ }
421
+ this.buffer = ''
422
+ this.pasteMode = false
423
+ this.pasteBuffer = ''
424
+ this.pendingKittyPrintableCodepoint = undefined
425
+ }
426
+
427
+ getBuffer(): string {
428
+ return this.buffer
429
+ }
430
+
431
+ destroy(): void {
432
+ this.clear()
433
+ }
434
+ }