lonny-agent 0.1.1 → 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 (225) 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/compaction.d.ts.map +1 -1
  7. package/dist/agent/compaction.js +3 -2
  8. package/dist/agent/compaction.js.map +1 -1
  9. package/dist/agent/llm.d.ts +1 -1
  10. package/dist/agent/llm.d.ts.map +1 -1
  11. package/dist/agent/prompt-builder.d.ts.map +1 -1
  12. package/dist/agent/prompt-builder.js +26 -8
  13. package/dist/agent/prompt-builder.js.map +1 -1
  14. package/dist/agent/providers/anthropic.d.ts +1 -1
  15. package/dist/agent/providers/anthropic.d.ts.map +1 -1
  16. package/dist/agent/providers/anthropic.js +2 -2
  17. package/dist/agent/providers/anthropic.js.map +1 -1
  18. package/dist/agent/providers/google.d.ts +1 -1
  19. package/dist/agent/providers/google.d.ts.map +1 -1
  20. package/dist/agent/providers/google.js +2 -1
  21. package/dist/agent/providers/google.js.map +1 -1
  22. package/dist/agent/providers/ollama.d.ts +1 -1
  23. package/dist/agent/providers/ollama.d.ts.map +1 -1
  24. package/dist/agent/providers/ollama.js +2 -1
  25. package/dist/agent/providers/ollama.js.map +1 -1
  26. package/dist/agent/providers/openai.d.ts +1 -1
  27. package/dist/agent/providers/openai.d.ts.map +1 -1
  28. package/dist/agent/providers/openai.js +2 -2
  29. package/dist/agent/providers/openai.js.map +1 -1
  30. package/dist/agent/session.d.ts +5 -1
  31. package/dist/agent/session.d.ts.map +1 -1
  32. package/dist/agent/session.js +22 -3
  33. package/dist/agent/session.js.map +1 -1
  34. package/dist/pi-tui/autocomplete.d.ts +54 -0
  35. package/dist/pi-tui/autocomplete.d.ts.map +1 -0
  36. package/dist/pi-tui/autocomplete.js +636 -0
  37. package/dist/pi-tui/autocomplete.js.map +1 -0
  38. package/dist/pi-tui/components/box.d.ts +22 -0
  39. package/dist/pi-tui/components/box.d.ts.map +1 -0
  40. package/dist/pi-tui/components/box.js +104 -0
  41. package/dist/pi-tui/components/box.js.map +1 -0
  42. package/dist/pi-tui/components/cancellable-loader.d.ts +22 -0
  43. package/dist/pi-tui/components/cancellable-loader.d.ts.map +1 -0
  44. package/dist/pi-tui/components/cancellable-loader.js +35 -0
  45. package/dist/pi-tui/components/cancellable-loader.js.map +1 -0
  46. package/dist/pi-tui/components/editor.d.ts +249 -0
  47. package/dist/pi-tui/components/editor.d.ts.map +1 -0
  48. package/dist/pi-tui/components/editor.js +1883 -0
  49. package/dist/pi-tui/components/editor.js.map +1 -0
  50. package/dist/pi-tui/components/image.d.ts +28 -0
  51. package/dist/pi-tui/components/image.d.ts.map +1 -0
  52. package/dist/pi-tui/components/image.js +90 -0
  53. package/dist/pi-tui/components/image.js.map +1 -0
  54. package/dist/pi-tui/components/input.d.ts +37 -0
  55. package/dist/pi-tui/components/input.d.ts.map +1 -0
  56. package/dist/pi-tui/components/input.js +382 -0
  57. package/dist/pi-tui/components/input.js.map +1 -0
  58. package/dist/pi-tui/components/loader.d.ts +31 -0
  59. package/dist/pi-tui/components/loader.d.ts.map +1 -0
  60. package/dist/pi-tui/components/loader.js +70 -0
  61. package/dist/pi-tui/components/loader.js.map +1 -0
  62. package/dist/pi-tui/components/markdown.d.ts +96 -0
  63. package/dist/pi-tui/components/markdown.d.ts.map +1 -0
  64. package/dist/pi-tui/components/markdown.js +647 -0
  65. package/dist/pi-tui/components/markdown.js.map +1 -0
  66. package/dist/pi-tui/components/select-list.d.ts +50 -0
  67. package/dist/pi-tui/components/select-list.d.ts.map +1 -0
  68. package/dist/pi-tui/components/select-list.js +167 -0
  69. package/dist/pi-tui/components/select-list.js.map +1 -0
  70. package/dist/pi-tui/components/settings-list.d.ts +50 -0
  71. package/dist/pi-tui/components/settings-list.d.ts.map +1 -0
  72. package/dist/pi-tui/components/settings-list.js +189 -0
  73. package/dist/pi-tui/components/settings-list.js.map +1 -0
  74. package/dist/pi-tui/components/spacer.d.ts +12 -0
  75. package/dist/pi-tui/components/spacer.d.ts.map +1 -0
  76. package/dist/pi-tui/components/spacer.js +23 -0
  77. package/dist/pi-tui/components/spacer.js.map +1 -0
  78. package/dist/pi-tui/components/text.d.ts +19 -0
  79. package/dist/pi-tui/components/text.d.ts.map +1 -0
  80. package/dist/pi-tui/components/text.js +91 -0
  81. package/dist/pi-tui/components/text.js.map +1 -0
  82. package/dist/pi-tui/components/truncated-text.d.ts +13 -0
  83. package/dist/pi-tui/components/truncated-text.d.ts.map +1 -0
  84. package/dist/pi-tui/components/truncated-text.js +51 -0
  85. package/dist/pi-tui/components/truncated-text.js.map +1 -0
  86. package/dist/pi-tui/editor-component.d.ts +39 -0
  87. package/dist/pi-tui/editor-component.d.ts.map +1 -0
  88. package/dist/pi-tui/editor-component.js +2 -0
  89. package/dist/pi-tui/editor-component.js.map +1 -0
  90. package/dist/pi-tui/fuzzy.d.ts +16 -0
  91. package/dist/pi-tui/fuzzy.d.ts.map +1 -0
  92. package/dist/pi-tui/fuzzy.js +110 -0
  93. package/dist/pi-tui/fuzzy.js.map +1 -0
  94. package/dist/pi-tui/index.d.ts +23 -0
  95. package/dist/pi-tui/index.d.ts.map +1 -0
  96. package/dist/pi-tui/index.js +32 -0
  97. package/dist/pi-tui/index.js.map +1 -0
  98. package/dist/pi-tui/keybindings.d.ts +193 -0
  99. package/dist/pi-tui/keybindings.d.ts.map +1 -0
  100. package/dist/pi-tui/keybindings.js +174 -0
  101. package/dist/pi-tui/keybindings.js.map +1 -0
  102. package/dist/pi-tui/keys.d.ts +184 -0
  103. package/dist/pi-tui/keys.d.ts.map +1 -0
  104. package/dist/pi-tui/keys.js +1182 -0
  105. package/dist/pi-tui/keys.js.map +1 -0
  106. package/dist/pi-tui/kill-ring.d.ts +28 -0
  107. package/dist/pi-tui/kill-ring.d.ts.map +1 -0
  108. package/dist/pi-tui/kill-ring.js +44 -0
  109. package/dist/pi-tui/kill-ring.js.map +1 -0
  110. package/dist/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  111. package/dist/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  112. package/dist/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  113. package/dist/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  114. package/dist/pi-tui/native-modifiers.d.ts +3 -0
  115. package/dist/pi-tui/native-modifiers.d.ts.map +1 -0
  116. package/dist/pi-tui/native-modifiers.js +53 -0
  117. package/dist/pi-tui/native-modifiers.js.map +1 -0
  118. package/dist/pi-tui/stdin-buffer.d.ts +50 -0
  119. package/dist/pi-tui/stdin-buffer.d.ts.map +1 -0
  120. package/dist/pi-tui/stdin-buffer.js +361 -0
  121. package/dist/pi-tui/stdin-buffer.js.map +1 -0
  122. package/dist/pi-tui/terminal-image.d.ts +90 -0
  123. package/dist/pi-tui/terminal-image.d.ts.map +1 -0
  124. package/dist/pi-tui/terminal-image.js +343 -0
  125. package/dist/pi-tui/terminal-image.js.map +1 -0
  126. package/dist/pi-tui/terminal.d.ts +113 -0
  127. package/dist/pi-tui/terminal.d.ts.map +1 -0
  128. package/dist/pi-tui/terminal.js +478 -0
  129. package/dist/pi-tui/terminal.js.map +1 -0
  130. package/dist/pi-tui/tui.d.ts +227 -0
  131. package/dist/pi-tui/tui.d.ts.map +1 -0
  132. package/dist/pi-tui/tui.js +1091 -0
  133. package/dist/pi-tui/tui.js.map +1 -0
  134. package/dist/pi-tui/undo-stack.d.ts +17 -0
  135. package/dist/pi-tui/undo-stack.d.ts.map +1 -0
  136. package/dist/pi-tui/undo-stack.js +25 -0
  137. package/dist/pi-tui/undo-stack.js.map +1 -0
  138. package/dist/pi-tui/utils.d.ts +84 -0
  139. package/dist/pi-tui/utils.d.ts.map +1 -0
  140. package/dist/pi-tui/utils.js +1024 -0
  141. package/dist/pi-tui/utils.js.map +1 -0
  142. package/dist/pi-tui/word-navigation.d.ts +25 -0
  143. package/dist/pi-tui/word-navigation.d.ts.map +1 -0
  144. package/dist/pi-tui/word-navigation.js +98 -0
  145. package/dist/pi-tui/word-navigation.js.map +1 -0
  146. package/dist/tools/__tests__/edit.test.js +85 -0
  147. package/dist/tools/__tests__/edit.test.js.map +1 -1
  148. package/dist/tools/edit.d.ts.map +1 -1
  149. package/dist/tools/edit.js +23 -1
  150. package/dist/tools/edit.js.map +1 -1
  151. package/dist/tools/registry.d.ts +4 -0
  152. package/dist/tools/registry.d.ts.map +1 -1
  153. package/dist/tools/registry.js +15 -7
  154. package/dist/tools/registry.js.map +1 -1
  155. package/dist/tui/components.d.ts +1 -1
  156. package/dist/tui/components.d.ts.map +1 -1
  157. package/dist/tui/components.js +1 -1
  158. package/dist/tui/components.js.map +1 -1
  159. package/dist/tui/editor-patch.d.ts +13 -0
  160. package/dist/tui/editor-patch.d.ts.map +1 -0
  161. package/dist/tui/editor-patch.js +41 -0
  162. package/dist/tui/editor-patch.js.map +1 -0
  163. package/dist/tui/index.d.ts.map +1 -1
  164. package/dist/tui/index.js +15 -3
  165. package/dist/tui/index.js.map +1 -1
  166. package/dist/web/index.d.ts.map +1 -1
  167. package/dist/web/index.js +11 -2
  168. package/dist/web/index.js.map +1 -1
  169. package/dist/web/public/app.js +65 -4
  170. package/dist/web/public/style.css +49 -0
  171. package/dist/web/session-bridge.d.ts +0 -4
  172. package/dist/web/session-bridge.d.ts.map +1 -1
  173. package/dist/web/session-bridge.js +16 -2
  174. package/dist/web/session-bridge.js.map +1 -1
  175. package/package.json +5 -3
  176. package/src/agent/__tests__/compaction.test.ts +6 -4
  177. package/src/agent/compaction.ts +3 -2
  178. package/src/agent/llm.ts +5 -1
  179. package/src/agent/prompt-builder.ts +27 -8
  180. package/src/agent/providers/anthropic.ts +15 -8
  181. package/src/agent/providers/google.ts +6 -1
  182. package/src/agent/providers/ollama.ts +6 -1
  183. package/src/agent/providers/openai.ts +21 -12
  184. package/src/agent/session.ts +26 -3
  185. package/src/pi-tui/autocomplete.ts +808 -0
  186. package/src/pi-tui/components/box.ts +137 -0
  187. package/src/pi-tui/components/cancellable-loader.ts +40 -0
  188. package/src/pi-tui/components/editor.ts +2321 -0
  189. package/src/pi-tui/components/image.ts +130 -0
  190. package/src/pi-tui/components/input.ts +456 -0
  191. package/src/pi-tui/components/loader.ts +93 -0
  192. package/src/pi-tui/components/markdown.ts +839 -0
  193. package/src/pi-tui/components/select-list.ts +263 -0
  194. package/src/pi-tui/components/settings-list.ts +264 -0
  195. package/src/pi-tui/components/spacer.ts +28 -0
  196. package/src/pi-tui/components/text.ts +113 -0
  197. package/src/pi-tui/components/truncated-text.ts +65 -0
  198. package/src/pi-tui/editor-component.ts +74 -0
  199. package/src/pi-tui/fuzzy.ts +137 -0
  200. package/src/pi-tui/index.ts +115 -0
  201. package/src/pi-tui/keybindings.ts +245 -0
  202. package/src/pi-tui/keys.ts +1440 -0
  203. package/src/pi-tui/kill-ring.ts +46 -0
  204. package/src/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  205. package/src/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  206. package/src/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  207. package/src/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  208. package/src/pi-tui/native-modifiers.ts +65 -0
  209. package/src/pi-tui/stdin-buffer.ts +434 -0
  210. package/src/pi-tui/terminal-image.ts +469 -0
  211. package/src/pi-tui/terminal.ts +594 -0
  212. package/src/pi-tui/tui.ts +1376 -0
  213. package/src/pi-tui/undo-stack.ts +28 -0
  214. package/src/pi-tui/utils.ts +1176 -0
  215. package/src/pi-tui/word-navigation.ts +127 -0
  216. package/src/tools/__tests__/edit.test.ts +91 -0
  217. package/src/tools/edit.ts +27 -1
  218. package/src/tools/registry.ts +18 -8
  219. package/src/tui/components.ts +2 -2
  220. package/src/tui/index.ts +24 -10
  221. package/src/web/index.ts +11 -2
  222. package/src/web/public/app.js +65 -4
  223. package/src/web/public/style.css +49 -0
  224. package/src/web/session-bridge.ts +13 -2
  225. package/tsconfig.json +2 -2
