pi-ui-extend 0.1.34 → 0.1.35

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 (58) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.js +4 -2
  3. package/dist/app/constants.d.ts +2 -1
  4. package/dist/app/constants.js +6 -1
  5. package/dist/app/input/input-controller.d.ts +4 -1
  6. package/dist/app/input/input-controller.js +95 -16
  7. package/dist/app/input/input-paste-handler.js +3 -1
  8. package/dist/app/input/terminal-edit-shortcuts.d.ts +20 -0
  9. package/dist/app/input/terminal-edit-shortcuts.js +50 -16
  10. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  11. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  12. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  13. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  14. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  15. package/dist/app/rendering/conversation-viewport.js +41 -5
  16. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  17. package/dist/app/rendering/editor-panels.js +27 -10
  18. package/dist/app/runtime.d.ts +1 -0
  19. package/dist/app/runtime.js +33 -14
  20. package/dist/app/session/session-event-controller.d.ts +7 -0
  21. package/dist/app/session/session-event-controller.js +78 -0
  22. package/dist/app/session/tabs-controller.js +3 -1
  23. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  24. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  25. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  26. package/dist/app/terminal/terminal-controller.js +91 -2
  27. package/dist/app/todo/todo-model.js +2 -0
  28. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  29. package/dist/app/todo/todo-widget-controller.js +17 -7
  30. package/dist/app/types.d.ts +4 -0
  31. package/dist/bundled-extensions/question/tui.js +8 -1
  32. package/dist/bundled-extensions/session-title/index.js +65 -14
  33. package/dist/input-editor-files.js +23 -4
  34. package/dist/markdown-format.d.ts +4 -1
  35. package/dist/markdown-format.js +76 -9
  36. package/external/pi-tools-suite/README.md +71 -1
  37. package/external/pi-tools-suite/package.json +3 -3
  38. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  39. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  40. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  41. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  42. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  43. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  44. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  45. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  46. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  47. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  48. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  49. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  50. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  51. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  52. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  53. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  54. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  55. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  56. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  57. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  58. 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.js CHANGED
@@ -166,7 +166,7 @@ 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,
@@ -334,6 +334,7 @@ export class PiUiExtendApp {
334
334
  });
335
335
  this.todoWidgetController = new AppTodoWidgetController({
336
336
  sessionFile: () => this.runtime?.session.sessionFile,
337
+ sessionId: () => this.runtime?.session.sessionId,
337
338
  isRunning: () => this.running,
338
339
  render: () => this.scheduleRender(),
339
340
  });
@@ -761,10 +762,11 @@ export class PiUiExtendApp {
761
762
  });
762
763
  this.slashCommands = this.commandController.slashCommands;
763
764
  }
