@mariozechner/pi-coding-agent 0.32.3 → 0.33.0
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.
- package/CHANGELOG.md +16 -0
- package/README.md +76 -2
- package/dist/core/export-html/template.css +34 -4
- package/dist/core/export-html/template.js +17 -4
- package/dist/core/keybindings.d.ts +59 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +149 -0
- package/dist/core/keybindings.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts +11 -12
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +48 -72
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-editor.js +5 -4
- package/dist/modes/interactive/components/hook-editor.js.map +1 -1
- package/dist/modes/interactive/components/hook-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-input.js +4 -3
- package/dist/modes/interactive/components/hook-input.js.map +1 -1
- package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-selector.js +6 -5
- package/dist/modes/interactive/components/hook-selector.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +6 -5
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +6 -5
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +6 -9
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +14 -15
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +6 -11
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +21 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +175 -45
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/tui.md +18 -15
- package/examples/custom-tools/subagent/README.md +2 -2
- package/examples/hooks/snake.ts +7 -7
- package/examples/hooks/todo/index.ts +2 -2
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.33.0] - 2026-01-04
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- **Key detection functions removed from `@mariozechner/pi-tui`**: All `isXxx()` key detection functions (`isEnter()`, `isEscape()`, `isCtrlC()`, etc.) have been removed. Use `matchesKey(data, keyId)` instead (e.g., `matchesKey(data, "enter")`, `matchesKey(data, "ctrl+c")`). This affects hooks and custom tools that use `ctx.ui.custom()` with keyboard input handling. ([#405](https://github.com/badlogic/pi-mono/pull/405))
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Clipboard image paste support via `Ctrl+V`. Images are saved to a temp file and attached to the message. Works on macOS, Windows, and Linux (X11). ([#419](https://github.com/badlogic/pi-mono/issues/419))
|
|
12
|
+
- Configurable keybindings via `~/.pi/agent/keybindings.json`. All keyboard shortcuts (editor navigation, deletion, app actions like model cycling, etc.) can now be customized. Supports multiple bindings per action. ([#405](https://github.com/badlogic/pi-mono/pull/405) by [@hjanuschka](https://github.com/hjanuschka))
|
|
13
|
+
- `/quit` and `/exit` slash commands to gracefully exit the application. Unlike double Ctrl+C, these properly await hook and custom tool cleanup handlers before exiting. ([#426](https://github.com/badlogic/pi-mono/pull/426) by [@ben-vargas](https://github.com/ben-vargas))
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Subagent example README referenced incorrect filename `subagent.ts` instead of `index.ts` ([#427](https://github.com/badlogic/pi-mono/pull/427) by [@Whamp](https://github.com/Whamp))
|
|
18
|
+
|
|
3
19
|
## [0.32.3] - 2026-01-03
|
|
4
20
|
|
|
5
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ Works on Linux, macOS, and Windows (requires bash; see [Windows Setup](#windows-
|
|
|
24
24
|
- [Slash Commands](#slash-commands)
|
|
25
25
|
- [Editor Features](#editor-features)
|
|
26
26
|
- [Keyboard Shortcuts](#keyboard-shortcuts)
|
|
27
|
+
- [Custom Keybindings](#custom-keybindings)
|
|
27
28
|
- [Bash Mode](#bash-mode)
|
|
28
29
|
- [Image Support](#image-support)
|
|
29
30
|
- [Sessions](#sessions)
|
|
@@ -265,6 +266,77 @@ Both modes are configurable via `/settings`: "one-at-a-time" delivers messages o
|
|
|
265
266
|
| Ctrl+O | Toggle tool output expansion |
|
|
266
267
|
| Ctrl+T | Toggle thinking block visibility |
|
|
267
268
|
| Ctrl+G | Edit message in external editor (`$VISUAL` or `$EDITOR`) |
|
|
269
|
+
| Ctrl+V | Paste image from clipboard |
|
|
270
|
+
|
|
271
|
+
### Custom Keybindings
|
|
272
|
+
|
|
273
|
+
All keyboard shortcuts can be customized via `~/.pi/agent/keybindings.json`. Each action can be bound to one or more keys.
|
|
274
|
+
|
|
275
|
+
**Key format:** `modifier+key` where modifiers are `ctrl`, `shift`, `alt` and keys are `a-z`, `0-9`, `escape`, `tab`, `enter`, `space`, `backspace`, `delete`, `home`, `end`, `up`, `down`, `left`, `right`.
|
|
276
|
+
|
|
277
|
+
**Configurable actions:**
|
|
278
|
+
|
|
279
|
+
| Action | Default | Description |
|
|
280
|
+
|--------|---------|-------------|
|
|
281
|
+
| `cursorUp` | `up` | Move cursor up |
|
|
282
|
+
| `cursorDown` | `down` | Move cursor down |
|
|
283
|
+
| `cursorLeft` | `left` | Move cursor left |
|
|
284
|
+
| `cursorRight` | `right` | Move cursor right |
|
|
285
|
+
| `cursorWordLeft` | `alt+left`, `ctrl+left` | Move cursor word left |
|
|
286
|
+
| `cursorWordRight` | `alt+right`, `ctrl+right` | Move cursor word right |
|
|
287
|
+
| `cursorLineStart` | `home`, `ctrl+a` | Move to line start |
|
|
288
|
+
| `cursorLineEnd` | `end`, `ctrl+e` | Move to line end |
|
|
289
|
+
| `deleteCharBackward` | `backspace` | Delete char backward |
|
|
290
|
+
| `deleteCharForward` | `delete` | Delete char forward |
|
|
291
|
+
| `deleteWordBackward` | `ctrl+w`, `alt+backspace` | Delete word backward |
|
|
292
|
+
| `deleteToLineStart` | `ctrl+u` | Delete to line start |
|
|
293
|
+
| `deleteToLineEnd` | `ctrl+k` | Delete to line end |
|
|
294
|
+
| `newLine` | `shift+enter`, `alt+enter` | Insert new line |
|
|
295
|
+
| `submit` | `enter` | Submit input |
|
|
296
|
+
| `tab` | `tab` | Tab/autocomplete |
|
|
297
|
+
| `interrupt` | `escape` | Interrupt operation |
|
|
298
|
+
| `clear` | `ctrl+c` | Clear editor |
|
|
299
|
+
| `exit` | `ctrl+d` | Exit (when empty) |
|
|
300
|
+
| `suspend` | `ctrl+z` | Suspend process |
|
|
301
|
+
| `cycleThinkingLevel` | `shift+tab` | Cycle thinking level |
|
|
302
|
+
| `cycleModelForward` | `ctrl+p` | Next model |
|
|
303
|
+
| `cycleModelBackward` | `shift+ctrl+p` | Previous model |
|
|
304
|
+
| `selectModel` | `ctrl+l` | Open model selector |
|
|
305
|
+
| `expandTools` | `ctrl+o` | Expand tool output |
|
|
306
|
+
| `toggleThinking` | `ctrl+t` | Toggle thinking |
|
|
307
|
+
| `externalEditor` | `ctrl+g` | Open external editor |
|
|
308
|
+
| `followUp` | `alt+enter` | Queue follow-up message |
|
|
309
|
+
|
|
310
|
+
**Example (Emacs-style):**
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"cursorUp": ["up", "ctrl+p"],
|
|
315
|
+
"cursorDown": ["down", "ctrl+n"],
|
|
316
|
+
"cursorLeft": ["left", "ctrl+b"],
|
|
317
|
+
"cursorRight": ["right", "ctrl+f"],
|
|
318
|
+
"cursorWordLeft": ["alt+left", "alt+b"],
|
|
319
|
+
"cursorWordRight": ["alt+right", "alt+f"],
|
|
320
|
+
"deleteCharForward": ["delete", "ctrl+d"],
|
|
321
|
+
"deleteCharBackward": ["backspace", "ctrl+h"],
|
|
322
|
+
"newLine": ["shift+enter", "ctrl+j"]
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Example (Vim-style):**
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"cursorUp": ["up", "alt+k"],
|
|
331
|
+
"cursorDown": ["down", "alt+j"],
|
|
332
|
+
"cursorLeft": ["left", "alt+h"],
|
|
333
|
+
"cursorRight": ["right", "alt+l"],
|
|
334
|
+
"cursorWordLeft": ["alt+left", "alt+b"],
|
|
335
|
+
"cursorWordRight": ["alt+right", "alt+w"],
|
|
336
|
+
"deleteCharBackward": ["backspace", "ctrl+h"],
|
|
337
|
+
"deleteWordBackward": ["ctrl+w", "alt+backspace"]
|
|
338
|
+
}
|
|
339
|
+
```
|
|
268
340
|
|
|
269
341
|
### Bash Mode
|
|
270
342
|
|
|
@@ -291,6 +363,10 @@ Run multiple commands before prompting; all outputs are included together.
|
|
|
291
363
|
|
|
292
364
|
### Image Support
|
|
293
365
|
|
|
366
|
+
**Pasting images:** Press `Ctrl+V` to paste an image from your clipboard.
|
|
367
|
+
|
|
368
|
+
**Dragging images:** Drag image files onto the terminal to insert their path. On macOS, you can also drag the screenshot thumbnail (after Cmd+Shift+4) directly onto the terminal.
|
|
369
|
+
|
|
294
370
|
**Attaching images:** Include image paths in your message:
|
|
295
371
|
|
|
296
372
|
```
|
|
@@ -1061,8 +1137,6 @@ Never use `__dirname` directly for package assets.
|
|
|
1061
1137
|
|
|
1062
1138
|
`/debug` (hidden) writes rendered lines with ANSI codes to `~/.pi/agent/pi-debug.log` for TUI debugging, as well as the last set of messages that were sent to the LLM.
|
|
1063
1139
|
|
|
1064
|
-
For architecture and contribution guidelines, see [DEVELOPMENT.md](./DEVELOPMENT.md).
|
|
1065
|
-
|
|
1066
1140
|
---
|
|
1067
1141
|
|
|
1068
1142
|
## License
|
|
@@ -333,6 +333,10 @@
|
|
|
333
333
|
margin-top: var(--line-height);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
.assistant-text + .tool-execution {
|
|
337
|
+
margin-top: var(--line-height);
|
|
338
|
+
}
|
|
339
|
+
|
|
336
340
|
.tool-execution.pending { background: var(--toolPendingBg); }
|
|
337
341
|
.tool-execution.success { background: var(--toolSuccessBg); }
|
|
338
342
|
.tool-execution.error { background: var(--toolErrorBg); }
|
|
@@ -493,21 +497,47 @@
|
|
|
493
497
|
margin-bottom: var(--line-height);
|
|
494
498
|
}
|
|
495
499
|
|
|
500
|
+
.system-prompt.expandable {
|
|
501
|
+
cursor: pointer;
|
|
502
|
+
}
|
|
503
|
+
|
|
496
504
|
.system-prompt-header {
|
|
497
505
|
font-weight: bold;
|
|
498
506
|
color: var(--customMessageLabel);
|
|
499
507
|
}
|
|
500
508
|
|
|
501
|
-
.system-prompt-
|
|
509
|
+
.system-prompt-preview {
|
|
502
510
|
color: var(--customMessageText);
|
|
503
511
|
white-space: pre-wrap;
|
|
504
512
|
word-wrap: break-word;
|
|
505
513
|
font-size: 11px;
|
|
506
|
-
max-height: 200px;
|
|
507
|
-
overflow-y: auto;
|
|
508
514
|
margin-top: var(--line-height);
|
|
509
515
|
}
|
|
510
516
|
|
|
517
|
+
.system-prompt-expand-hint {
|
|
518
|
+
color: var(--muted);
|
|
519
|
+
font-style: italic;
|
|
520
|
+
margin-top: 4px;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.system-prompt-full {
|
|
524
|
+
display: none;
|
|
525
|
+
color: var(--customMessageText);
|
|
526
|
+
white-space: pre-wrap;
|
|
527
|
+
word-wrap: break-word;
|
|
528
|
+
font-size: 11px;
|
|
529
|
+
margin-top: var(--line-height);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.system-prompt.expanded .system-prompt-preview,
|
|
533
|
+
.system-prompt.expanded .system-prompt-expand-hint {
|
|
534
|
+
display: none;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.system-prompt.expanded .system-prompt-full {
|
|
538
|
+
display: block;
|
|
539
|
+
}
|
|
540
|
+
|
|
511
541
|
/* Tools list */
|
|
512
542
|
.tools-list {
|
|
513
543
|
background: var(--customMessageBg);
|
|
@@ -518,7 +548,7 @@
|
|
|
518
548
|
|
|
519
549
|
.tools-header {
|
|
520
550
|
font-weight: bold;
|
|
521
|
-
color: var(--
|
|
551
|
+
color: var(--customMessageLabel);
|
|
522
552
|
margin-bottom: var(--line-height);
|
|
523
553
|
}
|
|
524
554
|
|
|
@@ -964,10 +964,23 @@
|
|
|
964
964
|
</div>`;
|
|
965
965
|
|
|
966
966
|
if (systemPrompt) {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
967
|
+
const lines = systemPrompt.split('\n');
|
|
968
|
+
const previewLines = 10;
|
|
969
|
+
if (lines.length > previewLines) {
|
|
970
|
+
const preview = lines.slice(0, previewLines).join('\n');
|
|
971
|
+
const remaining = lines.length - previewLines;
|
|
972
|
+
html += `<div class="system-prompt expandable" onclick="this.classList.toggle('expanded')">
|
|
973
|
+
<div class="system-prompt-header">System Prompt</div>
|
|
974
|
+
<div class="system-prompt-preview">${escapeHtml(preview)}</div>
|
|
975
|
+
<div class="system-prompt-expand-hint">... (${remaining} more lines, click to expand)</div>
|
|
976
|
+
<div class="system-prompt-full">${escapeHtml(systemPrompt)}</div>
|
|
977
|
+
</div>`;
|
|
978
|
+
} else {
|
|
979
|
+
html += `<div class="system-prompt">
|
|
980
|
+
<div class="system-prompt-header">System Prompt</div>
|
|
981
|
+
<div class="system-prompt-full" style="display: block">${escapeHtml(systemPrompt)}</div>
|
|
982
|
+
</div>`;
|
|
983
|
+
}
|
|
971
984
|
}
|
|
972
985
|
|
|
973
986
|
if (tools && tools.length > 0) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type EditorAction, type KeyId } from "@mariozechner/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* Application-level actions (coding agent specific).
|
|
4
|
+
*/
|
|
5
|
+
export type AppAction = "interrupt" | "clear" | "exit" | "suspend" | "cycleThinkingLevel" | "cycleModelForward" | "cycleModelBackward" | "selectModel" | "expandTools" | "toggleThinking" | "externalEditor" | "followUp";
|
|
6
|
+
/**
|
|
7
|
+
* All configurable actions.
|
|
8
|
+
*/
|
|
9
|
+
export type KeyAction = AppAction | EditorAction;
|
|
10
|
+
/**
|
|
11
|
+
* Full keybindings configuration (app + editor actions).
|
|
12
|
+
*/
|
|
13
|
+
export type KeybindingsConfig = {
|
|
14
|
+
[K in KeyAction]?: KeyId | KeyId[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Default application keybindings.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]>;
|
|
20
|
+
/**
|
|
21
|
+
* All default keybindings (app + editor).
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_KEYBINDINGS: Required<KeybindingsConfig>;
|
|
24
|
+
/**
|
|
25
|
+
* Manages all keybindings (app + editor).
|
|
26
|
+
*/
|
|
27
|
+
export declare class KeybindingsManager {
|
|
28
|
+
private config;
|
|
29
|
+
private appActionToKeys;
|
|
30
|
+
private constructor();
|
|
31
|
+
/**
|
|
32
|
+
* Create from config file and set up editor keybindings.
|
|
33
|
+
*/
|
|
34
|
+
static create(agentDir?: string): KeybindingsManager;
|
|
35
|
+
/**
|
|
36
|
+
* Create in-memory.
|
|
37
|
+
*/
|
|
38
|
+
static inMemory(config?: KeybindingsConfig): KeybindingsManager;
|
|
39
|
+
private static loadFromFile;
|
|
40
|
+
private buildMaps;
|
|
41
|
+
/**
|
|
42
|
+
* Check if input matches an app action.
|
|
43
|
+
*/
|
|
44
|
+
matches(data: string, action: AppAction): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Get keys bound to an app action.
|
|
47
|
+
*/
|
|
48
|
+
getKeys(action: AppAction): KeyId[];
|
|
49
|
+
/**
|
|
50
|
+
* Get display string for an action.
|
|
51
|
+
*/
|
|
52
|
+
getDisplayString(action: AppAction): string;
|
|
53
|
+
/**
|
|
54
|
+
* Get the full effective config.
|
|
55
|
+
*/
|
|
56
|
+
getEffectiveConfig(): Required<KeybindingsConfig>;
|
|
57
|
+
}
|
|
58
|
+
export type { EditorAction, KeyId };
|
|
59
|
+
//# sourceMappingURL=keybindings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keybindings.d.ts","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,YAAY,EAGjB,KAAK,KAAK,EAGV,MAAM,sBAAsB,CAAC;AAK9B;;GAEG;AACH,MAAM,MAAM,SAAS,GAClB,WAAW,GACX,OAAO,GACP,MAAM,GACN,SAAS,GACT,oBAAoB,GACpB,mBAAmB,GACnB,oBAAoB,GACpB,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,gBAAgB,GAChB,UAAU,CAAC;AAEd;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;KAC9B,CAAC,IAAI,SAAS,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,EAAE;CAClC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,GAAG,KAAK,EAAE,CAatE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,iBAAiB,CAG3D,CAAC;AAsBF;;GAEG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,eAAe,CAA0B;IAEjD,OAAO,eAIN;IAED;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAE,MAAsB,GAAG,kBAAkB,CAelE;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAE,iBAAsB,GAAG,kBAAkB,CAElE;IAED,OAAO,CAAC,MAAM,CAAC,YAAY;IAS3B,OAAO,CAAC,SAAS;IAiBjB;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAOhD;IAED;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,EAAE,CAElC;IAED;;OAEG;IACH,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAK1C;IAED;;OAEG;IACH,kBAAkB,IAAI,QAAQ,CAAC,iBAAiB,CAAC,CAQhD;CACD;AAGD,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC","sourcesContent":["import {\n\tDEFAULT_EDITOR_KEYBINDINGS,\n\ttype EditorAction,\n\ttype EditorKeybindingsConfig,\n\tEditorKeybindingsManager,\n\ttype KeyId,\n\tmatchesKey,\n\tsetEditorKeybindings,\n} from \"@mariozechner/pi-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\n/**\n * Application-level actions (coding agent specific).\n */\nexport type AppAction =\n\t| \"interrupt\"\n\t| \"clear\"\n\t| \"exit\"\n\t| \"suspend\"\n\t| \"cycleThinkingLevel\"\n\t| \"cycleModelForward\"\n\t| \"cycleModelBackward\"\n\t| \"selectModel\"\n\t| \"expandTools\"\n\t| \"toggleThinking\"\n\t| \"externalEditor\"\n\t| \"followUp\";\n\n/**\n * All configurable actions.\n */\nexport type KeyAction = AppAction | EditorAction;\n\n/**\n * Full keybindings configuration (app + editor actions).\n */\nexport type KeybindingsConfig = {\n\t[K in KeyAction]?: KeyId | KeyId[];\n};\n\n/**\n * Default application keybindings.\n */\nexport const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {\n\tinterrupt: \"escape\",\n\tclear: \"ctrl+c\",\n\texit: \"ctrl+d\",\n\tsuspend: \"ctrl+z\",\n\tcycleThinkingLevel: \"shift+tab\",\n\tcycleModelForward: \"ctrl+p\",\n\tcycleModelBackward: \"shift+ctrl+p\",\n\tselectModel: \"ctrl+l\",\n\texpandTools: \"ctrl+o\",\n\ttoggleThinking: \"ctrl+t\",\n\texternalEditor: \"ctrl+g\",\n\tfollowUp: \"alt+enter\",\n};\n\n/**\n * All default keybindings (app + editor).\n */\nexport const DEFAULT_KEYBINDINGS: Required<KeybindingsConfig> = {\n\t...DEFAULT_EDITOR_KEYBINDINGS,\n\t...DEFAULT_APP_KEYBINDINGS,\n};\n\n// App actions list for type checking\nconst APP_ACTIONS: AppAction[] = [\n\t\"interrupt\",\n\t\"clear\",\n\t\"exit\",\n\t\"suspend\",\n\t\"cycleThinkingLevel\",\n\t\"cycleModelForward\",\n\t\"cycleModelBackward\",\n\t\"selectModel\",\n\t\"expandTools\",\n\t\"toggleThinking\",\n\t\"externalEditor\",\n\t\"followUp\",\n];\n\nfunction isAppAction(action: string): action is AppAction {\n\treturn APP_ACTIONS.includes(action as AppAction);\n}\n\n/**\n * Manages all keybindings (app + editor).\n */\nexport class KeybindingsManager {\n\tprivate config: KeybindingsConfig;\n\tprivate appActionToKeys: Map<AppAction, KeyId[]>;\n\n\tprivate constructor(config: KeybindingsConfig) {\n\t\tthis.config = config;\n\t\tthis.appActionToKeys = new Map();\n\t\tthis.buildMaps();\n\t}\n\n\t/**\n\t * Create from config file and set up editor keybindings.\n\t */\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst config = KeybindingsManager.loadFromFile(configPath);\n\t\tconst manager = new KeybindingsManager(config);\n\n\t\t// Set up editor keybindings globally\n\t\tconst editorConfig: EditorKeybindingsConfig = {};\n\t\tfor (const [action, keys] of Object.entries(config)) {\n\t\t\tif (!isAppAction(action)) {\n\t\t\t\teditorConfig[action as EditorAction] = keys;\n\t\t\t}\n\t\t}\n\t\tsetEditorKeybindings(new EditorKeybindingsManager(editorConfig));\n\n\t\treturn manager;\n\t}\n\n\t/**\n\t * Create in-memory.\n\t */\n\tstatic inMemory(config: KeybindingsConfig = {}): KeybindingsManager {\n\t\treturn new KeybindingsManager(config);\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tif (!existsSync(path)) return {};\n\t\ttry {\n\t\t\treturn JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate buildMaps(): void {\n\t\tthis.appActionToKeys.clear();\n\n\t\t// Set defaults for app actions\n\t\tfor (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {\n\t\t\tconst keyArray = Array.isArray(keys) ? keys : [keys];\n\t\t\tthis.appActionToKeys.set(action as AppAction, [...keyArray]);\n\t\t}\n\n\t\t// Override with user config (app actions only)\n\t\tfor (const [action, keys] of Object.entries(this.config)) {\n\t\t\tif (keys === undefined || !isAppAction(action)) continue;\n\t\t\tconst keyArray = Array.isArray(keys) ? keys : [keys];\n\t\t\tthis.appActionToKeys.set(action, keyArray);\n\t\t}\n\t}\n\n\t/**\n\t * Check if input matches an app action.\n\t */\n\tmatches(data: string, action: AppAction): boolean {\n\t\tconst keys = this.appActionToKeys.get(action);\n\t\tif (!keys) return false;\n\t\tfor (const key of keys) {\n\t\t\tif (matchesKey(data, key)) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get keys bound to an app action.\n\t */\n\tgetKeys(action: AppAction): KeyId[] {\n\t\treturn this.appActionToKeys.get(action) ?? [];\n\t}\n\n\t/**\n\t * Get display string for an action.\n\t */\n\tgetDisplayString(action: AppAction): string {\n\t\tconst keys = this.getKeys(action);\n\t\tif (keys.length === 0) return \"\";\n\t\tif (keys.length === 1) return keys[0]!;\n\t\treturn keys.join(\"/\");\n\t}\n\n\t/**\n\t * Get the full effective config.\n\t */\n\tgetEffectiveConfig(): Required<KeybindingsConfig> {\n\t\tconst result = { ...DEFAULT_KEYBINDINGS };\n\t\tfor (const [action, keys] of Object.entries(this.config)) {\n\t\t\tif (keys !== undefined) {\n\t\t\t\t(result as KeybindingsConfig)[action as KeyAction] = keys;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n\n// Re-export for convenience\nexport type { EditorAction, KeyId };\n"]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { DEFAULT_EDITOR_KEYBINDINGS, EditorKeybindingsManager, matchesKey, setEditorKeybindings, } from "@mariozechner/pi-tui";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { getAgentDir } from "../config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Default application keybindings.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_APP_KEYBINDINGS = {
|
|
9
|
+
interrupt: "escape",
|
|
10
|
+
clear: "ctrl+c",
|
|
11
|
+
exit: "ctrl+d",
|
|
12
|
+
suspend: "ctrl+z",
|
|
13
|
+
cycleThinkingLevel: "shift+tab",
|
|
14
|
+
cycleModelForward: "ctrl+p",
|
|
15
|
+
cycleModelBackward: "shift+ctrl+p",
|
|
16
|
+
selectModel: "ctrl+l",
|
|
17
|
+
expandTools: "ctrl+o",
|
|
18
|
+
toggleThinking: "ctrl+t",
|
|
19
|
+
externalEditor: "ctrl+g",
|
|
20
|
+
followUp: "alt+enter",
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* All default keybindings (app + editor).
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_KEYBINDINGS = {
|
|
26
|
+
...DEFAULT_EDITOR_KEYBINDINGS,
|
|
27
|
+
...DEFAULT_APP_KEYBINDINGS,
|
|
28
|
+
};
|
|
29
|
+
// App actions list for type checking
|
|
30
|
+
const APP_ACTIONS = [
|
|
31
|
+
"interrupt",
|
|
32
|
+
"clear",
|
|
33
|
+
"exit",
|
|
34
|
+
"suspend",
|
|
35
|
+
"cycleThinkingLevel",
|
|
36
|
+
"cycleModelForward",
|
|
37
|
+
"cycleModelBackward",
|
|
38
|
+
"selectModel",
|
|
39
|
+
"expandTools",
|
|
40
|
+
"toggleThinking",
|
|
41
|
+
"externalEditor",
|
|
42
|
+
"followUp",
|
|
43
|
+
];
|
|
44
|
+
function isAppAction(action) {
|
|
45
|
+
return APP_ACTIONS.includes(action);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Manages all keybindings (app + editor).
|
|
49
|
+
*/
|
|
50
|
+
export class KeybindingsManager {
|
|
51
|
+
config;
|
|
52
|
+
appActionToKeys;
|
|
53
|
+
constructor(config) {
|
|
54
|
+
this.config = config;
|
|
55
|
+
this.appActionToKeys = new Map();
|
|
56
|
+
this.buildMaps();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create from config file and set up editor keybindings.
|
|
60
|
+
*/
|
|
61
|
+
static create(agentDir = getAgentDir()) {
|
|
62
|
+
const configPath = join(agentDir, "keybindings.json");
|
|
63
|
+
const config = KeybindingsManager.loadFromFile(configPath);
|
|
64
|
+
const manager = new KeybindingsManager(config);
|
|
65
|
+
// Set up editor keybindings globally
|
|
66
|
+
const editorConfig = {};
|
|
67
|
+
for (const [action, keys] of Object.entries(config)) {
|
|
68
|
+
if (!isAppAction(action)) {
|
|
69
|
+
editorConfig[action] = keys;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
setEditorKeybindings(new EditorKeybindingsManager(editorConfig));
|
|
73
|
+
return manager;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Create in-memory.
|
|
77
|
+
*/
|
|
78
|
+
static inMemory(config = {}) {
|
|
79
|
+
return new KeybindingsManager(config);
|
|
80
|
+
}
|
|
81
|
+
static loadFromFile(path) {
|
|
82
|
+
if (!existsSync(path))
|
|
83
|
+
return {};
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
buildMaps() {
|
|
92
|
+
this.appActionToKeys.clear();
|
|
93
|
+
// Set defaults for app actions
|
|
94
|
+
for (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {
|
|
95
|
+
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
96
|
+
this.appActionToKeys.set(action, [...keyArray]);
|
|
97
|
+
}
|
|
98
|
+
// Override with user config (app actions only)
|
|
99
|
+
for (const [action, keys] of Object.entries(this.config)) {
|
|
100
|
+
if (keys === undefined || !isAppAction(action))
|
|
101
|
+
continue;
|
|
102
|
+
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
103
|
+
this.appActionToKeys.set(action, keyArray);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Check if input matches an app action.
|
|
108
|
+
*/
|
|
109
|
+
matches(data, action) {
|
|
110
|
+
const keys = this.appActionToKeys.get(action);
|
|
111
|
+
if (!keys)
|
|
112
|
+
return false;
|
|
113
|
+
for (const key of keys) {
|
|
114
|
+
if (matchesKey(data, key))
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get keys bound to an app action.
|
|
121
|
+
*/
|
|
122
|
+
getKeys(action) {
|
|
123
|
+
return this.appActionToKeys.get(action) ?? [];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get display string for an action.
|
|
127
|
+
*/
|
|
128
|
+
getDisplayString(action) {
|
|
129
|
+
const keys = this.getKeys(action);
|
|
130
|
+
if (keys.length === 0)
|
|
131
|
+
return "";
|
|
132
|
+
if (keys.length === 1)
|
|
133
|
+
return keys[0];
|
|
134
|
+
return keys.join("/");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get the full effective config.
|
|
138
|
+
*/
|
|
139
|
+
getEffectiveConfig() {
|
|
140
|
+
const result = { ...DEFAULT_KEYBINDINGS };
|
|
141
|
+
for (const [action, keys] of Object.entries(this.config)) {
|
|
142
|
+
if (keys !== undefined) {
|
|
143
|
+
result[action] = keys;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=keybindings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keybindings.js","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tDEFAULT_EDITOR_KEYBINDINGS,\n\ttype EditorAction,\n\ttype EditorKeybindingsConfig,\n\tEditorKeybindingsManager,\n\ttype KeyId,\n\tmatchesKey,\n\tsetEditorKeybindings,\n} from \"@mariozechner/pi-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\n/**\n * Application-level actions (coding agent specific).\n */\nexport type AppAction =\n\t| \"interrupt\"\n\t| \"clear\"\n\t| \"exit\"\n\t| \"suspend\"\n\t| \"cycleThinkingLevel\"\n\t| \"cycleModelForward\"\n\t| \"cycleModelBackward\"\n\t| \"selectModel\"\n\t| \"expandTools\"\n\t| \"toggleThinking\"\n\t| \"externalEditor\"\n\t| \"followUp\";\n\n/**\n * All configurable actions.\n */\nexport type KeyAction = AppAction | EditorAction;\n\n/**\n * Full keybindings configuration (app + editor actions).\n */\nexport type KeybindingsConfig = {\n\t[K in KeyAction]?: KeyId | KeyId[];\n};\n\n/**\n * Default application keybindings.\n */\nexport const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {\n\tinterrupt: \"escape\",\n\tclear: \"ctrl+c\",\n\texit: \"ctrl+d\",\n\tsuspend: \"ctrl+z\",\n\tcycleThinkingLevel: \"shift+tab\",\n\tcycleModelForward: \"ctrl+p\",\n\tcycleModelBackward: \"shift+ctrl+p\",\n\tselectModel: \"ctrl+l\",\n\texpandTools: \"ctrl+o\",\n\ttoggleThinking: \"ctrl+t\",\n\texternalEditor: \"ctrl+g\",\n\tfollowUp: \"alt+enter\",\n};\n\n/**\n * All default keybindings (app + editor).\n */\nexport const DEFAULT_KEYBINDINGS: Required<KeybindingsConfig> = {\n\t...DEFAULT_EDITOR_KEYBINDINGS,\n\t...DEFAULT_APP_KEYBINDINGS,\n};\n\n// App actions list for type checking\nconst APP_ACTIONS: AppAction[] = [\n\t\"interrupt\",\n\t\"clear\",\n\t\"exit\",\n\t\"suspend\",\n\t\"cycleThinkingLevel\",\n\t\"cycleModelForward\",\n\t\"cycleModelBackward\",\n\t\"selectModel\",\n\t\"expandTools\",\n\t\"toggleThinking\",\n\t\"externalEditor\",\n\t\"followUp\",\n];\n\nfunction isAppAction(action: string): action is AppAction {\n\treturn APP_ACTIONS.includes(action as AppAction);\n}\n\n/**\n * Manages all keybindings (app + editor).\n */\nexport class KeybindingsManager {\n\tprivate config: KeybindingsConfig;\n\tprivate appActionToKeys: Map<AppAction, KeyId[]>;\n\n\tprivate constructor(config: KeybindingsConfig) {\n\t\tthis.config = config;\n\t\tthis.appActionToKeys = new Map();\n\t\tthis.buildMaps();\n\t}\n\n\t/**\n\t * Create from config file and set up editor keybindings.\n\t */\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst config = KeybindingsManager.loadFromFile(configPath);\n\t\tconst manager = new KeybindingsManager(config);\n\n\t\t// Set up editor keybindings globally\n\t\tconst editorConfig: EditorKeybindingsConfig = {};\n\t\tfor (const [action, keys] of Object.entries(config)) {\n\t\t\tif (!isAppAction(action)) {\n\t\t\t\teditorConfig[action as EditorAction] = keys;\n\t\t\t}\n\t\t}\n\t\tsetEditorKeybindings(new EditorKeybindingsManager(editorConfig));\n\n\t\treturn manager;\n\t}\n\n\t/**\n\t * Create in-memory.\n\t */\n\tstatic inMemory(config: KeybindingsConfig = {}): KeybindingsManager {\n\t\treturn new KeybindingsManager(config);\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tif (!existsSync(path)) return {};\n\t\ttry {\n\t\t\treturn JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate buildMaps(): void {\n\t\tthis.appActionToKeys.clear();\n\n\t\t// Set defaults for app actions\n\t\tfor (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {\n\t\t\tconst keyArray = Array.isArray(keys) ? keys : [keys];\n\t\t\tthis.appActionToKeys.set(action as AppAction, [...keyArray]);\n\t\t}\n\n\t\t// Override with user config (app actions only)\n\t\tfor (const [action, keys] of Object.entries(this.config)) {\n\t\t\tif (keys === undefined || !isAppAction(action)) continue;\n\t\t\tconst keyArray = Array.isArray(keys) ? keys : [keys];\n\t\t\tthis.appActionToKeys.set(action, keyArray);\n\t\t}\n\t}\n\n\t/**\n\t * Check if input matches an app action.\n\t */\n\tmatches(data: string, action: AppAction): boolean {\n\t\tconst keys = this.appActionToKeys.get(action);\n\t\tif (!keys) return false;\n\t\tfor (const key of keys) {\n\t\t\tif (matchesKey(data, key)) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get keys bound to an app action.\n\t */\n\tgetKeys(action: AppAction): KeyId[] {\n\t\treturn this.appActionToKeys.get(action) ?? [];\n\t}\n\n\t/**\n\t * Get display string for an action.\n\t */\n\tgetDisplayString(action: AppAction): string {\n\t\tconst keys = this.getKeys(action);\n\t\tif (keys.length === 0) return \"\";\n\t\tif (keys.length === 1) return keys[0]!;\n\t\treturn keys.join(\"/\");\n\t}\n\n\t/**\n\t * Get the full effective config.\n\t */\n\tgetEffectiveConfig(): Required<KeybindingsConfig> {\n\t\tconst result = { ...DEFAULT_KEYBINDINGS };\n\t\tfor (const [action, keys] of Object.entries(this.config)) {\n\t\t\tif (keys !== undefined) {\n\t\t\t\t(result as KeybindingsConfig)[action as KeyAction] = keys;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n\n// Re-export for convenience\nexport type { EditorAction, KeyId };\n"]}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import { Editor } from "@mariozechner/pi-tui";
|
|
1
|
+
import { Editor, type EditorTheme } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { AppAction, KeybindingsManager } from "../../../core/keybindings.js";
|
|
2
3
|
/**
|
|
3
|
-
* Custom editor that handles
|
|
4
|
+
* Custom editor that handles app-level keybindings for coding-agent.
|
|
4
5
|
*/
|
|
5
6
|
export declare class CustomEditor extends Editor {
|
|
7
|
+
private keybindings;
|
|
8
|
+
private actionHandlers;
|
|
6
9
|
onEscape?: () => void;
|
|
7
|
-
onCtrlC?: () => void;
|
|
8
10
|
onCtrlD?: () => void;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
onCtrlG?: () => void;
|
|
16
|
-
onCtrlZ?: () => void;
|
|
17
|
-
onAltEnter?: () => void;
|
|
11
|
+
onPasteImage?: () => void;
|
|
12
|
+
constructor(theme: EditorTheme, keybindings: KeybindingsManager);
|
|
13
|
+
/**
|
|
14
|
+
* Register a handler for an app action.
|
|
15
|
+
*/
|
|
16
|
+
onAction(action: AppAction, handler: () => void): void;
|
|
18
17
|
handleInput(data: string): void;
|
|
19
18
|
}
|
|
20
19
|
//# sourceMappingURL=custom-editor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-editor.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"custom-editor.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAc,MAAM,sBAAsB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElF;;GAEG;AACH,qBAAa,YAAa,SAAQ,MAAM;IACvC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,cAAc,CAAyC;IAGxD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAEjC,YAAY,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAG9D;IAED;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAErD;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CA2C9B;CACD","sourcesContent":["import { Editor, type EditorTheme, matchesKey } from \"@mariozechner/pi-tui\";\nimport type { AppAction, KeybindingsManager } from \"../../../core/keybindings.js\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tprivate actionHandlers: Map<AppAction, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\n\tconstructor(theme: EditorTheme, keybindings: KeybindingsManager) {\n\t\tsuper(theme);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppAction, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check for Ctrl+V to handle clipboard image paste\n\t\tif (matchesKey(data, \"ctrl+v\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t}\n\t\t\treturn; // Always consume\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"interrupt\" && action !== \"exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
|