@@ -0,0 +1,594 @@
1
+ import * as fs from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import * as path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { setKittyProtocolActive } from './keys.js'
6
+ import { isNativeModifierPressed } from './native-modifiers.js'
7
+ import { StdinBuffer } from './stdin-buffer.js'
8
+
9
+ const cjsRequire = createRequire(import.meta.url)
10
+
11
+ const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000
12
+ const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = '\x1b]9;4;3\x07'
13
+ const TERMINAL_PROGRESS_CLEAR_SEQUENCE = '\x1b]9;4;0;\x07'
14
+ const APPLE_TERMINAL_SHIFT_ENTER_SEQUENCE = '\x1b[13;2u'
15
+ const DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS = 7
16
+ const KITTY_KEYBOARD_PROTOCOL_FALLBACK_TIMEOUT_MS = 150
17
+ const KEYBOARD_PROTOCOL_RESPONSE_FRAGMENT_TIMEOUT_MS = 150
18
+ const KITTY_KEYBOARD_PROTOCOL_QUERY = `\x1b[>${DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS}u\x1b[?u\x1b[c`
19
+
20
+ export type KeyboardProtocolNegotiationSequence =
21
+ | { type: 'kitty-flags'; flags: number }
22
+ | { type: 'device-attributes' }
23
+
24
+ export function parseKeyboardProtocolNegotiationSequence(
25
+ sequence: string,
26
+ ): KeyboardProtocolNegotiationSequence | undefined {
27
+ const kittyFlags = sequence.match(/^\x1b\[\?(\d+)u$/)
28
+ if (kittyFlags) {
29
+ return { type: 'kitty-flags', flags: Number.parseInt(kittyFlags[1]!, 10) }
30
+ }
31
+ if (/^\x1b\[\?[\d;]*c$/.test(sequence)) {
32
+ return { type: 'device-attributes' }
33
+ }
34
+ return undefined
35
+ }
36
+
37
+ function isKeyboardProtocolNegotiationSequencePrefix(
38
+ sequence: string,
39
+ allowBareEscapePrefix: boolean,
40
+ ): boolean {
41
+ return (
42
+ (allowBareEscapePrefix && sequence === '\x1b') ||
43
+ sequence === '\x1b[' ||
44
+ /^\x1b\[\?[\d;]*$/.test(sequence)
45
+ )
46
+ }
47
+
48
+ export function isAppleTerminalSession(): boolean {
49
+ return process.platform === 'darwin' && process.env.TERM_PROGRAM === 'Apple_Terminal'
50
+ }
51
+
52
+ export function normalizeAppleTerminalInput(
53
+ data: string,
54
+ isAppleTerminal: boolean,
55
+ isShiftPressed: boolean,
56
+ ): string {
57
+ if (isAppleTerminal && data === '\r' && isShiftPressed) return APPLE_TERMINAL_SHIFT_ENTER_SEQUENCE
58
+ return data
59
+ }
60
+
61
+ /**
62
+ * Minimal terminal interface for TUI
63
+ */
64
+ export interface Terminal {
65
+ // Start the terminal with input and resize handlers
66
+ start(onInput: (data: string) => void, onResize: () => void): void
67
+
68
+ // Stop the terminal and restore state
69
+ stop(): void
70
+
71
+ /**
72
+ * Drain stdin before exiting to prevent Kitty key release events from
73
+ * leaking to the parent shell over slow SSH connections.
74
+ * @param maxMs - Maximum time to drain (default: 1000ms)
75
+ * @param idleMs - Exit early if no input arrives within this time (default: 50ms)
76
+ */
77
+ drainInput(maxMs?: number, idleMs?: number): Promise<void>
78
+
79
+ // Write output to terminal
80
+ write(data: string): void
81
+
82
+ // Get terminal dimensions
83
+ get columns(): number
84
+ get rows(): number
85
+
86
+ // Whether Kitty keyboard protocol is active
87
+ get kittyProtocolActive(): boolean
88
+
89
+ // Cursor positioning (relative to current position)
90
+ moveBy(lines: number): void // Move cursor up (negative) or down (positive) by N lines
91
+
92
+ // Cursor visibility
93
+ hideCursor(): void // Hide the cursor
94
+ showCursor(): void // Show the cursor
95
+
96
+ // Clear operations
97
+ clearLine(): void // Clear current line
98
+ clearFromCursor(): void // Clear from cursor to end of screen
99
+ clearScreen(): void // Clear entire screen and move cursor to (0,0)
100
+
101
+ // Title operations
102
+ setTitle(title: string): void // Set terminal window title
103
+
104
+ // Progress indicator (OSC 9;4)
105
+ setProgress(active: boolean): void
106
+ }
107
+
108
+ /**
109
+ * Real terminal using process.stdin/stdout
110
+ */
111
+ export class ProcessTerminal implements Terminal {
112
+ private wasRaw = false
113
+ private inputHandler?: (data: string) => void
114
+ private resizeHandler?: () => void
115
+ private _kittyProtocolActive = false
116
+ private _modifyOtherKeysActive = false
117
+ private keyboardProtocolPushed = false
118
+ private keyboardProtocolNegotiationPending = false
119
+ private keyboardProtocolLateResponsePending = false
120
+ private keyboardProtocolNegotiationBuffer = ''
121
+ private keyboardProtocolFallbackTimer?: ReturnType<typeof setTimeout>
122
+ private keyboardProtocolBufferFlushTimer?: ReturnType<typeof setTimeout>
123
+ private stdinBuffer?: StdinBuffer
124
+ private stdinDataHandler?: (data: string) => void
125
+ private progressInterval?: ReturnType<typeof setInterval>
126
+ private writeLogPath = (() => {
127
+ const env = process.env.PI_TUI_WRITE_LOG || ''
128
+ if (!env) return ''
129
+ try {
130
+ if (fs.statSync(env).isDirectory()) {
131
+ const now = new Date()
132
+ const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}`
133
+ return path.join(env, `tui-${ts}-${process.pid}.log`)
134
+ }
135
+ } catch {
136
+ // Not an existing directory - use as-is (file path)
137
+ }
138
+ return env
139
+ })()
140
+
141
+ get kittyProtocolActive(): boolean {
142
+ return this._kittyProtocolActive
143
+ }
144
+
145
+ start(onInput: (data: string) => void, onResize: () => void): void {
146
+ this.inputHandler = onInput
147
+ this.resizeHandler = onResize
148
+
149
+ // Save previous state and enable raw mode
150
+ this.wasRaw = process.stdin.isRaw || false
151
+ if (process.stdin.setRawMode) {
152
+ process.stdin.setRawMode(true)
153
+ }
154
+ process.stdin.setEncoding('utf8')
155
+ process.stdin.resume()
156
+
157
+ // Enable bracketed paste mode - terminal will wrap pastes in \x1b[200~ ... \x1b[201~
158
+ process.stdout.write('\x1b[?2004h')
159
+
160
+ // Set up resize handler immediately
161
+ process.stdout.on('resize', this.resizeHandler)
162
+
163
+ // Refresh terminal dimensions - they may be stale after suspend/resume
164
+ // (SIGWINCH is lost while process is stopped). Unix only.
165
+ if (process.platform !== 'win32') {
166
+ process.kill(process.pid, 'SIGWINCH')
167
+ }
168
+
169
+ // On Windows, enable ENABLE_VIRTUAL_TERMINAL_INPUT so the console sends
170
+ // VT escape sequences (e.g. \x1b[Z for Shift+Tab) instead of raw console
171
+ // events that lose modifier information. Must run AFTER setRawMode(true)
172
+ // since that resets console mode flags.
173
+ this.enableWindowsVTInput()
174
+
175
+ // Query and enable Kitty keyboard protocol
176
+ // The query handler intercepts input temporarily, then installs the user's handler
177
+ // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
178
+ this.queryAndEnableKittyProtocol()
179
+ }
180
+
181
+ /**
182
+ * Set up StdinBuffer to split batched input into individual sequences.
183
+ * This ensures components receive single events, making matchesKey/isKeyRelease work correctly.
184
+ *
185
+ * Also watches for Kitty protocol response and enables it when detected.
186
+ * This is done here (after stdinBuffer parsing) rather than on raw stdin
187
+ * to handle the case where the response arrives split across multiple events.
188
+ */
189
+ private setupStdinBuffer(): void {
190
+ this.stdinBuffer = new StdinBuffer({ timeout: 10 })
191
+
192
+ // Forward individual sequences to the input handler
193
+ this.stdinBuffer.on('data', sequence => {
194
+ if (this.keyboardProtocolNegotiationPending) {
195
+ const negotiationSequence = this.readKeyboardProtocolNegotiationSequence(sequence, true)
196
+ if (negotiationSequence === 'pending') {
197
+ return // Wait for the rest of a split negotiation response.
198
+ }
199
+ if (this.handleKeyboardProtocolNegotiationSequence(negotiationSequence)) {
200
+ return
201
+ }
202
+ }
203
+
204
+ if (this.keyboardProtocolLateResponsePending) {
205
+ const negotiationSequence = this.readKeyboardProtocolNegotiationSequence(sequence, false)
206
+ if (negotiationSequence === 'pending') {
207
+ this.scheduleKeyboardProtocolNegotiationBufferFlush()
208
+ return // Wait for the rest of a split late negotiation response.
209
+ }
210
+ if (this.handleKeyboardProtocolNegotiationSequence(negotiationSequence)) {
211
+ return
212
+ }
213
+ }
214
+
215
+ this.forwardInputSequence(sequence)
216
+ })
217
+
218
+ // Re-wrap paste content with bracketed paste markers for existing editor handling
219
+ this.stdinBuffer.on('paste', content => {
220
+ if (this.inputHandler) {
221
+ this.inputHandler(`\x1b[200~${content}\x1b[201~`)
222
+ }
223
+ })
224
+
225
+ // Handler that pipes stdin data through the buffer
226
+ this.stdinDataHandler = (data: string) => {
227
+ this.stdinBuffer!.process(data)
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Query terminal for Kitty keyboard protocol support and enable it if available.
233
+ *
234
+ * Kitty's progressive enhancement detection requires requesting the desired
235
+ * flags before querying them. The trailing DA query is a sentinel supported by
236
+ * terminals that do not know Kitty keyboard protocol. A short timeout remains
237
+ * as a backup for terminals, PTYs, and SSH sessions that delay, split, or drop
238
+ * the DA response.
239
+ *
240
+ * The requested flags are:
241
+ * - 1 = disambiguate escape codes
242
+ * - 2 = report event types (press/repeat/release)
243
+ * - 4 = report alternate keys (shifted key, base layout key)
244
+ */
245
+ private queryAndEnableKittyProtocol(): void {
246
+ this.setupStdinBuffer()
247
+ process.stdin.on('data', this.stdinDataHandler!)
248
+ this.keyboardProtocolPushed = true
249
+ this.keyboardProtocolNegotiationPending = true
250
+ this.keyboardProtocolLateResponsePending = false
251
+ this.clearKeyboardProtocolNegotiationBuffer()
252
+ process.stdout.write(KITTY_KEYBOARD_PROTOCOL_QUERY)
253
+ this.keyboardProtocolFallbackTimer = setTimeout(() => {
254
+ this.keyboardProtocolFallbackTimer = undefined
255
+ this.keyboardProtocolNegotiationPending = false
256
+ this.keyboardProtocolLateResponsePending = true
257
+ if (this.keyboardProtocolNegotiationBuffer === '\x1b') {
258
+ this.flushKeyboardProtocolNegotiationBufferAsInput()
259
+ } else {
260
+ this.scheduleKeyboardProtocolNegotiationBufferFlush()
261
+ }
262
+ this.enableModifyOtherKeys()
263
+ }, KITTY_KEYBOARD_PROTOCOL_FALLBACK_TIMEOUT_MS)
264
+ }
265
+
266
+ private handleKeyboardProtocolNegotiationSequence(
267
+ negotiationSequence: KeyboardProtocolNegotiationSequence | undefined,
268
+ ): boolean {
269
+ if (!negotiationSequence) return false
270
+ if (negotiationSequence.type === 'kitty-flags') {
271
+ if (negotiationSequence.flags !== 0 && !this._kittyProtocolActive) {
272
+ this._kittyProtocolActive = true
273
+ setKittyProtocolActive(true)
274
+ this.keyboardProtocolNegotiationPending = false
275
+ this.keyboardProtocolLateResponsePending = true
276
+ this.clearKeyboardProtocolNegotiationBuffer()
277
+ this.clearKeyboardProtocolFallbackTimer()
278
+ }
279
+ return true
280
+ }
281
+
282
+ this.keyboardProtocolNegotiationPending = false
283
+ this.keyboardProtocolLateResponsePending = true
284
+ this.clearKeyboardProtocolNegotiationBuffer()
285
+ this.clearKeyboardProtocolFallbackTimer()
286
+ this.enableModifyOtherKeys()
287
+ return true
288
+ }
289
+
290
+ private readKeyboardProtocolNegotiationSequence(
291
+ sequence: string,
292
+ allowBareEscapePrefix: boolean,
293
+ ): KeyboardProtocolNegotiationSequence | 'pending' | undefined {
294
+ if (this.keyboardProtocolNegotiationBuffer) {
295
+ const bufferedSequence = this.keyboardProtocolNegotiationBuffer + sequence
296
+ const negotiationSequence = parseKeyboardProtocolNegotiationSequence(bufferedSequence)
297
+ if (negotiationSequence) {
298
+ this.clearKeyboardProtocolNegotiationBuffer()
299
+ return negotiationSequence
300
+ }
301
+ if (isKeyboardProtocolNegotiationSequencePrefix(bufferedSequence, allowBareEscapePrefix)) {
302
+ this.setKeyboardProtocolNegotiationBuffer(bufferedSequence)
303
+ return 'pending'
304
+ }
305
+ this.flushKeyboardProtocolNegotiationBufferAsInput()
306
+ }
307
+
308
+ const negotiationSequence = parseKeyboardProtocolNegotiationSequence(sequence)
309
+ if (negotiationSequence) return negotiationSequence
310
+ if (isKeyboardProtocolNegotiationSequencePrefix(sequence, allowBareEscapePrefix)) {
311
+ this.setKeyboardProtocolNegotiationBuffer(sequence)
312
+ return 'pending'
313
+ }
314
+ return undefined
315
+ }
316
+
317
+ private setKeyboardProtocolNegotiationBuffer(sequence: string): void {
318
+ this.clearKeyboardProtocolNegotiationBufferFlushTimer()
319
+ this.keyboardProtocolNegotiationBuffer = sequence
320
+ }
321
+
322
+ private clearKeyboardProtocolNegotiationBuffer(): void {
323
+ this.clearKeyboardProtocolNegotiationBufferFlushTimer()
324
+ this.keyboardProtocolNegotiationBuffer = ''
325
+ }
326
+
327
+ private flushKeyboardProtocolNegotiationBufferAsInput(): void {
328
+ if (!this.keyboardProtocolNegotiationBuffer) return
329
+ const sequence = this.keyboardProtocolNegotiationBuffer
330
+ this.clearKeyboardProtocolNegotiationBuffer()
331
+ this.forwardInputSequence(sequence)
332
+ }
333
+
334
+ private scheduleKeyboardProtocolNegotiationBufferFlush(): void {
335
+ if (!this.keyboardProtocolNegotiationBuffer || this.keyboardProtocolBufferFlushTimer) return
336
+ this.keyboardProtocolBufferFlushTimer = setTimeout(() => {
337
+ this.keyboardProtocolBufferFlushTimer = undefined
338
+ this.flushKeyboardProtocolNegotiationBufferAsInput()
339
+ }, KEYBOARD_PROTOCOL_RESPONSE_FRAGMENT_TIMEOUT_MS)
340
+ }
341
+
342
+ private clearKeyboardProtocolNegotiationBufferFlushTimer(): void {
343
+ if (!this.keyboardProtocolBufferFlushTimer) return
344
+ clearTimeout(this.keyboardProtocolBufferFlushTimer)
345
+ this.keyboardProtocolBufferFlushTimer = undefined
346
+ }
347
+
348
+ private forwardInputSequence(sequence: string): void {
349
+ if (!this.inputHandler) return
350
+ const isAppleTerminal = sequence === '\r' && isAppleTerminalSession()
351
+ const input = normalizeAppleTerminalInput(
352
+ sequence,
353
+ isAppleTerminal,
354
+ isAppleTerminal && isNativeModifierPressed('shift'),
355
+ )
356
+ this.inputHandler(input)
357
+ }
358
+
359
+ private enableModifyOtherKeys(): void {
360
+ if (this._kittyProtocolActive || this._modifyOtherKeysActive) return
361
+ process.stdout.write('\x1b[>4;2m')
362
+ this._modifyOtherKeysActive = true
363
+ }
364
+
365
+ private clearKeyboardProtocolFallbackTimer(): void {
366
+ if (!this.keyboardProtocolFallbackTimer) return
367
+ clearTimeout(this.keyboardProtocolFallbackTimer)
368
+ this.keyboardProtocolFallbackTimer = undefined
369
+ }
370
+
371
+ /**
372
+ * On Windows, add ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200) to the stdin
373
+ * console handle so the terminal sends VT sequences for modified keys
374
+ * (e.g. \x1b[Z for Shift+Tab). Without this, libuv's ReadConsoleInputW
375
+ * discards modifier state and Shift+Tab arrives as plain \t.
376
+ */
377
+ private enableWindowsVTInput(): void {
378
+ if (process.platform !== 'win32') return
379
+ try {
380
+ const arch = process.arch
381
+ if (arch !== 'x64' && arch !== 'arm64') return
382
+
383
+ // Dynamic require so non-Windows and bundled/browser paths never load the
384
+ // native helper. In the npm package native/ is next to dist/; in compiled
385
+ // binary archives native/ is copied next to the executable.
386
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url))
387
+ const nativePath = path.join(
388
+ 'native',
389
+ 'win32',
390
+ 'prebuilds',
391
+ `win32-${arch}`,
392
+ 'win32-console-mode.node',
393
+ )
394
+ const candidates = [
395
+ path.join(moduleDir, '..', nativePath),
396
+ path.join(moduleDir, nativePath),
397
+ path.join(path.dirname(process.execPath), nativePath),
398
+ ]
399
+ for (const modulePath of candidates) {
400
+ try {
401
+ const helper = cjsRequire(modulePath) as { enableVirtualTerminalInput?: () => boolean }
402
+ helper.enableVirtualTerminalInput?.()
403
+ return
404
+ } catch {
405
+ // Try the next possible packaging location.
406
+ }
407
+ }
408
+ } catch {
409
+ // Native helper not available — Shift+Tab won't be distinguishable from Tab.
410
+ }
411
+ }
412
+
413
+ async drainInput(maxMs = 1000, idleMs = 50): Promise<void> {
414
+ const shouldDisableKittyProtocol =
415
+ this.keyboardProtocolPushed ||
416
+ this._kittyProtocolActive ||
417
+ this.keyboardProtocolNegotiationPending
418
+ this.keyboardProtocolLateResponsePending = false
419
+ this.clearKeyboardProtocolNegotiationBuffer()
420
+ this.clearKeyboardProtocolFallbackTimer()
421
+ if (shouldDisableKittyProtocol) {
422
+ // Disable Kitty keyboard protocol first so any late key releases
423
+ // do not generate new Kitty escape sequences.
424
+ process.stdout.write('\x1b[<u')
425
+ this.keyboardProtocolPushed = false
426
+ this._kittyProtocolActive = false
427
+ setKittyProtocolActive(false)
428
+ }
429
+ this.keyboardProtocolNegotiationPending = false
430
+ if (this._modifyOtherKeysActive) {
431
+ process.stdout.write('\x1b[>4;0m')
432
+ this._modifyOtherKeysActive = false
433
+ }
434
+
435
+ const previousHandler = this.inputHandler
436
+ this.inputHandler = undefined
437
+
438
+ let lastDataTime = Date.now()
439
+ const onData = () => {
440
+ lastDataTime = Date.now()
441
+ }
442
+
443
+ process.stdin.on('data', onData)
444
+ const endTime = Date.now() + maxMs
445
+
446
+ try {
447
+ while (true) {
448
+ const now = Date.now()
449
+ const timeLeft = endTime - now
450
+ if (timeLeft <= 0) break
451
+ if (now - lastDataTime >= idleMs) break
452
+ await new Promise(resolve => setTimeout(resolve, Math.min(idleMs, timeLeft)))
453
+ }
454
+ } finally {
455
+ process.stdin.removeListener('data', onData)
456
+ this.inputHandler = previousHandler
457
+ }
458
+ }
459
+
460
+ stop(): void {
461
+ if (this.clearProgressInterval()) {
462
+ process.stdout.write(TERMINAL_PROGRESS_CLEAR_SEQUENCE)
463
+ }
464
+
465
+ // Disable bracketed paste mode
466
+ process.stdout.write('\x1b[?2004l')
467
+
468
+ const shouldDisableKittyProtocol =
469
+ this.keyboardProtocolPushed ||
470
+ this._kittyProtocolActive ||
471
+ this.keyboardProtocolNegotiationPending
472
+ this.keyboardProtocolLateResponsePending = false
473
+ this.clearKeyboardProtocolNegotiationBuffer()
474
+ this.clearKeyboardProtocolFallbackTimer()
475
+
476
+ // Disable Kitty keyboard protocol if not already done by drainInput()
477
+ if (shouldDisableKittyProtocol) {
478
+ process.stdout.write('\x1b[<u')
479
+ this.keyboardProtocolPushed = false
480
+ this._kittyProtocolActive = false
481
+ setKittyProtocolActive(false)
482
+ }
483
+ this.keyboardProtocolNegotiationPending = false
484
+ if (this._modifyOtherKeysActive) {
485
+ process.stdout.write('\x1b[>4;0m')
486
+ this._modifyOtherKeysActive = false
487
+ }
488
+
489
+ // Clean up StdinBuffer
490
+ if (this.stdinBuffer) {
491
+ this.stdinBuffer.destroy()
492
+ this.stdinBuffer = undefined
493
+ }
494
+
495
+ // Remove event handlers
496
+ if (this.stdinDataHandler) {
497
+ process.stdin.removeListener('data', this.stdinDataHandler)
498
+ this.stdinDataHandler = undefined
499
+ }
500
+ this.inputHandler = undefined
501
+ if (this.resizeHandler) {
502
+ process.stdout.removeListener('resize', this.resizeHandler)
503
+ this.resizeHandler = undefined
504
+ }
505
+
506
+ // Pause stdin to prevent any buffered input (e.g., Ctrl+D) from being
507
+ // re-interpreted after raw mode is disabled. This fixes a race condition
508
+ // where Ctrl+D could close the parent shell over SSH.
509
+ process.stdin.pause()
510
+
511
+ // Restore raw mode state
512
+ if (process.stdin.setRawMode) {
513
+ process.stdin.setRawMode(this.wasRaw)
514
+ }
515
+ }
516
+
517
+ write(data: string): void {
518
+ process.stdout.write(data)
519
+ if (this.writeLogPath) {
520
+ try {
521
+ fs.appendFileSync(this.writeLogPath, data, { encoding: 'utf8' })
522
+ } catch {
523
+ // Ignore logging errors
524
+ }
525
+ }
526
+ }
527
+
528
+ get columns(): number {
529
+ return process.stdout.columns || Number(process.env.COLUMNS) || 80
530
+ }
531
+
532
+ get rows(): number {
533
+ return process.stdout.rows || Number(process.env.LINES) || 24
534
+ }
535
+
536
+ moveBy(lines: number): void {
537
+ if (lines > 0) {
538
+ // Move down
539
+ process.stdout.write(`\x1b[${lines}B`)
540
+ } else if (lines < 0) {
541
+ // Move up
542
+ process.stdout.write(`\x1b[${-lines}A`)
543
+ }
544
+ // lines === 0: no movement
545
+ }
546
+
547
+ hideCursor(): void {
548
+ process.stdout.write('\x1b[?25l')
549
+ }
550
+
551
+ showCursor(): void {
552
+ process.stdout.write('\x1b[?25h')
553
+ }
554
+
555
+ clearLine(): void {
556
+ process.stdout.write('\x1b[K')
557
+ }
558
+
559
+ clearFromCursor(): void {
560
+ process.stdout.write('\x1b[J')
561
+ }
562
+
563
+ clearScreen(): void {
564
+ process.stdout.write('\x1b[2J\x1b[H') // Clear screen and move to home (1,1)
565
+ }
566
+
567
+ setTitle(title: string): void {
568
+ // OSC 0;title BEL - set terminal window title
569
+ process.stdout.write(`\x1b]0;${title}\x07`)
570
+ }
571
+
572
+ setProgress(active: boolean): void {
573
+ if (active) {
574
+ // OSC 9;4;3 - indeterminate progress
575
+ process.stdout.write(TERMINAL_PROGRESS_ACTIVE_SEQUENCE)
576
+ if (!this.progressInterval) {
577
+ this.progressInterval = setInterval(() => {
578
+ process.stdout.write(TERMINAL_PROGRESS_ACTIVE_SEQUENCE)
579
+ }, TERMINAL_PROGRESS_KEEPALIVE_MS)
580
+ }
581
+ } else {
582
+ this.clearProgressInterval()
583
+ // OSC 9;4;0 - clear progress
584
+ process.stdout.write(TERMINAL_PROGRESS_CLEAR_SEQUENCE)
585
+ }
586
+ }
587
+
588
+ private clearProgressInterval(): boolean {
589
+ if (!this.progressInterval) return false
590
+ clearInterval(this.progressInterval)
591
+ this.progressInterval = undefined
592
+ return true
593
+ }
594
+ }