764
- createRuntime(options) {
765
+ createRuntime(options, runtimeOptions = {}) {
765
766
  return createPixRuntime(options, {
766
767
  eventBus: this.createExtensionEventBus(),
767
768
  config: this.pixConfig,
769
+ ...runtimeOptions,
768
770
  });
769
771
  }
770
772
  async loadStartupConfig() {
@@ -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";
@@ -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,9 @@ export declare class AppInputController {
45
45
  private consumeCommandArrowPageSequence;
46
46
  private consumeCommandArrowPageMatch;
47
47
  private consumeTerminalEditShortcutSequence;
48
+ private consumeIgnoredModifiedKeySequence;
49
+ private consumeClipboardImagePasteSequence;
50
+ private consumeShiftEnterSequence;
48
51
  private consumeTerminalInterruptSequence;
49
52
  private handleArrowUp;
50
53
  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, 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,26 @@ 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 ignoredModifiedKeySequence = this.consumeIgnoredModifiedKeySequence();
116
+ if (ignoredModifiedKeySequence === "consumed")
117
+ continue;
118
+ if (ignoredModifiedKeySequence === "pending")
119
+ return;
80
120
  if (this.consumeEscapeSequence())
81
121
  continue;
82
122
  if (this.isPendingEscapeSequence())
@@ -106,13 +146,9 @@ export class AppInputController {
106
146
  }
107
147
  getEscapeSequences() {
108
148
  return [
109
- ["\x1b[13;2u", () => this.insertInputNewline()],
110
- ["\x1b[13;2~", () => this.insertInputNewline()],
111
- ["\x1b[27;2;13~", () => this.insertInputNewline()],
149
+ ...SHIFT_ENTER_ESCAPE_SEQUENCES.map((sequence) => [sequence, () => this.insertInputNewline()]),
112
150
  ["\x1b[13u", () => this.host.handleEnter()],
113
151
  ["\x1b[13;1u", () => this.host.handleEnter()],
114
- ["\x1b\r", () => this.insertInputNewline()],
115
- ["\x1b\n", () => this.insertInputNewline()],
116
152
  ["\x1b[5~", () => this.host.scrollByPage(-1)],
117
153
  ["\x1b[6~", () => this.host.scrollByPage(1)],
118
154
  ["\x1b[A", () => this.handleArrowUp()],
@@ -139,8 +175,6 @@ export class AppInputController {
139
175
  ["\x1b[201~", () => this.pasteHandler.endBracketedPaste()],
140
176
  ["\x1b[1;2H", () => { this.host.inputEditor.moveToLineStartExtend(); this.host.render(); }],
141
177
  ["\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
178
  ["\x1b[122;9u", () => this.undoInput()],
145
179
  ["\x1b[27;9;122~", () => this.undoInput()],
146
180
  ["\x1b[90;10u", () => this.redoInput()],
@@ -232,6 +266,43 @@ export class AppInputController {
232
266
  }
233
267
  return "consumed";
234
268
  }
269
+ consumeIgnoredModifiedKeySequence() {
270
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
271
+ if (result.kind === "pending")
272
+ return "pending";
273
+ if (result.kind === "none")
274
+ return "none";
275
+ if (!terminalKeyShouldIgnore(result.key))
276
+ return "none";
277
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
278
+ return "consumed";
279
+ }
280
+ consumeClipboardImagePasteSequence() {
281
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
282
+ if (result.kind === "pending")
283
+ return "pending";
284
+ if (result.kind === "none")
285
+ return "none";
286
+ if (!terminalKeyIsClipboardImagePaste(result.key))
287
+ return "none";
288
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
289
+ if (!terminalKeyShouldIgnore(result.key))
290
+ void this.pasteHandler.handleClipboardImagePaste();
291
+ return "consumed";
292
+ }
293
+ consumeShiftEnterSequence() {
294
+ const result = parseTerminalModifiedKeySequence(this.inputBuffer);
295
+ if (result.kind === "pending")
296
+ return "pending";
297
+ if (result.kind === "none")
298
+ return "none";
299
+ if (!terminalKeyIsShiftEnter(result.key))
300
+ return "none";
301
+ this.inputBuffer = this.inputBuffer.slice(result.key.length);
302
+ if (!terminalKeyShouldIgnore(result.key))
303
+ this.insertInputNewline();
304
+ return "consumed";
305
+ }
235
306
  consumeTerminalInterruptSequence() {
236
307
  const result = parseTerminalInterruptSequence(this.inputBuffer);
237
308
  if (result.kind === "pending")
@@ -401,7 +472,15 @@ export class AppInputController {
401
472
  this.host.toggleVoiceRecording();
402
473
  return;
403
474
  }
404
- if (char === "\r" || char === "\n") {
475
+ if (char === "\n") {
476
+ if (this.host.inputEditor.isInBracketedPaste) {
477
+ this.pasteHandler.appendBracketedPasteText("\n");
478
+ return;
479
+ }
480
+ this.insertInputNewline();
481
+ return;
482
+ }
483
+ if (char === "\r") {
405
484
  if (this.host.inputEditor.isInBracketedPaste) {
406
485
  this.pasteHandler.appendBracketedPasteText("\n");
407
486
  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,8 @@ 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;
23
42
  export declare function terminalEditShortcutForControlChar(char: string, shiftPressed: boolean): TerminalEditShortcut | undefined;
43
+ export {};
@@ -3,34 +3,60 @@ const CONTROL_MODIFIER_FLAG = 4;
3
3
  const COMMAND_MODIFIER_FLAG = 8;
4
4
  const LOCK_MODIFIER_MASK = 64 + 128;
5
5
  const KEY_CODE_C = 99;
6
+ const KEY_CODE_ENTER = 13;
7
+ const KEY_CODE_V = 118;
6
8
  const KEY_CODE_Y = 121;
7
9
  const KEY_CODE_Z = 122;
8
10
  const CYRILLIC_SMALL_ES_CODE = 1089;
9
11
  const CYRILLIC_CAPITAL_ES_CODE = 1057;
12
+ const CYRILLIC_SMALL_EM_CODE = 1084;
13
+ const CYRILLIC_CAPITAL_EM_CODE = 1052;
10
14
  const KITTY_CSI_U_SEQUENCE = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u/;
11
15
  const XTERM_MODIFY_OTHER_KEYS_SEQUENCE = /^\x1b\[27;(\d+);(\d+)~/;
12
- export function parseTerminalEditShortcutSequence(input) {
16
+ export function parseTerminalModifiedKeySequence(input) {
13
17
  const kitty = parseKittyCsiUSequence(input);
14
18
  if (kitty)
15
- return terminalEditShortcutResult(kitty);
19
+ return { kind: "key", key: kitty };
16
20
  const xterm = parseXtermModifyOtherKeysSequence(input);
17
21
  if (xterm)
18
- return terminalEditShortcutResult(xterm);
19
- if (isPotentialEditShortcutPrefix(input))
22
+ return { kind: "key", key: xterm };
23
+ if (isPotentialModifiedKeyPrefix(input))
20
24
  return { kind: "pending" };
21
25
  return { kind: "none" };
22
26
  }
27
+ export function parseTerminalEditShortcutSequence(input) {
28
+ const result = parseTerminalModifiedKeySequence(input);
29
+ if (result.kind === "pending")
30
+ return { kind: "pending" };
31
+ if (result.kind === "none")
32
+ return { kind: "none" };
33
+ return terminalEditShortcutResult(result.key);
34
+ }
23
35
  export function parseTerminalInterruptSequence(input) {
24
- const kitty = parseKittyCsiUSequence(input);
25
- if (kitty && terminalKeyIsControlC(kitty))
26
- return { kind: "interrupt", length: kitty.length };
27
- const xterm = parseXtermModifyOtherKeysSequence(input);
28
- if (xterm && terminalKeyIsControlC(xterm))
29
- return { kind: "interrupt", length: xterm.length };
30
- if (isPotentialInterruptPrefix(input))
36
+ const result = parseTerminalModifiedKeySequence(input);
37
+ if (result.kind === "pending")
31
38
  return { kind: "pending" };
39
+ if (result.kind === "key" && terminalKeyIsControlC(result.key))
40
+ return { kind: "interrupt", length: result.key.length };
41
+ if (result.kind === "key" || isPotentialInterruptPrefix(input))
42
+ return { kind: "none" };
32
43
  return { kind: "none" };
33
44
  }
45
+ export function terminalKeyIsShiftEnter(key) {
46
+ const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
47
+ if ((effectiveModifier & SHIFT_MODIFIER_FLAG) === 0)
48
+ return false;
49
+ return terminalKeyMatchesCodepoint(key, KEY_CODE_ENTER);
50
+ }
51
+ export function terminalKeyIsClipboardImagePaste(key) {
52
+ const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
53
+ if ((effectiveModifier & (CONTROL_MODIFIER_FLAG | COMMAND_MODIFIER_FLAG)) === 0)
54
+ return false;
55
+ return terminalKeyMatchesCodepoint(key, KEY_CODE_V, CYRILLIC_SMALL_EM_CODE, CYRILLIC_CAPITAL_EM_CODE);
56
+ }
57
+ export function terminalKeyShouldIgnore(key) {
58
+ return key.eventType === 3;
59
+ }
34
60
  export function terminalEditShortcutForControlChar(char, shiftPressed) {
35
61
  if (char === "\u001a")
36
62
  return shiftPressed ? "redo" : "undo";
@@ -95,6 +121,17 @@ function interruptCodepointIsC(codepoint) {
95
121
  const normalized = normalizeLetterCodepoint(codepoint);
96
122
  return normalized === KEY_CODE_C || normalized === CYRILLIC_SMALL_ES_CODE || normalized === CYRILLIC_CAPITAL_ES_CODE;
97
123
  }
124
+ function terminalKeyMatchesCodepoint(key, ...expectedCodepoints) {
125
+ return expectedCodepoints.some((codepoint) => keyMatchesCodepoint(key, codepoint));
126
+ }
127
+ function keyMatchesCodepoint(key, expectedCodepoint) {
128
+ const normalizedExpected = normalizeLetterCodepoint(expectedCodepoint);
129
+ const primary = normalizeLetterCodepoint(key.codepoint);
130
+ if (primary === normalizedExpected)
131
+ return true;
132
+ const baseLayout = key.baseLayoutKey;
133
+ return baseLayout !== undefined && normalizeLetterCodepoint(baseLayout) === normalizedExpected;
134
+ }
98
135
  function terminalEditShortcutForKey(key, effectiveModifier) {
99
136
  const codepoint = editShortcutCodepoint(key);
100
137
  if (codepoint === KEY_CODE_Y)
@@ -114,16 +151,13 @@ function normalizeLetterCodepoint(codepoint) {
114
151
  return codepoint + 32;
115
152
  return codepoint;
116
153
  }
117
- function isPotentialEditShortcutPrefix(input) {
154
+ function isPotentialModifiedKeyPrefix(input) {
118
155
  if (!input.startsWith("\x1b["))
119
156
  return false;
120
157
  if (input.includes("u") || input.includes("~"))
121
158
  return false;
122
159
  const body = input.slice(2);
123
- if (!/^[\d:;]*$/.test(body))
124
- return false;
125
- const possibleStarts = ["122", "121", "90", "27;"];
126
- return possibleStarts.some((start) => start.startsWith(body) || body.startsWith(start));
160
+ return /^[\d:;]*$/.test(body);
127
161
  }
128
162
  function isPotentialInterruptPrefix(input) {
129
163
  if (!input.startsWith("\x1b["))
@@ -14,6 +14,7 @@ export type ConversationEntryRenderOptions = {
14
14
  availableThinkingLevels?: readonly string[];
15
15
  superCompactTools?: boolean;
16
16
  allThinkingExpanded?: boolean;
17
+ currentTimeMs?: number;
17
18
  renderInlineUserMessageMenu: (entry: Extract<Entry, {
18
19
  kind: "user";
19
20
  }>, context: InlineUserMessageMenuContext) => RenderedLine[];
@@ -75,7 +75,7 @@ function renderAssistantLines(text, width, options) {
75
75
  if (!displayText)
76
76
  return [];
77
77
  const { left: contentLeft, contentWidth } = horizontalPaddingLayout(width);
78
- const contentLines = renderMarkdownTextLines(displayText, contentWidth, contentLeft);
78
+ const contentLines = renderMarkdownTextLines(displayText, contentWidth, contentLeft, { preserveWrappedWordSeparator: true });
79
79
  if (contentLines.length === 0)
80
80
  return [];
81
81
  const lines = [];
@@ -8,6 +8,7 @@ export type ConversationToolRenderOptions = {
8
8
  availableThinkingLevels?: readonly string[];
9
9
  superCompactTools?: boolean;
10
10
  allThinkingExpanded?: boolean;
11
+ currentTimeMs?: number;
11
12
  };
12
13
  export declare function renderConversationToolEntry(entry: Extract<Entry, {
13
14
  kind: "tool";
@@ -56,9 +56,11 @@ export function renderThinkingEntry(entry, width, options) {
56
56
  const headerColorOverride = entry.level
57
57
  ? thinkingLevelThemeColor(entry.level, options.colors, options.availableThinkingLevels)
58
58
  : undefined;
59
+ const elapsed = thinkingElapsedText(entry, options.currentTimeMs ?? Date.now());
59
60
  return renderToolBlock({
60
61
  id: entry.id,
61
62
  toolName: THINKING_TOOL_NAME,
63
+ ...(elapsed === undefined ? {} : { headerArgs: elapsed }),
62
64
  expanded,
63
65
  status: entry.status,
64
66
  isError: false,
@@ -74,6 +76,25 @@ export function renderThinkingEntry(entry, width, options) {
74
76
  ...(headerColorOverride === undefined ? {} : { headerColorOverride }),
75
77
  });
76
78
  }
79
+ function thinkingElapsedText(entry, currentTimeMs) {
80
+ if (entry.startedAt === undefined)
81
+ return undefined;
82
+ const endTimeMs = entry.finishedAt ?? currentTimeMs;
83
+ const elapsedMs = Math.max(0, endTimeMs - entry.startedAt);
84
+ return formatThinkingElapsed(elapsedMs);
85
+ }
86
+ function formatThinkingElapsed(elapsedMs) {
87
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
88
+ const seconds = totalSeconds % 60;
89
+ const totalMinutes = Math.floor(totalSeconds / 60);
90
+ if (totalMinutes < 1)
91
+ return `${seconds}s`;
92
+ const minutes = totalMinutes % 60;
93
+ const hours = Math.floor(totalMinutes / 60);
94
+ if (hours < 1)
95
+ return `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
96
+ return `${hours}h ${minutes.toString().padStart(2, "0")}m`;
97
+ }
77
98
  function trimTrailingBlankLines(text) {
78
99
  return text.replace(/(?:\r?\n[ \t]*)+$/u, "");
79
100
  }
@@ -49,11 +49,14 @@ export declare class ConversationViewport {
49
49
  private blockCacheForWidth;
50
50
  private refreshDynamicLayoutEntries;
51
51
  private ensureEntryMeasured;
52
+ private hasDynamicConversationBlock;
53
+ private isDynamicConversationBlock;
52
54
  private refreshLayoutEntry;
53
55
  private measuredLineCountForEntry;
54
56
  private estimatedLineCountForEntry;
55
57
  private lineCountWithGap;
56
58
  private estimatedBlockLineCountForEntry;
59
+ private estimatedToolEntryLineCount;
57
60
  private nextVisibleEntry;
58
61
  private nextEstimatedVisibleEntry;
59
62
  private gapAfterEntry;
@@ -1,4 +1,5 @@
1
1
  import { resolveToolRule } from "../../config.js";
2
+ import { renderToolDisplay } from "../../tool-renderers/index.js";
2
3
  import { stringDisplayWidth } from "../../terminal-width.js";
3
4
  import { renderConversationEntry as renderConversationEntryLines } from "./conversation-entry-renderer.js";
4
5
  import { horizontalPaddingLayout } from "./render-text.js";
@@ -70,7 +71,7 @@ export class ConversationViewport {
70
71
  + (this.host.superCompactTools ? 1_000_000_000 : 0)
71
72
  + (this.host.allThinkingExpanded ? 2_000_000_000 : 0);
72
73
  const cached = blockCache.get(entry.id);
73
- const dynamic = this.host.isDynamicConversationBlock(entry);
74
+ const dynamic = this.isDynamicConversationBlock(entry);
74
75
  if (!dynamic && cached?.version === version)
75
76
  return cached;
76
77
  const availableThinkingLevels = this.host.availableThinkingLevels?.();
@@ -82,6 +83,7 @@ export class ConversationViewport {
82
83
  ...(availableThinkingLevels ? { availableThinkingLevels } : {}),
83
84
  superCompactTools: Boolean(this.host.superCompactTools),
84
85
  allThinkingExpanded: Boolean(this.host.allThinkingExpanded),
86
+ currentTimeMs: Date.now(),
85
87
  renderInlineUserMessageMenu: (userEntry, context) => this.host.renderInlineUserMessageMenu(userEntry, context),
86
88
  });
87
89
  const block = {
@@ -156,7 +158,7 @@ export class ConversationViewport {
156
158
  }
157
159
  }
158
160
  this.refreshDirtyLayoutEntries(layout, width);
159
- if (this.host.hasDynamicConversationBlock?.()) {
161
+ if (this.hasDynamicConversationBlock(layout.entries)) {
160
162
  this.refreshDynamicLayoutEntries(layout, width);
161
163
  }
162
164
  return layout;
@@ -278,7 +280,7 @@ export class ConversationViewport {
278
280
  }
279
281
  refreshDynamicLayoutEntries(layout, width) {
280
282
  for (let index = 0; index < layout.entries.length; index += 1) {
281
- if (this.host.isDynamicConversationBlock(layout.entries[index]))
283
+ if (this.isDynamicConversationBlock(layout.entries[index]))
282
284
  this.refreshLayoutEntry(layout, width, index, true);
283
285
  }
284
286
  }
@@ -286,10 +288,17 @@ export class ConversationViewport {
286
288
  const entry = layout.entries[index];
287
289
  if (!entry)
288
290
  return false;
289
- if (layout.measuredLineCounts[index] === true && !this.host.isDynamicConversationBlock(entry))
291
+ if (layout.measuredLineCounts[index] === true && !this.isDynamicConversationBlock(entry))
290
292
  return false;
291
293
  return this.refreshLayoutEntry(layout, width, index, true);
292
294
  }
295
+ hasDynamicConversationBlock(entries) {
296
+ return this.host.hasDynamicConversationBlock?.() === true || entries.some((entry) => this.isDynamicConversationBlock(entry));
297
+ }
298
+ isDynamicConversationBlock(entry) {
299
+ return (entry.kind === "thinking" && entry.status === "running" && entry.startedAt !== undefined)
300
+ || this.host.isDynamicConversationBlock(entry);
301
+ }
293
302
  refreshLayoutEntry(layout, width, index, measure) {
294
303
  const entry = layout.entries[index];
295
304
  if (!entry)
@@ -348,11 +357,38 @@ export class ConversationViewport {
348
357
  case "shell":
349
358
  return estimateToolLikeLineCount("shell", entry.expanded, `${entry.output}\n${entry.status}`, width, this.host.pixConfig, this.host.superCompactTools === true, true);
350
359
  case "tool":
351
- return estimateToolLikeLineCount(entry.toolName, entry.expanded, entry.output, width, this.host.pixConfig, this.host.superCompactTools === true, false);
360
+ return this.estimatedToolEntryLineCount(entry, width);
352
361
  default:
353
362
  return 1;
354
363
  }
355
364
  }
365
+ estimatedToolEntryLineCount(entry, width) {
366
+ const display = renderToolDisplay({
367
+ toolName: entry.toolName,
368
+ argsText: entry.argsText,
369
+ output: entry.output,
370
+ details: entry.details,
371
+ isError: entry.isError,
372
+ status: entry.status,
373
+ cwd: this.host.cwd,
374
+ colors: this.host.colors,
375
+ });
376
+ const toolName = display.toolName ?? entry.toolName;
377
+ const rule = resolveToolRule(toolName, this.host.pixConfig.toolRenderer);
378
+ if (rule.hidden)
379
+ return 0;
380
+ const bodyWidth = Math.max(1, width - 2);
381
+ if (entry.expanded)
382
+ return 1 + estimateWrappedLineCount(display.expandedText, bodyWidth);
383
+ if (rule.compactHidden || (rule.defaultExpanded === true && this.host.superCompactTools !== true))
384
+ return 1;
385
+ const body = display.collapsedBody.trimEnd();
386
+ if (!body || rule.previewLines === 0)
387
+ return 1;
388
+ const bodyLineCount = estimateWrappedLineCount(body, bodyWidth);
389
+ const previewLineCount = Math.min(rule.previewLines, bodyLineCount);
390
+ return this.host.superCompactTools === true ? 1 : 1 + previewLineCount;
391
+ }
356
392
  nextVisibleEntry(entries, index, width) {
357
393
  for (let nextIndex = index + 1; nextIndex < entries.length; nextIndex += 1) {
358
394
  const nextEntry = entries[nextIndex];
@@ -1,4 +1,4 @@
1
- import { ABOVE_EDITOR_WIDGET_KEY_GROUPS, BUILT_IN_SUBAGENTS_WIDGET_KEYS, INPUT_MAX_ROWS, LEGACY_TODO_WIDGET_KEYS, } from "../constants.js";
1
+ import { ABOVE_EDITOR_WIDGET_KEY_GROUPS, BUILT_IN_SUBAGENTS_WIDGET_KEYS, LEGACY_TODO_WIDGET_KEYS, } from "../constants.js";
2
2
  import { renderSubagentsPanel, renderTodoPanel } from "./editor-panels.js";
3
3
  import { ellipsizeDisplay, horizontalPaddingLayout, padHorizontalText, sanitizeText, wrapText } from "./render-text.js";
4
4
  import { APP_ICONS } from "../icons.js";
@@ -9,7 +9,8 @@ export class EditorLayoutRenderer {
9
9
  }
10
10
  computeLayout(width, rows) {
11
11
  const maxAvailableInputRows = Math.max(1, rows - 4);
12
- const renderedInput = this.renderInput(width, Math.min(INPUT_MAX_ROWS, maxAvailableInputRows), maxAvailableInputRows);
12
+ const maxComposerRows = Math.max(1, Math.min(maxAvailableInputRows, Math.floor(rows * 0.7)));
13
+ const renderedInput = this.renderInput(width, maxComposerRows, maxComposerRows);
13
14
  const maxEntityRows = Math.max(0, rows - renderedInput.lines.length - 4);
14
15
  const editorEntityWidth = inputFrameContentWidth(width);
15
16
  const aboveEditorEntities = this.renderAboveEditorEntities(editorEntityWidth);