pi-ui-extend 0.1.34 → 0.1.36

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 (73) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.d.ts +1 -0
  3. package/dist/app/app.js +12 -2
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.d.ts +1 -0
  6. package/dist/app/commands/command-model-actions.js +32 -0
  7. package/dist/app/commands/command-navigation-actions.js +3 -0
  8. package/dist/app/commands/command-session-actions.js +2 -0
  9. package/dist/app/constants.d.ts +2 -1
  10. package/dist/app/constants.js +6 -1
  11. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  12. package/dist/app/extensions/extension-actions-controller.js +4 -0
  13. package/dist/app/input/input-controller.d.ts +5 -1
  14. package/dist/app/input/input-controller.js +122 -16
  15. package/dist/app/input/input-paste-handler.js +3 -1
  16. package/dist/app/input/terminal-edit-shortcuts.d.ts +21 -0
  17. package/dist/app/input/terminal-edit-shortcuts.js +92 -16
  18. package/dist/app/popup/popup-action-controller.d.ts +1 -0
  19. package/dist/app/popup/popup-action-controller.js +1 -0
  20. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  21. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  22. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  23. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  24. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  25. package/dist/app/rendering/conversation-viewport.js +41 -5
  26. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  27. package/dist/app/rendering/editor-panels.js +27 -10
  28. package/dist/app/runtime.d.ts +1 -0
  29. package/dist/app/runtime.js +33 -14
  30. package/dist/app/session/session-event-controller.d.ts +7 -0
  31. package/dist/app/session/session-event-controller.js +78 -0
  32. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  33. package/dist/app/session/session-lifecycle-controller.js +7 -0
  34. package/dist/app/session/tabs-controller.d.ts +1 -0
  35. package/dist/app/session/tabs-controller.js +4 -1
  36. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  37. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  38. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  39. package/dist/app/terminal/terminal-controller.js +91 -2
  40. package/dist/app/todo/todo-model.js +2 -0
  41. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  42. package/dist/app/todo/todo-widget-controller.js +17 -7
  43. package/dist/app/types.d.ts +4 -0
  44. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
  45. package/dist/app/workspace/workspace-actions-controller.js +1 -0
  46. package/dist/bundled-extensions/question/tui.js +8 -1
  47. package/dist/bundled-extensions/session-title/index.js +65 -14
  48. package/dist/input-editor-files.js +23 -4
  49. package/dist/markdown-format.d.ts +4 -1
  50. package/dist/markdown-format.js +76 -9
  51. package/external/pi-tools-suite/README.md +71 -1
  52. package/external/pi-tools-suite/package.json +5 -5
  53. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  55. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  56. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  57. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  58. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  59. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  60. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  61. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  62. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  63. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  64. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  65. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  66. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  67. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  68. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  69. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  70. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  71. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  72. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  73. package/package.json +7 -5
package/README.md CHANGED
@@ -160,6 +160,26 @@ Before committing code changes, run:
160
160
  npm run check
