@psnext/slingcli 2.4.20260601-1 → 2.5.20260602-2

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 (96) hide show
  1. package/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler/package.json +4 -4
  2. package/node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js +12 -1
  3. package/node_modules/@aws-sdk/credential-provider-node/dist-es/runtime/memoize-chain.js +12 -1
  4. package/node_modules/@aws-sdk/credential-provider-node/package.json +1 -1
  5. package/node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler/package.json +4 -4
  6. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +81 -18
  7. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +1 -0
  8. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +1 -0
  9. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +14 -1
  10. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  11. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +197 -190
  12. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +84 -21
  13. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +24 -14
  14. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js +5 -9
  15. package/node_modules/@earendil-works/pi-ai/dist/providers/google-vertex.js +1 -1
  16. package/node_modules/@earendil-works/pi-ai/dist/providers/google.js +5 -3
  17. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.js +2 -3
  18. package/node_modules/@earendil-works/pi-ai/dist/providers/mistral.js +2 -3
  19. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.js +130 -62
  20. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js +18 -12
  21. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses-shared.js +4 -1
  22. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js +5 -9
  23. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js +1 -0
  24. package/node_modules/@earendil-works/pi-ai/dist/stream.js +14 -2
  25. package/node_modules/@earendil-works/pi-ai/dist/utils/abort-signals.js +34 -0
  26. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js +8 -7
  27. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js +1 -1
  28. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/index.js +1 -1
  29. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js +179 -79
  30. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  31. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js +27 -0
  32. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +9 -1
  33. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js +1 -0
  34. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +29 -5
  35. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js +21 -3
  36. package/node_modules/@earendil-works/pi-coding-agent/dist/core/footer-data-provider.js +29 -1
  37. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +65 -13
  38. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -1
  39. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resolve-config-value.js +134 -11
  40. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +12 -6
  41. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +167 -96
  42. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +14 -9
  43. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +0 -3
  44. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.js +7 -10
  45. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.js +5 -7
  46. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +6 -7
  47. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/render-utils.js +17 -1
  48. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.js +5 -6
  49. package/node_modules/@earendil-works/pi-coding-agent/dist/index.js +2 -0
  50. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +69 -16
  51. package/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js +118 -1
  52. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js +1 -1
  53. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +2 -4
  54. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +61 -6
  55. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  56. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/print-mode.js +1 -0
  57. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +4 -1
  58. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/deprecation.js +13 -0
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/json.js +7 -0
  60. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +71 -56
  61. package/node_modules/@earendil-works/pi-coding-agent/package.json +7 -7
  62. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +9 -53
  63. package/node_modules/@earendil-works/pi-tui/dist/components/input.js +6 -54
  64. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +34 -7
  65. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +172 -37
  66. package/node_modules/@earendil-works/pi-tui/dist/tui.js +166 -22
  67. package/node_modules/@earendil-works/pi-tui/dist/utils.js +8 -3
  68. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js +96 -0
  69. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  70. package/node_modules/@mariozechner/clipboard/Cargo.toml +3 -3
  71. package/node_modules/@mariozechner/clipboard/index.d.ts +34 -20
  72. package/node_modules/@mariozechner/clipboard/index.js +546 -257
  73. package/node_modules/@mariozechner/clipboard/package.json +14 -14
  74. package/node_modules/@mariozechner/clipboard/src/lib.rs +4 -9
  75. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/clipboard.linux-x64-gnu.node +0 -0
  76. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/package.json +2 -2
  77. package/node_modules/@smithy/core/dist-cjs/submodules/retry/index.browser.js +39 -22
  78. package/node_modules/@smithy/core/dist-cjs/submodules/retry/index.js +39 -22
  79. package/node_modules/@smithy/core/dist-es/submodules/retry/middleware-retry/configurations.js +19 -6
  80. package/node_modules/@smithy/core/dist-es/submodules/retry/middleware-retry/retryMiddleware.js +4 -5
  81. package/node_modules/@smithy/core/dist-es/submodules/retry/service-error-classification/constants.js +1 -1
  82. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/ConfiguredRetryStrategy.js +4 -5
  83. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/DefaultRetryToken.js +3 -0
  84. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/StandardRetryStrategy.js +9 -5
  85. package/node_modules/@smithy/core/dist-types/submodules/retry/middleware-retry/configurations.d.ts +4 -1
  86. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/ConfiguredRetryStrategy.d.ts +1 -2
  87. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/DefaultRetryToken.d.ts +3 -0
  88. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/StandardRetryStrategy.d.ts +1 -1
  89. package/node_modules/@smithy/core/package.json +2 -2
  90. package/node_modules/@smithy/credential-provider-imds/package.json +3 -3
  91. package/node_modules/@smithy/fetch-http-handler/package.json +4 -4
  92. package/node_modules/@smithy/signature-v4/package.json +3 -3
  93. package/node_modules/@smithy/types/dist-types/retry.d.ts +25 -0
  94. package/node_modules/@smithy/types/package.json +1 -1
  95. package/package.json +6 -6
  96. package/slingshot/index.js +231 -230
@@ -10,6 +10,23 @@ const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
10
10
  const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
11
11
  const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
12
12
  const APPLE_TERMINAL_SHIFT_ENTER_SEQUENCE = "\x1b[13;2u";
13
+ const DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS = 7;
14
+ const KITTY_KEYBOARD_PROTOCOL_FALLBACK_TIMEOUT_MS = 150;
15
+ const KEYBOARD_PROTOCOL_RESPONSE_FRAGMENT_TIMEOUT_MS = 150;
16
+ const KITTY_KEYBOARD_PROTOCOL_QUERY = `\x1b[>${DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS}u\x1b[?u\x1b[c`;
17
+ export function parseKeyboardProtocolNegotiationSequence(sequence) {
18
+ const kittyFlags = sequence.match(/^\x1b\[\?(\d+)u$/);
19
+ if (kittyFlags) {
20
+ return { type: "kitty-flags", flags: Number.parseInt(kittyFlags[1], 10) };
21
+ }
22
+ if (/^\x1b\[\?[\d;]*c$/.test(sequence)) {
23
+ return { type: "device-attributes" };
24
+ }
25
+ return undefined;
26
+ }
27
+ function isKeyboardProtocolNegotiationSequencePrefix(sequence, allowBareEscapePrefix) {
28
+ return (allowBareEscapePrefix && sequence === "\x1b") || sequence === "\x1b[" || /^\x1b\[\?[\d;]*$/.test(sequence);
29
+ }
13
30
  export function isAppleTerminalSession() {
14
31
  return process.platform === "darwin" && process.env.TERM_PROGRAM === "Apple_Terminal";
15
32
  }
@@ -27,6 +44,12 @@ export class ProcessTerminal {
27
44
  resizeHandler;
28
45
  _kittyProtocolActive = false;
29
46
  _modifyOtherKeysActive = false;
47
+ keyboardProtocolPushed = false;
48
+ keyboardProtocolNegotiationPending = false;
49
+ keyboardProtocolLateResponsePending = false;
50
+ keyboardProtocolNegotiationBuffer = "";
51
+ keyboardProtocolFallbackTimer;
52
+ keyboardProtocolBufferFlushTimer;
30
53
  stdinBuffer;
31
54
  stdinDataHandler;
32
55
  progressInterval;
@@ -88,30 +111,28 @@ export class ProcessTerminal {
88
111
  */
89
112
  setupStdinBuffer() {
90
113
  this.stdinBuffer = new StdinBuffer({ timeout: 10 });
91
- // Kitty protocol response pattern: \x1b[?<flags>u
92
- const kittyResponsePattern = /^\x1b\[\?(\d+)u$/;
93
114
  // Forward individual sequences to the input handler
94
115
  this.stdinBuffer.on("data", (sequence) => {
95
- // Check for Kitty protocol response (only if not already enabled)
96
- if (!this._kittyProtocolActive) {
97
- const match = sequence.match(kittyResponsePattern);
98
- if (match) {
99
- this._kittyProtocolActive = true;
100
- setKittyProtocolActive(true);
101
- // Enable Kitty keyboard protocol (push flags)
102
- // Flag 1 = disambiguate escape codes
103
- // Flag 2 = report event types (press/repeat/release)
104
- // Flag 4 = report alternate keys (shifted key, base layout key)
105
- // Base layout key enables shortcuts to work with non-Latin keyboard layouts
106
- process.stdout.write("\x1b[>7u");
107
- return; // Don't forward protocol response to TUI
116
+ if (this.keyboardProtocolNegotiationPending) {
117
+ const negotiationSequence = this.readKeyboardProtocolNegotiationSequence(sequence, true);
118
+ if (negotiationSequence === "pending") {
119
+ return; // Wait for the rest of a split negotiation response.
120
+ }
121
+ if (this.handleKeyboardProtocolNegotiationSequence(negotiationSequence)) {
122
+ return;
108
123
  }
109
124
  }
110
- if (this.inputHandler) {
111
- const isAppleTerminal = sequence === "\r" && isAppleTerminalSession();
112
- const input = normalizeAppleTerminalInput(sequence, isAppleTerminal, isAppleTerminal && isNativeModifierPressed("shift"));
113
- this.inputHandler(input);
125
+ if (this.keyboardProtocolLateResponsePending) {
126
+ const negotiationSequence = this.readKeyboardProtocolNegotiationSequence(sequence, false);
127
+ if (negotiationSequence === "pending") {
128
+ this.scheduleKeyboardProtocolNegotiationBufferFlush();
129
+ return; // Wait for the rest of a split late negotiation response.
130
+ }
131
+ if (this.handleKeyboardProtocolNegotiationSequence(negotiationSequence)) {
132
+ return;
133
+ }
114
134
  }
135
+ this.forwardInputSequence(sequence);
115
136
  });
116
137
  // Re-wrap paste content with bracketed paste markers for existing editor handling
117
138
  this.stdinBuffer.on("paste", (content) => {
@@ -125,29 +146,131 @@ export class ProcessTerminal {
125
146
  };
126
147
  }
127
148
  /**
128
- * Query terminal for Kitty keyboard protocol support and enable if available.
129
- *
130
- * Sends CSI ? u to query current flags. If terminal responds with CSI ? <flags> u,
131
- * it supports the protocol and we enable it with CSI > 1 u.
149
+ * Query terminal for Kitty keyboard protocol support and enable it if available.
132
150
  *
133
- * If no Kitty response arrives shortly after startup, fall back to enabling
134
- * xterm modifyOtherKeys mode 2. This is needed for tmux, which can forward
135
- * modified enter keys as CSI-u when extended-keys is enabled, but may not
136
- * answer the Kitty protocol query.
151
+ * Kitty's progressive enhancement detection requires requesting the desired
152
+ * flags before querying them. The trailing DA query is a sentinel supported by
153
+ * terminals that do not know Kitty keyboard protocol. A short timeout remains
154
+ * as a backup for terminals, PTYs, and SSH sessions that delay, split, or drop
155
+ * the DA response.
137
156
  *
138
- * The response is detected in setupStdinBuffer's data handler, which properly
139
- * handles the case where the response arrives split across multiple stdin events.
157
+ * The requested flags are:
158
+ * - 1 = disambiguate escape codes
159
+ * - 2 = report event types (press/repeat/release)
160
+ * - 4 = report alternate keys (shifted key, base layout key)
140
161
  */
141
162
  queryAndEnableKittyProtocol() {
142
163
  this.setupStdinBuffer();
143
164
  process.stdin.on("data", this.stdinDataHandler);
144
- process.stdout.write("\x1b[?u");
145
- setTimeout(() => {
146
- if (!this._kittyProtocolActive && !this._modifyOtherKeysActive) {
147
- process.stdout.write("\x1b[>4;2m");
148
- this._modifyOtherKeysActive = true;
165
+ this.keyboardProtocolPushed = true;
166
+ this.keyboardProtocolNegotiationPending = true;
167
+ this.keyboardProtocolLateResponsePending = false;
168
+ this.clearKeyboardProtocolNegotiationBuffer();
169
+ process.stdout.write(KITTY_KEYBOARD_PROTOCOL_QUERY);
170
+ this.keyboardProtocolFallbackTimer = setTimeout(() => {
171
+ this.keyboardProtocolFallbackTimer = undefined;
172
+ this.keyboardProtocolNegotiationPending = false;
173
+ this.keyboardProtocolLateResponsePending = true;
174
+ if (this.keyboardProtocolNegotiationBuffer === "\x1b") {
175
+ this.flushKeyboardProtocolNegotiationBufferAsInput();
176
+ }
177
+ else {
178
+ this.scheduleKeyboardProtocolNegotiationBufferFlush();
179
+ }
180
+ this.enableModifyOtherKeys();
181
+ }, KITTY_KEYBOARD_PROTOCOL_FALLBACK_TIMEOUT_MS);
182
+ }
183
+ handleKeyboardProtocolNegotiationSequence(negotiationSequence) {
184
+ if (!negotiationSequence)
185
+ return false;
186
+ if (negotiationSequence.type === "kitty-flags") {
187
+ if (negotiationSequence.flags !== 0 && !this._kittyProtocolActive) {
188
+ this._kittyProtocolActive = true;
189
+ setKittyProtocolActive(true);
190
+ this.keyboardProtocolNegotiationPending = false;
191
+ this.keyboardProtocolLateResponsePending = true;
192
+ this.clearKeyboardProtocolNegotiationBuffer();
193
+ this.clearKeyboardProtocolFallbackTimer();
194
+ }
195
+ return true;
196
+ }
197
+ this.keyboardProtocolNegotiationPending = false;
198
+ this.keyboardProtocolLateResponsePending = true;
199
+ this.clearKeyboardProtocolNegotiationBuffer();
200
+ this.clearKeyboardProtocolFallbackTimer();
201
+ this.enableModifyOtherKeys();
202
+ return true;
203
+ }
204
+ readKeyboardProtocolNegotiationSequence(sequence, allowBareEscapePrefix) {
205
+ if (this.keyboardProtocolNegotiationBuffer) {
206
+ const bufferedSequence = this.keyboardProtocolNegotiationBuffer + sequence;
207
+ const negotiationSequence = parseKeyboardProtocolNegotiationSequence(bufferedSequence);
208
+ if (negotiationSequence) {
209
+ this.clearKeyboardProtocolNegotiationBuffer();
210
+ return negotiationSequence;
211
+ }
212
+ if (isKeyboardProtocolNegotiationSequencePrefix(bufferedSequence, allowBareEscapePrefix)) {
213
+ this.setKeyboardProtocolNegotiationBuffer(bufferedSequence);
214
+ return "pending";
149
215
  }
150
- }, 150);
216
+ this.flushKeyboardProtocolNegotiationBufferAsInput();
217
+ }
218
+ const negotiationSequence = parseKeyboardProtocolNegotiationSequence(sequence);
219
+ if (negotiationSequence)
220
+ return negotiationSequence;
221
+ if (isKeyboardProtocolNegotiationSequencePrefix(sequence, allowBareEscapePrefix)) {
222
+ this.setKeyboardProtocolNegotiationBuffer(sequence);
223
+ return "pending";
224
+ }
225
+ return undefined;
226
+ }
227
+ setKeyboardProtocolNegotiationBuffer(sequence) {
228
+ this.clearKeyboardProtocolNegotiationBufferFlushTimer();
229
+ this.keyboardProtocolNegotiationBuffer = sequence;
230
+ }
231
+ clearKeyboardProtocolNegotiationBuffer() {
232
+ this.clearKeyboardProtocolNegotiationBufferFlushTimer();
233
+ this.keyboardProtocolNegotiationBuffer = "";
234
+ }
235
+ flushKeyboardProtocolNegotiationBufferAsInput() {
236
+ if (!this.keyboardProtocolNegotiationBuffer)
237
+ return;
238
+ const sequence = this.keyboardProtocolNegotiationBuffer;
239
+ this.clearKeyboardProtocolNegotiationBuffer();
240
+ this.forwardInputSequence(sequence);
241
+ }
242
+ scheduleKeyboardProtocolNegotiationBufferFlush() {
243
+ if (!this.keyboardProtocolNegotiationBuffer || this.keyboardProtocolBufferFlushTimer)
244
+ return;
245
+ this.keyboardProtocolBufferFlushTimer = setTimeout(() => {
246
+ this.keyboardProtocolBufferFlushTimer = undefined;
247
+ this.flushKeyboardProtocolNegotiationBufferAsInput();
248
+ }, KEYBOARD_PROTOCOL_RESPONSE_FRAGMENT_TIMEOUT_MS);
249
+ }
250
+ clearKeyboardProtocolNegotiationBufferFlushTimer() {
251
+ if (!this.keyboardProtocolBufferFlushTimer)
252
+ return;
253
+ clearTimeout(this.keyboardProtocolBufferFlushTimer);
254
+ this.keyboardProtocolBufferFlushTimer = undefined;
255
+ }
256
+ forwardInputSequence(sequence) {
257
+ if (!this.inputHandler)
258
+ return;
259
+ const isAppleTerminal = sequence === "\r" && isAppleTerminalSession();
260
+ const input = normalizeAppleTerminalInput(sequence, isAppleTerminal, isAppleTerminal && isNativeModifierPressed("shift"));
261
+ this.inputHandler(input);
262
+ }
263
+ enableModifyOtherKeys() {
264
+ if (this._kittyProtocolActive || this._modifyOtherKeysActive)
265
+ return;
266
+ process.stdout.write("\x1b[>4;2m");
267
+ this._modifyOtherKeysActive = true;
268
+ }
269
+ clearKeyboardProtocolFallbackTimer() {
270
+ if (!this.keyboardProtocolFallbackTimer)
271
+ return;
272
+ clearTimeout(this.keyboardProtocolFallbackTimer);
273
+ this.keyboardProtocolFallbackTimer = undefined;
151
274
  }
152
275
  /**
153
276
  * On Windows, add ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200) to the stdin
@@ -188,13 +311,19 @@ export class ProcessTerminal {
188
311
  }
189
312
  }
190
313
  async drainInput(maxMs = 1000, idleMs = 50) {
191
- if (this._kittyProtocolActive) {
314
+ const shouldDisableKittyProtocol = this.keyboardProtocolPushed || this._kittyProtocolActive || this.keyboardProtocolNegotiationPending;
315
+ this.keyboardProtocolLateResponsePending = false;
316
+ this.clearKeyboardProtocolNegotiationBuffer();
317
+ this.clearKeyboardProtocolFallbackTimer();
318
+ if (shouldDisableKittyProtocol) {
192
319
  // Disable Kitty keyboard protocol first so any late key releases
193
320
  // do not generate new Kitty escape sequences.
194
321
  process.stdout.write("\x1b[<u");
322
+ this.keyboardProtocolPushed = false;
195
323
  this._kittyProtocolActive = false;
196
324
  setKittyProtocolActive(false);
197
325
  }
326
+ this.keyboardProtocolNegotiationPending = false;
198
327
  if (this._modifyOtherKeysActive) {
199
328
  process.stdout.write("\x1b[>4;0m");
200
329
  this._modifyOtherKeysActive = false;
@@ -229,12 +358,18 @@ export class ProcessTerminal {
229
358
  }
230
359
  // Disable bracketed paste mode
231
360
  process.stdout.write("\x1b[?2004l");
361
+ const shouldDisableKittyProtocol = this.keyboardProtocolPushed || this._kittyProtocolActive || this.keyboardProtocolNegotiationPending;
362
+ this.keyboardProtocolLateResponsePending = false;
363
+ this.clearKeyboardProtocolNegotiationBuffer();
364
+ this.clearKeyboardProtocolFallbackTimer();
232
365
  // Disable Kitty keyboard protocol if not already done by drainInput()
233
- if (this._kittyProtocolActive) {
366
+ if (shouldDisableKittyProtocol) {
234
367
  process.stdout.write("\x1b[<u");
368
+ this.keyboardProtocolPushed = false;
235
369
  this._kittyProtocolActive = false;
236
370
  setKittyProtocolActive(false);
237
371
  }
372
+ this.keyboardProtocolNegotiationPending = false;
238
373
  if (this._modifyOtherKeysActive) {
239
374
  process.stdout.write("\x1b[>4;0m");
240
375
  this._modifyOtherKeysActive = false;
@@ -118,6 +118,7 @@ export class TUI extends Container {
118
118
  // Overlay stack for modal components rendered on top of base content
119
119
  focusOrderCounter = 0;
120
120
  overlayStack = [];
121
+ overlayFocusRestore = { status: "inactive" };
121
122
  constructor(terminal, showHardwareCursor) {
122
123
  super();
123
124
  this.terminal = terminal;
@@ -152,16 +153,115 @@ export class TUI extends Container {
152
153
  this.clearOnShrink = enabled;
153
154
  }
154
155
  setFocus(component) {
155
- // Clear focused flag on old component
156
+ this.setFocusInternal({ component, overlayFocusRestore: "clear" });
157
+ }
158
+ setFocusInternal({ component, overlayFocusRestore, }) {
159
+ const previousFocus = this.focusedComponent;
160
+ let nextFocus = component;
161
+ const previousFocusedOverlay = previousFocus
162
+ ? this.overlayStack.find((entry) => entry.component === previousFocus && this.isOverlayVisible(entry))
163
+ : undefined;
164
+ const nextFocusIsOverlay = nextFocus ? this.overlayStack.some((entry) => entry.component === nextFocus) : false;
165
+ const restoreState = this.getVisibleOverlayFocusRestore();
166
+ if (nextFocus && !nextFocusIsOverlay) {
167
+ if (restoreState.status === "blocked" && restoreState.blockedBy === previousFocus) {
168
+ if (restoreState.resume.status === "focus-target" || !this.isComponentMounted(restoreState.blockedBy)) {
169
+ nextFocus = this.resolveBlockedOverlayFocusResume(restoreState);
170
+ }
171
+ else {
172
+ this.overlayFocusRestore = {
173
+ status: "blocked",
174
+ overlay: restoreState.overlay,
175
+ blockedBy: nextFocus,
176
+ resume: restoreState.resume,
177
+ };
178
+ }
179
+ }
180
+ else if (previousFocusedOverlay &&
181
+ restoreState.status !== "inactive" &&
182
+ restoreState.overlay === previousFocusedOverlay &&
183
+ !this.isOverlayFocusAncestor(previousFocusedOverlay, nextFocus)) {
184
+ this.overlayFocusRestore = {
185
+ status: "blocked",
186
+ overlay: previousFocusedOverlay,
187
+ blockedBy: nextFocus,
188
+ resume: { status: "restore-overlay" },
189
+ };
190
+ }
191
+ }
192
+ else if (nextFocus === null) {
193
+ if (restoreState.status === "blocked" && restoreState.blockedBy === previousFocus) {
194
+ nextFocus = this.resolveBlockedOverlayFocusResume(restoreState);
195
+ }
196
+ else if (overlayFocusRestore === "clear") {
197
+ this.clearOverlayFocusRestore();
198
+ }
199
+ }
156
200
  if (isFocusable(this.focusedComponent)) {
157
201
  this.focusedComponent.focused = false;
158
202
  }
159
- this.focusedComponent = component;
160
- // Set focused flag on new component
161
- if (isFocusable(component)) {
162
- component.focused = true;
203
+ this.focusedComponent = nextFocus;
204
+ if (isFocusable(nextFocus)) {
205
+ nextFocus.focused = true;
206
+ }
207
+ const focusedOverlay = nextFocus
208
+ ? this.overlayStack.find((entry) => entry.component === nextFocus && this.isOverlayVisible(entry))
209
+ : undefined;
210
+ if (focusedOverlay) {
211
+ this.overlayFocusRestore = { status: "eligible", overlay: focusedOverlay };
212
+ }
213
+ }
214
+ clearOverlayFocusRestore() {
215
+ this.overlayFocusRestore = { status: "inactive" };
216
+ }
217
+ clearOverlayFocusRestoreFor(overlay) {
218
+ if (this.overlayFocusRestore.status !== "inactive" && this.overlayFocusRestore.overlay === overlay) {
219
+ this.clearOverlayFocusRestore();
220
+ }
221
+ }
222
+ resolveBlockedOverlayFocusResume(restoreState) {
223
+ if (restoreState.resume.status === "restore-overlay")
224
+ return restoreState.overlay.component;
225
+ this.clearOverlayFocusRestore();
226
+ return restoreState.resume.target;
227
+ }
228
+ getVisibleOverlayFocusRestore() {
229
+ const restoreState = this.overlayFocusRestore;
230
+ if (restoreState.status === "inactive")
231
+ return restoreState;
232
+ if (!this.overlayStack.includes(restoreState.overlay) || !this.isOverlayVisible(restoreState.overlay)) {
233
+ return { status: "inactive" };
234
+ }
235
+ return restoreState;
236
+ }
237
+ isOverlayFocusAncestor(entry, component) {
238
+ const visited = new Set();
239
+ let current = entry.preFocus;
240
+ while (current && !visited.has(current)) {
241
+ visited.add(current);
242
+ if (current === component)
243
+ return true;
244
+ current = this.overlayStack.find((overlay) => overlay.component === current)?.preFocus ?? null;
245
+ }
246
+ return false;
247
+ }
248
+ retargetOverlayPreFocus(removed) {
249
+ for (const overlay of this.overlayStack) {
250
+ if (overlay !== removed && overlay.preFocus === removed.component) {
251
+ overlay.preFocus = removed.preFocus;
252
+ }
163
253
  }
164
254
  }
255
+ isComponentMounted(component) {
256
+ return this.children.some((child) => this.containsComponent(child, component));
257
+ }
258
+ containsComponent(root, target) {
259
+ if (root === target)
260
+ return true;
261
+ if (!(root instanceof Container))
262
+ return false;
263
+ return root.children.some((child) => this.containsComponent(child, target));
264
+ }
165
265
  /**
166
266
  * Show an overlay component with configurable positioning and sizing.
167
267
  * Returns a handle to control the overlay's visibility.
@@ -169,7 +269,7 @@ export class TUI extends Container {
169
269
  showOverlay(component, options) {
170
270
  const entry = {
171
271
  component,
172
- options,
272
+ ...(options === undefined ? {} : { options }),
173
273
  preFocus: this.focusedComponent,
174
274
  hidden: false,
175
275
  focusOrder: ++this.focusOrderCounter,
@@ -186,6 +286,8 @@ export class TUI extends Container {
186
286
  hide: () => {
187
287
  const index = this.overlayStack.indexOf(entry);
188
288
  if (index !== -1) {
289
+ this.clearOverlayFocusRestoreFor(entry);
290
+ this.retargetOverlayPreFocus(entry);
189
291
  this.overlayStack.splice(index, 1);
190
292
  // Restore focus if this overlay had focus
191
293
  if (this.focusedComponent === component) {
@@ -203,6 +305,7 @@ export class TUI extends Container {
203
305
  entry.hidden = hidden;
204
306
  // Update focus when hiding/showing
205
307
  if (hidden) {
308
+ this.clearOverlayFocusRestoreFor(entry);
206
309
  // If this overlay had focus, move focus to next visible or preFocus
207
310
  if (this.focusedComponent === component) {
208
311
  const topVisible = this.getTopmostVisibleOverlay();
@@ -222,17 +325,39 @@ export class TUI extends Container {
222
325
  focus: () => {
223
326
  if (!this.overlayStack.includes(entry) || !this.isOverlayVisible(entry))
224
327
  return;
225
- if (this.focusedComponent !== component) {
226
- this.setFocus(component);
227
- }
228
328
  entry.focusOrder = ++this.focusOrderCounter;
329
+ this.setFocus(component);
229
330
  this.requestRender();
230
331
  },
231
- unfocus: () => {
232
- if (this.focusedComponent !== component)
332
+ unfocus: (unfocusOptions) => {
333
+ const isFocused = this.focusedComponent === component;
334
+ const restoreState = this.overlayFocusRestore;
335
+ const hasPendingRestore = restoreState.status !== "inactive" && restoreState.overlay === entry;
336
+ if (!isFocused && !hasPendingRestore)
337
+ return;
338
+ if (restoreState.status === "blocked" &&
339
+ restoreState.overlay === entry &&
340
+ this.focusedComponent === restoreState.blockedBy) {
341
+ if (unfocusOptions) {
342
+ this.overlayFocusRestore = {
343
+ status: "blocked",
344
+ overlay: entry,
345
+ blockedBy: restoreState.blockedBy,
346
+ resume: { status: "focus-target", target: unfocusOptions.target },
347
+ };
348
+ }
349
+ else {
350
+ this.clearOverlayFocusRestore();
351
+ }
352
+ this.requestRender();
233
353
  return;
234
- const topVisible = this.getTopmostVisibleOverlay();
235
- this.setFocus(topVisible && topVisible !== entry ? topVisible.component : entry.preFocus);
354
+ }
355
+ this.clearOverlayFocusRestoreFor(entry);
356
+ if (isFocused || unfocusOptions) {
357
+ const topVisible = this.getTopmostVisibleOverlay();
358
+ const fallbackTarget = topVisible && topVisible !== entry ? topVisible.component : entry.preFocus;
359
+ this.setFocus(unfocusOptions ? unfocusOptions.target : fallbackTarget);
360
+ }
236
361
  this.requestRender();
237
362
  },
238
363
  isFocused: () => this.focusedComponent === component,
@@ -240,9 +365,12 @@ export class TUI extends Container {
240
365
  }
241
366
  /** Hide the topmost overlay and restore previous focus. */
242
367
  hideOverlay() {
243
- const overlay = this.overlayStack.pop();
368
+ const overlay = this.overlayStack[this.overlayStack.length - 1];
244
369
  if (!overlay)
245
370
  return;
371
+ this.clearOverlayFocusRestoreFor(overlay);
372
+ this.retargetOverlayPreFocus(overlay);
373
+ this.overlayStack.pop();
246
374
  if (this.focusedComponent === overlay.component) {
247
375
  // Find topmost visible overlay, or fall back to preFocus
248
376
  const topVisible = this.getTopmostVisibleOverlay();
@@ -265,16 +393,17 @@ export class TUI extends Container {
265
393
  }
266
394
  return true;
267
395
  }
268
- /** Find the topmost visible capturing overlay, if any */
396
+ /** Find the visual-frontmost visible capturing overlay, if any */
269
397
  getTopmostVisibleOverlay() {
270
- for (let i = this.overlayStack.length - 1; i >= 0; i--) {
271
- if (this.overlayStack[i].options?.nonCapturing)
398
+ let topmost;
399
+ for (const overlay of this.overlayStack) {
400
+ if (overlay.options?.nonCapturing || !this.isOverlayVisible(overlay))
272
401
  continue;
273
- if (this.isOverlayVisible(this.overlayStack[i])) {
274
- return this.overlayStack[i];
402
+ if (!topmost || overlay.focusOrder > topmost.focusOrder) {
403
+ topmost = overlay;
275
404
  }
276
405
  }
277
- return undefined;
406
+ return topmost;
278
407
  }
279
408
  invalidate() {
280
409
  super.invalidate();
@@ -411,8 +540,23 @@ export class TUI extends Container {
411
540
  this.setFocus(topVisible.component);
412
541
  }
413
542
  else {
414
- // No visible overlays, restore to preFocus
415
- this.setFocus(focusedOverlay.preFocus);
543
+ this.setFocusInternal({ component: focusedOverlay.preFocus, overlayFocusRestore: "preserve" });
544
+ }
545
+ }
546
+ const focusIsOverlay = this.overlayStack.some((o) => o.component === this.focusedComponent);
547
+ if (!focusIsOverlay) {
548
+ const restoreState = this.getVisibleOverlayFocusRestore();
549
+ if (restoreState.status === "eligible") {
550
+ this.setFocus(restoreState.overlay.component);
551
+ }
552
+ else if (restoreState.status === "blocked" && restoreState.blockedBy !== this.focusedComponent) {
553
+ if (restoreState.resume.status === "restore-overlay") {
554
+ this.setFocus(restoreState.overlay.component);
555
+ }
556
+ else {
557
+ this.clearOverlayFocusRestore();
558
+ this.setFocus(restoreState.resume.target);
559
+ }
416
560
  }
417
561
  }
418
562
  // Pass input to focused component (including Ctrl+C)
@@ -604,7 +604,10 @@ export function wrapTextWithAnsi(text, width) {
604
604
  for (const inputLine of inputLines) {
605
605
  // Prepend active ANSI codes from previous lines (except for first line)
606
606
  const prefix = result.length > 0 ? tracker.getActiveCodes() : "";
607
- result.push(...wrapSingleLine(prefix + inputLine, width));
607
+ const wrappedLines = wrapSingleLine(prefix + inputLine, width);
608
+ for (const wrappedLine of wrappedLines) {
609
+ result.push(wrappedLine);
610
+ }
608
611
  // Update tracker with codes from this line for next iteration
609
612
  updateTrackerFromText(inputLine, tracker);
610
613
  }
@@ -640,7 +643,9 @@ function wrapSingleLine(line, width) {
640
643
  }
641
644
  // Break long token - breakLongWord handles its own resets
642
645
  const broken = breakLongWord(token, width, tracker);
643
- wrapped.push(...broken.slice(0, -1));
646
+ for (let i = 0; i < broken.length - 1; i++) {
647
+ wrapped.push(broken[i]);
648
+ }
644
649
  currentLine = broken[broken.length - 1];
645
650
  currentVisibleLength = visibleWidth(currentLine);
646
651
  continue;
@@ -679,7 +684,7 @@ function wrapSingleLine(line, width) {
679
684
  // Trailing whitespace can cause lines to exceed the requested width
680
685
  return wrapped.length > 0 ? wrapped.map((line) => line.trimEnd()) : [""];
681
686
  }
682
- const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;
687
+ export const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;
683
688
  /**
684
689
  * Check if a character is whitespace.
685
690
  */
@@ -0,0 +1,96 @@
1
+ import { getWordSegmenter, isWhitespaceChar, PUNCTUATION_REGEX } from "./utils.js";
2
+ const wordSegmenter = getWordSegmenter();
3
+ /**
4
+ * Find the cursor position after moving one word backward from `cursor` in `text`.
5
+ * Skips trailing whitespace, then stops at the next word/punctuation boundary.
6
+ *
7
+ * Pure function - does not mutate any state.
8
+ */
9
+ export function findWordBackward(text, cursor, options) {
10
+ if (cursor <= 0)
11
+ return 0;
12
+ const textBeforeCursor = text.slice(0, cursor);
13
+ const segmentFn = options?.segment;
14
+ const isAtomic = options?.isAtomicSegment;
15
+ const segments = segmentFn ? [...segmentFn(textBeforeCursor)] : [...wordSegmenter.segment(textBeforeCursor)];
16
+ let newCursor = cursor;
17
+ // Skip trailing whitespace
18
+ while (segments.length > 0 &&
19
+ !isAtomic?.(segments[segments.length - 1]?.segment || "") &&
20
+ isWhitespaceChar(segments[segments.length - 1]?.segment || "")) {
21
+ newCursor -= segments.pop()?.segment.length || 0;
22
+ }
23
+ if (segments.length === 0)
24
+ return newCursor;
25
+ const last = segments[segments.length - 1];
26
+ if (isAtomic?.(last.segment)) {
27
+ // Skip one atomic segment.
28
+ newCursor -= last.segment.length;
29
+ }
30
+ else if (last.isWordLike) {
31
+ // Skip inside one word-like segment, preserving ASCII punctuation boundaries.
32
+ const segment = last.segment;
33
+ const matches = [...segment.matchAll(new RegExp(PUNCTUATION_REGEX, "g"))];
34
+ if (matches.length <= 0) {
35
+ newCursor -= segment.length;
36
+ }
37
+ else {
38
+ const lastMatch = matches[matches.length - 1];
39
+ newCursor -= segment.length - (lastMatch.index + lastMatch[0].length);
40
+ }
41
+ }
42
+ else {
43
+ // Skip non-word non-whitespace run (punctuation)
44
+ while (segments.length > 0 &&
45
+ !isAtomic?.(segments[segments.length - 1]?.segment || "") &&
46
+ !segments[segments.length - 1]?.isWordLike &&
47
+ !isWhitespaceChar(segments[segments.length - 1]?.segment || "")) {
48
+ newCursor -= segments.pop()?.segment.length || 0;
49
+ }
50
+ }
51
+ return newCursor;
52
+ }
53
+ /**
54
+ * Find the cursor position after moving one word forward from `cursor` in `text`.
55
+ * Skips leading whitespace, then stops at the next word/punctuation boundary.
56
+ *
57
+ * Pure function - does not mutate any state.
58
+ */
59
+ export function findWordForward(text, cursor, options) {
60
+ if (cursor >= text.length)
61
+ return text.length;
62
+ const textAfterCursor = text.slice(cursor);
63
+ const segmentFn = options?.segment;
64
+ const isAtomic = options?.isAtomicSegment;
65
+ const segments = segmentFn ? segmentFn(textAfterCursor) : wordSegmenter.segment(textAfterCursor);
66
+ const iterator = segments[Symbol.iterator]();
67
+ let next = iterator.next();
68
+ let newCursor = cursor;
69
+ // Skip leading whitespace
70
+ while (!next.done && !isAtomic?.(next.value.segment) && isWhitespaceChar(next.value.segment)) {
71
+ newCursor += next.value.segment.length;
72
+ next = iterator.next();
73
+ }
74
+ if (next.done)
75
+ return newCursor;
76
+ if (isAtomic?.(next.value.segment)) {
77
+ // Skip one atomic segment.
78
+ newCursor += next.value.segment.length;
79
+ }
80
+ else if (next.value.isWordLike) {
81
+ // Skip inside one word-like segment, preserving ASCII punctuation boundaries.
82
+ newCursor += PUNCTUATION_REGEX.exec(next.value.segment)?.index ?? next.value.segment.length;
83
+ }
84
+ else {
85
+ // Skip non-word non-whitespace run (punctuation)
86
+ while (!next.done &&
87
+ !isAtomic?.(next.value.segment) &&
88
+ !next.value.isWordLike &&
89
+ !isWhitespaceChar(next.value.segment)) {
90
+ newCursor += next.value.segment.length;
91
+ next = iterator.next();
92
+ }
93
+ }
94
+ return newCursor;
95
+ }
96
+ //# sourceMappingURL=word-navigation.js.map