161
161
  ```
162
162
 
163
+ ### Keeping the bundled extension's SDK pin in sync
164
+
165
+ Pix bundles the `pi-tools-suite` extension (in `external/pi-tools-suite`), which
166
+ runs inside the Pi host process. Its `@earendil-works/*` peerDependencies must
167
+ match the host Pi SDK version exactly, or npm can resolve a stale copy in the
168
+ suite's own `node_modules` and cause a double-load (e.g. `0.75.4` in the suite
169
+ vs `0.79.4` in the host).
170
+
171
+ When you bump the Pi SDK in the root `package.json`, re-pin the suite and
172
+ reinstall it:
173
+
174
+ ```bash
175
+ npm run sync:sdk-pin # rewrite suite peerDeps to host version
176
+ cd external/pi-tools-suite && npm install --ignore-scripts # update suite lockfile/node_modules
177
+ ```
178
+
179
+ `npm run check` runs `npm run sync:sdk-pin:check` first, so a stale pin fails
180
+ the check fast. Use `npm run sync:sdk-pin:check` on its own for a drift-only
181
+ report (non-zero exit on drift).
182
+
163
183
  ## Configuration
164
184
 
165
185
  Useful environment variables:
package/dist/app/app.d.ts CHANGED
@@ -66,6 +66,7 @@ export declare class PiUiExtendApp {
66
66
  start(): Promise<void>;
67
67
  private checkPixUpdateOnStartup;
68
68
  private bindCurrentSession;
69
+ private awaitCurrentSessionExtensions;
69
70
  private activateRuntime;
70
71
  private createExtensionEventBus;
71
72
  private handleTerminalBellAttention;
package/dist/app/app.js CHANGED
@@ -166,12 +166,13 @@ export class PiUiExtendApp {
166
166
  maxProjectSessions: () => this.pixConfig.maxProjectSessions,
167
167
  blinkController: this.blinkController,
168
168
  runtime: () => this.runtime,
169
- createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options)),
169
+ createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options), this.runtime === undefined ? {} : { reuseServicesFrom: this.runtime }),
170
170
  createRuntimeForSession: (sessionPath) => this.createRuntime({
171
171
  ...this.options,
172
172
  noSession: false,
173
173
  sessionPath,
174
174
  }),
175
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
175
176
  activateRuntime: (runtime, options) => this.activateRuntime(runtime, options),
176
177
  disposeRuntime: (runtime) => this.terminalController.disposeRuntime(runtime),
177
178
  isRunning: () => this.running,
@@ -317,6 +318,7 @@ export class PiUiExtendApp {
317
318
  isRunning: () => this.running,
318
319
  getInput: () => this.input,
319
320
  setInput: (value) => this.setInput(value),
321
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
320
322
  resetSessionView: () => this.resetSessionView(),
321
323
  loadSessionHistory: () => this.loadSessionHistory(),
322
324
  afterSessionReplacement: (message) => this.afterSessionReplacement(message),
@@ -334,12 +336,14 @@ export class PiUiExtendApp {
334
336
  });
335
337
  this.todoWidgetController = new AppTodoWidgetController({
336
338
  sessionFile: () => this.runtime?.session.sessionFile,
339
+ sessionId: () => this.runtime?.session.sessionId,
337
340
  isRunning: () => this.running,
338
341
  render: () => this.scheduleRender(),
339
342
  });
340
343
  this.workspaceActions = new AppWorkspaceActionsController({
341
344
  entries: this.entries,
342
345
  runtime: () => this.runtime,
346
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
343
347
  findUserEntry: (entryId) => this.findUserEntry(entryId),
344
348
  touchEntry: (entry) => this.touchEntry(entry),
345
349
  resetSessionView: () => this.resetSessionView(),
@@ -470,6 +474,7 @@ export class PiUiExtendApp {
470
474
  showMenu: (items, options) => this.popupMenus.menuController.show(items, options),
471
475
  getModelMenuItems: (query) => this.menuItems.getModelMenuItems(query),
472
476
  getThinkingMenuItems: (query) => this.menuItems.getThinkingMenuItems(query),
477
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
473
478
  modelRef: (model) => this.menuItems.modelRef(model),
474
479
  getFavoriteScopedModels: () => this.menuItems.getFavoriteScopedModels(),
475
480
  setSessionStatus: (session) => this.setSessionStatus(session),
@@ -518,6 +523,7 @@ export class PiUiExtendApp {
518
523
  showToast: (message, kind) => this.showToast(message, kind),
519
524
  render: () => this.render(),
520
525
  resetSessionView: () => this.resetSessionView(),
526
+ awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
521
527
  bindCurrentSession: () => this.bindCurrentSession(),
522
528
  loadSessionHistory: () => this.loadSessionHistory(),
523
529
  scrollToConversationEntry: (entryId) => this.scrollController.scrollToConversationEntry(entryId),
@@ -761,10 +767,11 @@ export class PiUiExtendApp {
761
767
  });
762
768
  this.slashCommands = this.commandController.slashCommands;
763
769
  }
764
- createRuntime(options) {
770
+ createRuntime(options, runtimeOptions = {}) {
765
771
  return createPixRuntime(options, {
766
772
  eventBus: this.createExtensionEventBus(),
767
773
  config: this.pixConfig,
774
+ ...runtimeOptions,
768
775
  });
769
776
  }
770
777
  async loadStartupConfig() {
@@ -822,6 +829,9 @@ export class PiUiExtendApp {
822
829
  async bindCurrentSession(options) {
823
830
  await this.sessionLifecycle.bindCurrentSession(options);
824
831
  }
832
+ async awaitCurrentSessionExtensions(runtime) {
833
+ await this.sessionLifecycle.awaitCurrentSessionExtensions(runtime);
834
+ }
825
835
  async activateRuntime(runtime, options) {
826
836
  this.runtime = runtime;
827
837
  runtime.setRebindSession(async () => {
@@ -7,6 +7,7 @@ export type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
7
7
  export type CommandControllerHost = {
8
8
  readonly options: AppOptions;
9
9
  runtime(): AgentSessionRuntime | undefined;
10
+ awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
10
11
  requestHistory(): AppRequestHistory;
11
12
  getInput(): string;
12
13
  setInput(value: string): void;
@@ -14,6 +14,7 @@ export declare class ModelCommandActions {
14
14
  runModelCommand(model: SessionModel): Promise<void>;
15
15
  runThinkingCommand(level: ThinkingLevel): Promise<void>;
16
16
  private addPersistentSystemEntry;
17
+ private reloadAfterModelChange;
17
18
  private saveDefaultModel;
18
19
  private saveDefaultThinking;
19
20
  }
@@ -269,6 +269,17 @@ export class ModelCommandActions {
269
269
  this.host.render();
270
270
  await runtime.session.setModel(model);
271
271
  this.host.addEntry({ id: createId("system"), kind: "system", text: `Selected model ${ref}` });
272
+ if (runtime.session.isStreaming) {
273
+ this.host.addEntry({
274
+ id: createId("system"),
275
+ kind: "system",
276
+ text: "Skipped reload because the agent is still running. Run /reload when idle to refresh model-specific tools.",
277
+ });
278
+ this.host.toast.warning("Model changed; reload skipped while the agent is running");
279
+ this.host.setSessionStatus(runtime.session);
280
+ return;
281
+ }
282
+ await this.reloadAfterModelChange(runtime.session, ref);
272
283
  this.host.setSessionStatus(runtime.session);
273
284
  }
274
285
  async runThinkingCommand(level) {
@@ -285,6 +296,27 @@ export class ModelCommandActions {
285
296
  appendPixSystemDisplayEntry(session, text);
286
297
  this.host.addEntry({ id: createId("system"), kind: "system", text });
287
298
  }
299
+ async reloadAfterModelChange(session, ref) {
300
+ this.host.setStatus(`reloading resources for ${ref}`);
301
+ this.host.render();
302
+ try {
303
+ await session.reload();
304
+ this.host.addEntry({
305
+ id: createId("system"),
306
+ kind: "system",
307
+ text: `Reloaded resources after model change to ${ref}`,
308
+ });
309
+ this.host.toast.success("Model changed and resources reloaded");
310
+ }
311
+ catch (error) {
312
+ this.host.addEntry({
313
+ id: createId("error"),
314
+ kind: "error",
315
+ text: `Model changed to ${ref}, but reload failed: ${error instanceof Error ? error.message : String(error)}`,
316
+ });
317
+ this.host.toast.error("Model changed, but reload failed");
318
+ }
319
+ }
288
320
  saveDefaultModel(modelRef) {
289
321
  const saved = savePixDefaultModel(modelRef);
290
322
  if (!saved)
@@ -50,6 +50,7 @@ export class NavigationCommandActions {
50
50
  throw new Error("No user messages to fork from");
51
51
  this.host.setStatus("forking session");
52
52
  this.host.render();
53
+ await this.host.awaitCurrentSessionExtensions(runtime);
53
54
  const result = await runtime.fork(entryId);
54
55
  if (result.cancelled) {
55
56
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Fork cancelled." });
@@ -74,6 +75,7 @@ export class NavigationCommandActions {
74
75
  }
75
76
  this.host.setStatus("cloning session");
76
77
  this.host.render();
78
+ await this.host.awaitCurrentSessionExtensions(runtime);
77
79
  const result = await runtime.fork(leafId, { position: "at" });
78
80
  if (result.cancelled) {
79
81
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Clone cancelled." });
@@ -228,6 +230,7 @@ export class NavigationCommandActions {
228
230
  const resolvedSessionPath = resolve(runtime.cwd, sessionPath);
229
231
  this.host.setStatus("switching session");
230
232
  this.host.render();
233
+ await this.host.awaitCurrentSessionExtensions(runtime);
231
234
  const result = await runtime.switchSession(resolvedSessionPath);
232
235
  if (result.cancelled) {
233
236
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Resume cancelled." });
@@ -260,6 +260,7 @@ export class SessionCommandActions {
260
260
  this.host.setStatus("reloading");
261
261
  this.host.render();
262
262
  try {
263
+ await this.host.awaitCurrentSessionExtensions(runtime);
263
264
  await runtime.session.reload();
264
265
  this.host.setSessionStatus(runtime.session);
265
266
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Reloaded keybindings, extensions, skills, prompts, themes" });
@@ -277,6 +278,7 @@ export class SessionCommandActions {
277
278
  return;
278
279
  this.host.setStatus("starting new session");
279
280
  this.host.render();
281
+ await this.host.awaitCurrentSessionExtensions(runtime);
280
282
  const result = await runtime.newSession();
281
283
  if (result.cancelled) {
282
284
  this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
@@ -19,7 +19,8 @@ export declare const REQUEST_HISTORY_VERSION = 1;
19
19
  export declare const REQUEST_HISTORY_MAX_ENTRIES = 200;
20
20
  export declare const REQUEST_HISTORY_MAX_BYTES: number;
21
21
  export declare const REQUEST_HISTORY_MAX_ENTRY_BYTES: number;
22
- export declare const ENABLE_TERMINAL_KEY_REPORTING = "\u001B[>7u\u001B[>4;2m";
22
+ export declare const ENABLE_TERMINAL_KEY_REPORTING = "\u001B[>7u\u001B[?u\u001B[c";
23
+ export declare const ENABLE_TERMINAL_MODIFY_OTHER_KEYS = "\u001B[>4;2m";
23
24
  export declare const DISABLE_TERMINAL_KEY_REPORTING = "\u001B[<u\u001B[>4;0m";
24
25
  export declare const ENABLE_BRACKETED_PASTE = "\u001B[?2004h";
25
26
  export declare const DISABLE_BRACKETED_PASTE = "\u001B[?2004l";
@@ -52,7 +52,12 @@ export const REQUEST_HISTORY_VERSION = 1;
52
52
  export const REQUEST_HISTORY_MAX_ENTRIES = 200;
53
53
  export const REQUEST_HISTORY_MAX_BYTES = 128 * 1024;
54
54
  export const REQUEST_HISTORY_MAX_ENTRY_BYTES = 16 * 1024;
55
- export const ENABLE_TERMINAL_KEY_REPORTING = "\x1b[>7u\x1b[>4;2m";
55
+ // Match pi/@earendil-works/pi-tui keyboard setup: request Kitty keyboard
56
+ // protocol flags, query the terminal response, and use xterm modifyOtherKeys
57
+ // only as a response-driven fallback. Enabling both protocols blindly can make
58
+ // terminals disagree about modified Enter reporting.
59
+ export const ENABLE_TERMINAL_KEY_REPORTING = "\x1b[>7u\x1b[?u\x1b[c";
60
+ export const ENABLE_TERMINAL_MODIFY_OTHER_KEYS = "\x1b[>4;2m";
56
61
  export const DISABLE_TERMINAL_KEY_REPORTING = "\x1b[<u\x1b[>4;0m";
57
62
  export const ENABLE_BRACKETED_PASTE = "\x1b[?2004h";
58
63
  export const DISABLE_BRACKETED_PASTE = "\x1b[?2004l";
@@ -4,6 +4,7 @@ export type AppExtensionActionsHost = {
4
4
  isRunning(): boolean;
5
5
  getInput(): string;
6
6
  setInput(value: string): void;
7
+ awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
7
8
  resetSessionView(): void;
8
9
  loadSessionHistory(): void;
9
10
  afterSessionReplacement(message?: string): void;
@@ -8,12 +8,14 @@ export class AppExtensionActionsController {
8
8
  return {
9
9
  waitForIdle: () => this.waitForSessionIdle(runtime),
10
10
  newSession: async (options) => {
11
+ await this.host.awaitCurrentSessionExtensions(runtime);
11
12
  const result = await runtime.newSession(options);
12
13
  if (!result.cancelled)
13
14
  this.host.afterSessionReplacement("Started a new session.");
14
15
  return result;
15
16
  },
16
17
  fork: async (entryId, options) => {
18
+ await this.host.awaitCurrentSessionExtensions(runtime);
17
19
  const result = await runtime.fork(entryId, options);
18
20
  if (!result.cancelled)
19
21
  this.host.afterSessionReplacement("Forked to a new session.");
@@ -32,12 +34,14 @@ export class AppExtensionActionsController {
32
34
  return result;
33
35
  },
34
36
  switchSession: async (sessionPath, options) => {
37
+ await this.host.awaitCurrentSessionExtensions(runtime);
35
38
  const result = await runtime.switchSession(sessionPath, options);
36
39
  if (!result.cancelled)
37
40
  this.host.afterSessionReplacement(`Switched session: ${sessionPath}`);
38
41
  return result;
39
42
  },
40
43
  reload: async () => {
44
+ await this.host.awaitCurrentSessionExtensions(runtime);
41
45
  await runtime.session.reload();
42
46
  this.host.setSessionStatus(runtime.session);
43
47
  this.host.showToast("Reloaded resources", "success");
@@ -35,7 +35,7 @@ export declare class AppInputController {
35
35
  private readonly pasteHandler;
36
36
  constructor(host: InputControllerHost);
37
37
  handleChunk(chunk: Buffer): void;
38
- private consumeSharedEditorShiftEnter;
38
+ private consumeSharedEditorInput;
39
39
  private drainInputBuffer;
40
40
  private consumeBracketedPastePayload;
41
41
  private getEscapeSequences;
@@ -45,6 +45,10 @@ export declare class AppInputController {
45
45
  private consumeCommandArrowPageSequence;
46
46
  private consumeCommandArrowPageMatch;
47
47
  private consumeTerminalEditShortcutSequence;
48
+ private consumeIgnoredModifiedKeySequence;
49
+ private consumeModifiedArrowKeySequence;
50
+ private consumeClipboardImagePasteSequence;
51
+ private consumeShiftEnterSequence;
48
52
  private consumeTerminalInterruptSequence;
49
53
  private handleArrowUp;
50
54
  private handleArrowDown;
@@ -1,6 +1,7 @@
1
1
  import { InputPasteHandler } from "./input-paste-handler.js";
2
2
  import { hasTerminalCommandModifier, isNativeCommandPressed, isNativeShiftPressed } from "./native-modifiers.js";
3
- import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, terminalEditShortcutForControlChar } from "./terminal-edit-shortcuts.js";
3
+ import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, parseTerminalModifiedKeySequence, terminalKeyArrowDirection, terminalEditShortcutForControlChar, terminalKeyIsClipboardImagePaste, terminalKeyIsShiftEnter, terminalKeyShouldIgnore, } from "./terminal-edit-shortcuts.js";
4
+ const SHIFT_ENTER_ESCAPE_SEQUENCES = ["\x1b\r", "\x1b\n"];
4
5
  export class AppInputController {
5
6
  host;
6
7
  inputBuffer = "";
@@ -16,7 +17,7 @@ export class AppInputController {
16
17
  this.drainInputBuffer();
17
18
  return;
18
19
  }
19
- if (this.consumeSharedEditorShiftEnter(data))
20
+ if (this.consumeSharedEditorInput(data))
20
21
  return;
21
22
  const extensionInput = this.host.handleExtensionTerminalInput(data);
22
23
  if (extensionInput.consume)
@@ -28,17 +29,41 @@ export class AppInputController {
28
29
  this.inputBuffer += data;
29
30
  this.drainInputBuffer();
30
31
  }
31
- consumeSharedEditorShiftEnter(data) {
32
+ consumeSharedEditorInput(data) {
32
33
  if (this.host.extensionInputUsesEditor?.() !== true)
33
34
  return false;
34
35
  if (this.host.inputEditor.isInBracketedPaste)
35
36
  return false;
36
- if (!this.isShiftPressed())
37
- return false;
38
- if (data !== "\r" && data !== "\n")
37
+ if (data === "\n") {
38
+ this.insertInputNewline();
39
+ return true;
40
+ }
41
+ if (data === "\r" && this.isShiftPressed()) {
42
+ this.insertInputNewline();
43
+ return true;
44
+ }
45
+ if (SHIFT_ENTER_ESCAPE_SEQUENCES.includes(data)) {
46
+ this.insertInputNewline();
47
+ return true;
48
+ }
49
+ if (data === "\x16") {
50
+ void this.pasteHandler.handleClipboardImagePaste();
51
+ return true;
52
+ }
53
+ const modifiedKey = parseTerminalModifiedKeySequence(data);
54
+ if (modifiedKey.kind !== "key")
39
55
  return false;
40
- this.insertInputNewline();
41
- return true;
56
+ if (terminalKeyShouldIgnore(modifiedKey.key))
57
+ return true;
58
+ if (terminalKeyIsShiftEnter(modifiedKey.key)) {
59
+ this.insertInputNewline();
60
+ return true;
61
+ }
62
+ if (terminalKeyIsClipboardImagePaste(modifiedKey.key)) {
63
+ void this.pasteHandler.handleClipboardImagePaste();
64
+ return true;
65
+ }
66
+ return false;
42
67
  }
43
68
  drainInputBuffer() {
44
69
  while (this.inputBuffer.length > 0) {
@@ -72,11 +97,31 @@ export class AppInputController {
72
97
  continue;
73
98
  if (terminalInterruptSequence === "pending")
74
99
  return;
100
+ const shiftEnterSequence = this.consumeShiftEnterSequence();
101
+ if (shiftEnterSequence === "consumed")
102
+ continue;
103
+ if (shiftEnterSequence === "pending")
104
+ return;
105
+ const clipboardImagePasteSequence = this.consumeClipboardImagePasteSequence();
106
+ if (clipboardImagePasteSequence === "consumed")
107
+ continue;
108
+ if (clipboardImagePasteSequence === "pending")
109
+ return;
75
110
  const terminalEditShortcutSequence = this.consumeTerminalEditShortcutSequence();
76
111
  if (terminalEditShortcutSequence === "consumed")
77
112
  continue;
78
113
  if (terminalEditShortcutSequence === "pending")
79
114
  return;
115
+ const modifiedArrowKeySequence = this.consumeModifiedArrowKeySequence();
116
+ if (modifiedArrowKeySequence === "consumed")
117
+ continue;
118
+ if (modifiedArrowKeySequence === "pending")
119
+ return;
120
+ const ignoredModifiedKeySequence = this.consumeIgnoredModifiedKeySequence();
121
+ if (ignoredModifiedKeySequence === "consumed")
122
+ continue;
123
+ if (ignoredModifiedKeySequence === "pending")
124
+ return;
80
125
  if (this.consumeEscapeSequence())
81
126
  continue;
82
127
  if (this.isPendingEscapeSequence())
@@ -106,13 +151,9 @@ export class AppInputController {
106
151
  }
107
152
  getEscapeSequences() {
108
153
  return [
109
- ["\x1b[13;2u", () => this.insertInputNewline()],
110
- ["\x1b[13;2~", () => this.insertInputNewline()],
111
- ["\x1b[27;2;13~", () => this.insertInputNewline()],
154
+ ...SHIFT_ENTER_ESCAPE_SEQUENCES.map((sequence) => [sequence, () => this.insertInputNewline()]),
112
155
  ["\x1b[13u", () => this.host.handleEnter()],
113
156
  ["\x1b[13;1u", () => this.host.handleEnter()],
114
- ["\x1b\r", () => this.insertInputNewline()],
115
- ["\x1b\n", () => this.insertInputNewline()],
116
157
  ["\x1b[5~", () => this.host.scrollByPage(-1)],
117
158
  ["\x1b[6~", () => this.host.scrollByPage(1)],
118
159
  ["\x1b[A", () => this.handleArrowUp()],
@@ -139,8 +180,6 @@ export class AppInputController {
139
180
  ["\x1b[201~", () => this.pasteHandler.endBracketedPaste()],
140
181
  ["\x1b[1;2H", () => { this.host.inputEditor.moveToLineStartExtend(); this.host.render(); }],
141
182
  ["\x1b[1;2F", () => { this.host.inputEditor.moveToLineEndExtend(); this.host.render(); }],
142
- ["\x1b[118;5u", () => { void this.pasteHandler.handleClipboardImagePaste(); }],
143
- ["\x1b[27;5;118~", () => { void this.pasteHandler.handleClipboardImagePaste(); }],
144
183
  ["\x1b[122;9u", () => this.undoInput()],
145
184
  ["\x1b[27;9;122~", () => this.undoInput()],
146
185
  ["\x1b[90;10u", () => this.redoInput()],
@@ -232,6 +271,65 @@ export class AppInputController {
232
271
  }
233
272
  return "consumed";
234
273
  }
274
+ consumeIgnoredModifiedKeySequence() {
275
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
276
+ if (result.kind === "pending")
277
+ return "pending";
278
+ if (result.kind === "none")
279
+ return "none";
280
+ if (!terminalKeyShouldIgnore(result.key))
281
+ return "none";
282
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
283
+ return "consumed";
284
+ }
285
+ consumeModifiedArrowKeySequence() {
286
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
287
+ if (result.kind === "pending")
288
+ return "pending";
289
+ if (result.kind === "none")
290
+ return "none";
291
+ const direction = terminalKeyArrowDirection(result.key);
292
+ if (!direction)
293
+ return "none";
294
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
295
+ if (terminalKeyShouldIgnore(result.key))
296
+ return "consumed";
297
+ if (direction === "up")
298
+ this.handleArrowUp();
299
+ else if (direction === "down")
300
+ this.handleArrowDown();
301
+ else if (direction === "right")
302
+ this.handleArrowRight();
303
+ else
304
+ this.handleArrowLeft();
305
+ return "consumed";
306
+ }
307
+ consumeClipboardImagePasteSequence() {
308
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
309
+ if (result.kind === "pending")
310
+ return "pending";
311
+ if (result.kind === "none")
312
+ return "none";
313
+ if (!terminalKeyIsClipboardImagePaste(result.key))
314
+ return "none";
315
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
316
+ if (!terminalKeyShouldIgnore(result.key))
317
+ void this.pasteHandler.handleClipboardImagePaste();
318
+ return "consumed";
319
+ }
320
+ consumeShiftEnterSequence() {
321
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
322
+ if (result.kind === "pending")
323
+ return "pending";
324
+ if (result.kind === "none")
325
+ return "none";
326
+ if (!terminalKeyIsShiftEnter(result.key))
327
+ return "none";
328
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
329
+ if (!terminalKeyShouldIgnore(result.key))
330
+ this.insertInputNewline();
331
+ return "consumed";
332
+ }
235
333
  consumeTerminalInterruptSequence() {
236
334
  const result = parseTerminalInterruptSequence(this.inputBuffer);
237
335
  if (result.kind === "pending")
@@ -401,7 +499,15 @@ export class AppInputController {
401
499
  this.host.toggleVoiceRecording();
402
500
  return;
403
501
  }
404
- if (char === "\r" || char === "\n") {
502
+ if (char === "\n") {
503
+ if (this.host.inputEditor.isInBracketedPaste) {
504
+ this.pasteHandler.appendBracketedPasteText("\n");
505
+ return;
506
+ }
507
+ this.insertInputNewline();
508
+ return;
509
+ }
510
+ if (char === "\r") {
405
511
  if (this.host.inputEditor.isInBracketedPaste) {
406
512
  this.pasteHandler.appendBracketedPasteText("\n");
407
513
  return;
@@ -92,8 +92,10 @@ export class InputPasteHandler {
92
92
  return false;
93
93
  }
94
94
  handlePasteEnd(text) {
95
- if (!text)
95
+ if (!text) {
96
+ void this.handleClipboardImagePaste();
96
97
  return;
98
+ }
97
99
  const filePath = this.plainPasteFilePath(text);
98
100
  if (filePath) {
99
101
  if (isImagePath(filePath) && Date.now() < this.suppressImagePathPasteUntil) {
@@ -1,4 +1,12 @@
1
1
  export type TerminalEditShortcut = "undo" | "redo";
2
+ export type ParsedTerminalModifiedKeyResult = {
3
+ readonly kind: "key";
4
+ readonly key: ParsedModifiedKey;
5
+ } | {
6
+ readonly kind: "pending";
7
+ } | {
8
+ readonly kind: "none";
9
+ };
2
10
  export type TerminalEditShortcutSequenceResult = {
3
11
  readonly kind: "shortcut";
4
12
  readonly shortcut: TerminalEditShortcut;
@@ -11,6 +19,14 @@ export type TerminalEditShortcutSequenceResult = {
11
19
  } | {
12
20
  readonly kind: "none";
13
21
  };
22
+ interface ParsedModifiedKey {
23
+ readonly codepoint: number;
24
+ readonly baseLayoutKey: number | undefined;
25
+ readonly modifier: number;
26
+ readonly eventType: number | undefined;
27
+ readonly length: number;
28
+ }
29
+ export declare function parseTerminalModifiedKeySequence(input: string): ParsedTerminalModifiedKeyResult;
14
30
  export declare function parseTerminalEditShortcutSequence(input: string): TerminalEditShortcutSequenceResult;
15
31
  export declare function parseTerminalInterruptSequence(input: string): {
16
32
  readonly kind: "interrupt";
@@ -20,4 +36,9 @@ export declare function parseTerminalInterruptSequence(input: string): {
20
36
  } | {
21
37
  readonly kind: "none";
22
38
  };
39
+ export declare function terminalKeyIsShiftEnter(key: ParsedModifiedKey): boolean;
40
+ export declare function terminalKeyIsClipboardImagePaste(key: ParsedModifiedKey): boolean;
41
+ export declare function terminalKeyShouldIgnore(key: ParsedModifiedKey): boolean;
42
+ export declare function terminalKeyArrowDirection(key: ParsedModifiedKey): "up" | "down" | "right" | "left" | undefined;
23
43
  export declare function terminalEditShortcutForControlChar(char: string, shiftPressed: boolean): TerminalEditShortcut | undefined;
44
+ export {};