@mariozechner/pi-coding-agent 0.32.3 → 0.34.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 +56 -1
- package/README.md +76 -3
- package/dist/cli/args.d.ts +5 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +18 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +24 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +65 -9
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +2 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/custom-tools/loader.d.ts.map +1 -1
- package/dist/core/custom-tools/loader.js +1 -0
- package/dist/core/custom-tools/loader.js.map +1 -1
- package/dist/core/export-html/template.css +34 -4
- package/dist/core/export-html/template.js +17 -4
- package/dist/core/hooks/index.d.ts +1 -1
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js.map +1 -1
- package/dist/core/hooks/loader.d.ts +56 -1
- package/dist/core/hooks/loader.d.ts.map +1 -1
- package/dist/core/hooks/loader.js +54 -2
- package/dist/core/hooks/loader.js.map +1 -1
- package/dist/core/hooks/runner.d.ts +33 -5
- package/dist/core/hooks/runner.d.ts.map +1 -1
- package/dist/core/hooks/runner.js +100 -9
- package/dist/core/hooks/runner.js.map +1 -1
- package/dist/core/hooks/types.d.ts +135 -3
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- 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/core/sdk.d.ts +3 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +102 -27
- package/dist/core/sdk.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +32 -7
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +13 -12
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +50 -68
- 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 +34 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +300 -64
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +3 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +16 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +6 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/hooks.md +114 -4
- package/docs/tui.md +18 -15
- package/examples/custom-tools/subagent/README.md +2 -2
- package/examples/hooks/README.md +3 -0
- package/examples/hooks/pirate.ts +44 -0
- package/examples/hooks/plan-mode.ts +548 -0
- package/examples/hooks/snake.ts +7 -7
- package/examples/hooks/todo/index.ts +2 -2
- package/examples/hooks/tools.ts +145 -0
- package/package.json +5 -4
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* Interactive mode for the coding agent.
|
|
3
3
|
* Handles TUI rendering and user interaction, delegating business logic to AgentSession.
|
|
4
4
|
*/
|
|
5
|
+
import * as crypto from "node:crypto";
|
|
5
6
|
import * as fs from "node:fs";
|
|
6
7
|
import * as os from "node:os";
|
|
7
8
|
import * as path from "node:path";
|
|
8
|
-
import
|
|
9
|
+
import Clipboard from "@crosscopy/clipboard";
|
|
10
|
+
import { CombinedAutocompleteProvider, Container, getEditorKeybindings, Input, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
9
11
|
import { exec, spawn, spawnSync } from "child_process";
|
|
10
12
|
import { APP_NAME, getAuthPath, getDebugLogPath } from "../../config.js";
|
|
13
|
+
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
11
14
|
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|
12
15
|
import { SessionManager } from "../../core/session-manager.js";
|
|
13
16
|
import { loadSkills } from "../../core/skills.js";
|
|
@@ -49,6 +52,7 @@ export class InteractiveMode {
|
|
|
49
52
|
editor;
|
|
50
53
|
editorContainer;
|
|
51
54
|
footer;
|
|
55
|
+
keybindings;
|
|
52
56
|
version;
|
|
53
57
|
isInitialized = false;
|
|
54
58
|
onInputCallback;
|
|
@@ -86,6 +90,9 @@ export class InteractiveMode {
|
|
|
86
90
|
hookSelector = undefined;
|
|
87
91
|
hookInput = undefined;
|
|
88
92
|
hookEditor = undefined;
|
|
93
|
+
// Hook widgets (components rendered above the editor)
|
|
94
|
+
hookWidgets = new Map();
|
|
95
|
+
widgetContainer;
|
|
89
96
|
// Custom tools for custom rendering
|
|
90
97
|
customTools;
|
|
91
98
|
// Convenience accessors
|
|
@@ -108,7 +115,9 @@ export class InteractiveMode {
|
|
|
108
115
|
this.chatContainer = new Container();
|
|
109
116
|
this.pendingMessagesContainer = new Container();
|
|
110
117
|
this.statusContainer = new Container();
|
|
111
|
-
this.
|
|
118
|
+
this.widgetContainer = new Container();
|
|
119
|
+
this.keybindings = KeybindingsManager.create();
|
|
120
|
+
this.editor = new CustomEditor(getEditorTheme(), this.keybindings);
|
|
112
121
|
this.editorContainer = new Container();
|
|
113
122
|
this.editorContainer.addChild(this.editor);
|
|
114
123
|
this.footer = new FooterComponent(session);
|
|
@@ -150,42 +159,61 @@ export class InteractiveMode {
|
|
|
150
159
|
async init() {
|
|
151
160
|
if (this.isInitialized)
|
|
152
161
|
return;
|
|
153
|
-
// Add header
|
|
162
|
+
// Add header with keybindings from config
|
|
154
163
|
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
155
|
-
|
|
164
|
+
// Format keybinding for startup display (lowercase, compact)
|
|
165
|
+
const formatStartupKey = (keys) => {
|
|
166
|
+
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
167
|
+
return keyArray.join("/");
|
|
168
|
+
};
|
|
169
|
+
const kb = this.keybindings;
|
|
170
|
+
const interrupt = formatStartupKey(kb.getKeys("interrupt"));
|
|
171
|
+
const clear = formatStartupKey(kb.getKeys("clear"));
|
|
172
|
+
const exit = formatStartupKey(kb.getKeys("exit"));
|
|
173
|
+
const suspend = formatStartupKey(kb.getKeys("suspend"));
|
|
174
|
+
const deleteToLineEnd = formatStartupKey(getEditorKeybindings().getKeys("deleteToLineEnd"));
|
|
175
|
+
const cycleThinkingLevel = formatStartupKey(kb.getKeys("cycleThinkingLevel"));
|
|
176
|
+
const cycleModelForward = formatStartupKey(kb.getKeys("cycleModelForward"));
|
|
177
|
+
const cycleModelBackward = formatStartupKey(kb.getKeys("cycleModelBackward"));
|
|
178
|
+
const selectModel = formatStartupKey(kb.getKeys("selectModel"));
|
|
179
|
+
const expandTools = formatStartupKey(kb.getKeys("expandTools"));
|
|
180
|
+
const toggleThinking = formatStartupKey(kb.getKeys("toggleThinking"));
|
|
181
|
+
const externalEditor = formatStartupKey(kb.getKeys("externalEditor"));
|
|
182
|
+
const followUp = formatStartupKey(kb.getKeys("followUp"));
|
|
183
|
+
const instructions = theme.fg("dim", interrupt) +
|
|
156
184
|
theme.fg("muted", " to interrupt") +
|
|
157
185
|
"\n" +
|
|
158
|
-
theme.fg("dim",
|
|
186
|
+
theme.fg("dim", clear) +
|
|
159
187
|
theme.fg("muted", " to clear") +
|
|
160
188
|
"\n" +
|
|
161
|
-
theme.fg("dim",
|
|
189
|
+
theme.fg("dim", `${clear} twice`) +
|
|
162
190
|
theme.fg("muted", " to exit") +
|
|
163
191
|
"\n" +
|
|
164
|
-
theme.fg("dim",
|
|
192
|
+
theme.fg("dim", exit) +
|
|
165
193
|
theme.fg("muted", " to exit (empty)") +
|
|
166
194
|
"\n" +
|
|
167
|
-
theme.fg("dim",
|
|
195
|
+
theme.fg("dim", suspend) +
|
|
168
196
|
theme.fg("muted", " to suspend") +
|
|
169
197
|
"\n" +
|
|
170
|
-
theme.fg("dim",
|
|
198
|
+
theme.fg("dim", deleteToLineEnd) +
|
|
171
199
|
theme.fg("muted", " to delete line") +
|
|
172
200
|
"\n" +
|
|
173
|
-
theme.fg("dim",
|
|
201
|
+
theme.fg("dim", cycleThinkingLevel) +
|
|
174
202
|
theme.fg("muted", " to cycle thinking") +
|
|
175
203
|
"\n" +
|
|
176
|
-
theme.fg("dim",
|
|
204
|
+
theme.fg("dim", `${cycleModelForward}/${cycleModelBackward}`) +
|
|
177
205
|
theme.fg("muted", " to cycle models") +
|
|
178
206
|
"\n" +
|
|
179
|
-
theme.fg("dim",
|
|
207
|
+
theme.fg("dim", selectModel) +
|
|
180
208
|
theme.fg("muted", " to select model") +
|
|
181
209
|
"\n" +
|
|
182
|
-
theme.fg("dim",
|
|
210
|
+
theme.fg("dim", expandTools) +
|
|
183
211
|
theme.fg("muted", " to expand tools") +
|
|
184
212
|
"\n" +
|
|
185
|
-
theme.fg("dim",
|
|
213
|
+
theme.fg("dim", toggleThinking) +
|
|
186
214
|
theme.fg("muted", " to toggle thinking") +
|
|
187
215
|
"\n" +
|
|
188
|
-
theme.fg("dim",
|
|
216
|
+
theme.fg("dim", externalEditor) +
|
|
189
217
|
theme.fg("muted", " for external editor") +
|
|
190
218
|
"\n" +
|
|
191
219
|
theme.fg("dim", "/") +
|
|
@@ -194,9 +222,12 @@ export class InteractiveMode {
|
|
|
194
222
|
theme.fg("dim", "!") +
|
|
195
223
|
theme.fg("muted", " to run bash") +
|
|
196
224
|
"\n" +
|
|
197
|
-
theme.fg("dim",
|
|
225
|
+
theme.fg("dim", followUp) +
|
|
198
226
|
theme.fg("muted", " to queue follow-up") +
|
|
199
227
|
"\n" +
|
|
228
|
+
theme.fg("dim", "ctrl+v") +
|
|
229
|
+
theme.fg("muted", " to paste image") +
|
|
230
|
+
"\n" +
|
|
200
231
|
theme.fg("dim", "drop files") +
|
|
201
232
|
theme.fg("muted", " to attach");
|
|
202
233
|
const header = new Text(`${logo}\n${instructions}`, 1, 0);
|
|
@@ -224,6 +255,7 @@ export class InteractiveMode {
|
|
|
224
255
|
this.ui.addChild(this.chatContainer);
|
|
225
256
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
226
257
|
this.ui.addChild(this.statusContainer);
|
|
258
|
+
this.ui.addChild(this.widgetContainer);
|
|
227
259
|
this.ui.addChild(new Spacer(1));
|
|
228
260
|
this.ui.addChild(this.editorContainer);
|
|
229
261
|
this.ui.addChild(this.footer);
|
|
@@ -292,20 +324,7 @@ export class InteractiveMode {
|
|
|
292
324
|
this.chatContainer.addChild(new Spacer(1));
|
|
293
325
|
}
|
|
294
326
|
// Create and set hook & tool UI context
|
|
295
|
-
const uiContext =
|
|
296
|
-
select: (title, options) => this.showHookSelector(title, options),
|
|
297
|
-
confirm: (title, message) => this.showHookConfirm(title, message),
|
|
298
|
-
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
|
299
|
-
notify: (message, type) => this.showHookNotify(message, type),
|
|
300
|
-
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
301
|
-
custom: (factory) => this.showHookCustom(factory),
|
|
302
|
-
setEditorText: (text) => this.editor.setText(text),
|
|
303
|
-
getEditorText: () => this.editor.getText(),
|
|
304
|
-
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
|
305
|
-
get theme() {
|
|
306
|
-
return theme;
|
|
307
|
-
},
|
|
308
|
-
};
|
|
327
|
+
const uiContext = this.createHookUIContext();
|
|
309
328
|
this.setToolUIContext(uiContext, true);
|
|
310
329
|
// Notify custom tools of session start
|
|
311
330
|
await this.emitCustomToolSessionEvent({
|
|
@@ -336,6 +355,9 @@ export class InteractiveMode {
|
|
|
336
355
|
appendEntryHandler: (customType, data) => {
|
|
337
356
|
this.sessionManager.appendCustomEntry(customType, data);
|
|
338
357
|
},
|
|
358
|
+
getActiveToolsHandler: () => this.session.getActiveToolNames(),
|
|
359
|
+
getAllToolsHandler: () => this.session.getAllToolNames(),
|
|
360
|
+
setActiveToolsHandler: (toolNames) => this.session.setActiveToolsByName(toolNames),
|
|
339
361
|
newSessionHandler: async (options) => {
|
|
340
362
|
// Stop any loading animation
|
|
341
363
|
if (this.loadingAnimation) {
|
|
@@ -400,8 +422,10 @@ export class InteractiveMode {
|
|
|
400
422
|
});
|
|
401
423
|
// Subscribe to hook errors
|
|
402
424
|
hookRunner.onError((error) => {
|
|
403
|
-
this.showHookError(error.hookPath, error.error);
|
|
425
|
+
this.showHookError(error.hookPath, error.error, error.stack);
|
|
404
426
|
});
|
|
427
|
+
// Set up hook-registered shortcuts
|
|
428
|
+
this.setupHookShortcuts(hookRunner);
|
|
405
429
|
// Show loaded hooks
|
|
406
430
|
const hookPaths = hookRunner.getHookPaths();
|
|
407
431
|
if (hookPaths.length > 0) {
|
|
@@ -446,6 +470,40 @@ export class InteractiveMode {
|
|
|
446
470
|
this.chatContainer.addChild(errorText);
|
|
447
471
|
this.ui.requestRender();
|
|
448
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Set up keyboard shortcuts registered by hooks.
|
|
475
|
+
*/
|
|
476
|
+
setupHookShortcuts(hookRunner) {
|
|
477
|
+
const shortcuts = hookRunner.getShortcuts();
|
|
478
|
+
if (shortcuts.size === 0)
|
|
479
|
+
return;
|
|
480
|
+
// Create a context for shortcut handlers
|
|
481
|
+
const createContext = () => ({
|
|
482
|
+
ui: this.createHookUIContext(),
|
|
483
|
+
hasUI: true,
|
|
484
|
+
cwd: process.cwd(),
|
|
485
|
+
sessionManager: this.sessionManager,
|
|
486
|
+
modelRegistry: this.session.modelRegistry,
|
|
487
|
+
model: this.session.model,
|
|
488
|
+
isIdle: () => !this.session.isStreaming,
|
|
489
|
+
abort: () => this.session.abort(),
|
|
490
|
+
hasPendingMessages: () => this.session.pendingMessageCount > 0,
|
|
491
|
+
});
|
|
492
|
+
// Set up the hook shortcut handler on the editor
|
|
493
|
+
this.editor.onHookShortcut = (data) => {
|
|
494
|
+
for (const [shortcutStr, shortcut] of shortcuts) {
|
|
495
|
+
// Cast to KeyId - hook shortcuts use the same format
|
|
496
|
+
if (matchesKey(data, shortcutStr)) {
|
|
497
|
+
// Run handler async, don't block input
|
|
498
|
+
Promise.resolve(shortcut.handler(createContext())).catch((err) => {
|
|
499
|
+
this.showError(`Shortcut handler error: ${err instanceof Error ? err.message : String(err)}`);
|
|
500
|
+
});
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
};
|
|
506
|
+
}
|
|
449
507
|
/**
|
|
450
508
|
* Set hook status text in the footer.
|
|
451
509
|
*/
|
|
@@ -453,6 +511,73 @@ export class InteractiveMode {
|
|
|
453
511
|
this.footer.setHookStatus(key, text);
|
|
454
512
|
this.ui.requestRender();
|
|
455
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Set a hook widget (string array or custom component).
|
|
516
|
+
*/
|
|
517
|
+
setHookWidget(key, content) {
|
|
518
|
+
// Dispose and remove existing widget
|
|
519
|
+
const existing = this.hookWidgets.get(key);
|
|
520
|
+
if (existing?.dispose)
|
|
521
|
+
existing.dispose();
|
|
522
|
+
if (content === undefined) {
|
|
523
|
+
this.hookWidgets.delete(key);
|
|
524
|
+
}
|
|
525
|
+
else if (Array.isArray(content)) {
|
|
526
|
+
// Wrap string array in a Container with Text components
|
|
527
|
+
const container = new Container();
|
|
528
|
+
for (const line of content.slice(0, InteractiveMode.MAX_WIDGET_LINES)) {
|
|
529
|
+
container.addChild(new Text(line, 1, 0));
|
|
530
|
+
}
|
|
531
|
+
if (content.length > InteractiveMode.MAX_WIDGET_LINES) {
|
|
532
|
+
container.addChild(new Text(theme.fg("muted", "... (widget truncated)"), 1, 0));
|
|
533
|
+
}
|
|
534
|
+
this.hookWidgets.set(key, container);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// Factory function - create component
|
|
538
|
+
const component = content(this.ui, theme);
|
|
539
|
+
this.hookWidgets.set(key, component);
|
|
540
|
+
}
|
|
541
|
+
this.renderWidgets();
|
|
542
|
+
}
|
|
543
|
+
// Maximum total widget lines to prevent viewport overflow
|
|
544
|
+
static MAX_WIDGET_LINES = 10;
|
|
545
|
+
/**
|
|
546
|
+
* Render all hook widgets to the widget container.
|
|
547
|
+
*/
|
|
548
|
+
renderWidgets() {
|
|
549
|
+
if (!this.widgetContainer)
|
|
550
|
+
return;
|
|
551
|
+
this.widgetContainer.clear();
|
|
552
|
+
if (this.hookWidgets.size === 0) {
|
|
553
|
+
this.ui.requestRender();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
for (const [_key, component] of this.hookWidgets) {
|
|
557
|
+
this.widgetContainer.addChild(component);
|
|
558
|
+
}
|
|
559
|
+
this.ui.requestRender();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Create the HookUIContext for hooks and tools.
|
|
563
|
+
*/
|
|
564
|
+
createHookUIContext() {
|
|
565
|
+
return {
|
|
566
|
+
select: (title, options) => this.showHookSelector(title, options),
|
|
567
|
+
confirm: (title, message) => this.showHookConfirm(title, message),
|
|
568
|
+
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
|
569
|
+
notify: (message, type) => this.showHookNotify(message, type),
|
|
570
|
+
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
571
|
+
setWidget: (key, content) => this.setHookWidget(key, content),
|
|
572
|
+
custom: (factory) => this.showHookCustom(factory),
|
|
573
|
+
setEditorText: (text) => this.editor.setText(text),
|
|
574
|
+
getEditorText: () => this.editor.getText(),
|
|
575
|
+
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
|
576
|
+
get theme() {
|
|
577
|
+
return theme;
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|
|
456
581
|
/**
|
|
457
582
|
* Show a selector for hooks.
|
|
458
583
|
*/
|
|
@@ -586,9 +711,21 @@ export class InteractiveMode {
|
|
|
586
711
|
/**
|
|
587
712
|
* Show a hook error in the UI.
|
|
588
713
|
*/
|
|
589
|
-
showHookError(hookPath, error) {
|
|
590
|
-
const
|
|
714
|
+
showHookError(hookPath, error, stack) {
|
|
715
|
+
const errorMsg = `Hook "${hookPath}" error: ${error}`;
|
|
716
|
+
const errorText = new Text(theme.fg("error", errorMsg), 1, 0);
|
|
591
717
|
this.chatContainer.addChild(errorText);
|
|
718
|
+
if (stack) {
|
|
719
|
+
// Show stack trace in dim color, indented
|
|
720
|
+
const stackLines = stack
|
|
721
|
+
.split("\n")
|
|
722
|
+
.slice(1) // Skip first line (duplicates error message)
|
|
723
|
+
.map((line) => theme.fg("dim", ` ${line.trim()}`))
|
|
724
|
+
.join("\n");
|
|
725
|
+
if (stackLines) {
|
|
726
|
+
this.chatContainer.addChild(new Text(stackLines, 1, 0));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
592
729
|
this.ui.requestRender();
|
|
593
730
|
}
|
|
594
731
|
/**
|
|
@@ -636,19 +773,20 @@ export class InteractiveMode {
|
|
|
636
773
|
}
|
|
637
774
|
}
|
|
638
775
|
};
|
|
639
|
-
|
|
776
|
+
// Register app action handlers
|
|
777
|
+
this.editor.onAction("clear", () => this.handleCtrlC());
|
|
640
778
|
this.editor.onCtrlD = () => this.handleCtrlD();
|
|
641
|
-
this.editor.
|
|
642
|
-
this.editor.
|
|
643
|
-
this.editor.
|
|
644
|
-
this.editor.
|
|
779
|
+
this.editor.onAction("suspend", () => this.handleCtrlZ());
|
|
780
|
+
this.editor.onAction("cycleThinkingLevel", () => this.cycleThinkingLevel());
|
|
781
|
+
this.editor.onAction("cycleModelForward", () => this.cycleModel("forward"));
|
|
782
|
+
this.editor.onAction("cycleModelBackward", () => this.cycleModel("backward"));
|
|
645
783
|
// Global debug handler on TUI (works regardless of focus)
|
|
646
784
|
this.ui.onDebug = () => this.handleDebugCommand();
|
|
647
|
-
this.editor.
|
|
648
|
-
this.editor.
|
|
649
|
-
this.editor.
|
|
650
|
-
this.editor.
|
|
651
|
-
this.editor.
|
|
785
|
+
this.editor.onAction("selectModel", () => this.showModelSelector());
|
|
786
|
+
this.editor.onAction("expandTools", () => this.toggleToolOutputExpansion());
|
|
787
|
+
this.editor.onAction("toggleThinking", () => this.toggleThinkingBlockVisibility());
|
|
788
|
+
this.editor.onAction("externalEditor", () => this.openExternalEditor());
|
|
789
|
+
this.editor.onAction("followUp", () => this.handleFollowUp());
|
|
652
790
|
this.editor.onChange = (text) => {
|
|
653
791
|
const wasBashMode = this.isBashMode;
|
|
654
792
|
this.isBashMode = text.trimStart().startsWith("!");
|
|
@@ -656,6 +794,32 @@ export class InteractiveMode {
|
|
|
656
794
|
this.updateEditorBorderColor();
|
|
657
795
|
}
|
|
658
796
|
};
|
|
797
|
+
// Handle clipboard image paste (triggered on Ctrl+V)
|
|
798
|
+
this.editor.onPasteImage = () => {
|
|
799
|
+
this.handleClipboardImagePaste();
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
async handleClipboardImagePaste() {
|
|
803
|
+
try {
|
|
804
|
+
if (!Clipboard.hasImage()) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const imageData = await Clipboard.getImageBinary();
|
|
808
|
+
if (!imageData || imageData.length === 0) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
// Write to temp file
|
|
812
|
+
const tmpDir = os.tmpdir();
|
|
813
|
+
const fileName = `pi-clipboard-${crypto.randomUUID()}.png`;
|
|
814
|
+
const filePath = path.join(tmpDir, fileName);
|
|
815
|
+
fs.writeFileSync(filePath, Buffer.from(imageData));
|
|
816
|
+
// Insert file path directly
|
|
817
|
+
this.editor.insertTextAtCursor(filePath);
|
|
818
|
+
this.ui.requestRender();
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
// Silently ignore clipboard errors (may not have permission, etc.)
|
|
822
|
+
}
|
|
659
823
|
}
|
|
660
824
|
setupEditorSubmitHandler() {
|
|
661
825
|
this.editor.onSubmit = async (text) => {
|
|
@@ -755,6 +919,11 @@ export class InteractiveMode {
|
|
|
755
919
|
this.editor.setText("");
|
|
756
920
|
return;
|
|
757
921
|
}
|
|
922
|
+
if (text === "/quit" || text === "/exit") {
|
|
923
|
+
this.editor.setText("");
|
|
924
|
+
await this.shutdown();
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
758
927
|
// Handle bash command (! for normal, !! for excluded from context)
|
|
759
928
|
if (text.startsWith("!")) {
|
|
760
929
|
const isExcluded = text.startsWith("!!");
|
|
@@ -1228,7 +1397,7 @@ export class InteractiveMode {
|
|
|
1228
1397
|
// Send SIGTSTP to process group (pid=0 means all processes in group)
|
|
1229
1398
|
process.kill(0, "SIGTSTP");
|
|
1230
1399
|
}
|
|
1231
|
-
async
|
|
1400
|
+
async handleFollowUp() {
|
|
1232
1401
|
const text = this.editor.getText().trim();
|
|
1233
1402
|
if (!text)
|
|
1234
1403
|
return;
|
|
@@ -1315,7 +1484,7 @@ export class InteractiveMode {
|
|
|
1315
1484
|
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
1316
1485
|
return;
|
|
1317
1486
|
}
|
|
1318
|
-
const currentText = this.editor.
|
|
1487
|
+
const currentText = this.editor.getExpandedText();
|
|
1319
1488
|
const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
|
|
1320
1489
|
try {
|
|
1321
1490
|
// Write current content to temp file
|
|
@@ -1920,41 +2089,108 @@ export class InteractiveMode {
|
|
|
1920
2089
|
this.chatContainer.addChild(new DynamicBorder());
|
|
1921
2090
|
this.ui.requestRender();
|
|
1922
2091
|
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Format keybindings for display (e.g., "ctrl+c" -> "Ctrl+C").
|
|
2094
|
+
*/
|
|
2095
|
+
formatKeyDisplay(keys) {
|
|
2096
|
+
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
2097
|
+
return keyArray
|
|
2098
|
+
.map((key) => key
|
|
2099
|
+
.split("+")
|
|
2100
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
2101
|
+
.join("+"))
|
|
2102
|
+
.join("/");
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Get display string for an app keybinding action.
|
|
2106
|
+
*/
|
|
2107
|
+
getAppKeyDisplay(action) {
|
|
2108
|
+
const display = this.keybindings.getDisplayString(action);
|
|
2109
|
+
return this.formatKeyDisplay(display);
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Get display string for an editor keybinding action.
|
|
2113
|
+
*/
|
|
2114
|
+
getEditorKeyDisplay(action) {
|
|
2115
|
+
const keys = getEditorKeybindings().getKeys(action);
|
|
2116
|
+
return this.formatKeyDisplay(keys);
|
|
2117
|
+
}
|
|
1923
2118
|
handleHotkeysCommand() {
|
|
1924
|
-
|
|
2119
|
+
// Navigation keybindings
|
|
2120
|
+
const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
|
|
2121
|
+
const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
|
|
2122
|
+
const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
|
|
2123
|
+
const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
|
|
2124
|
+
// Editing keybindings
|
|
2125
|
+
const submit = this.getEditorKeyDisplay("submit");
|
|
2126
|
+
const newLine = this.getEditorKeyDisplay("newLine");
|
|
2127
|
+
const deleteWordBackward = this.getEditorKeyDisplay("deleteWordBackward");
|
|
2128
|
+
const deleteToLineStart = this.getEditorKeyDisplay("deleteToLineStart");
|
|
2129
|
+
const deleteToLineEnd = this.getEditorKeyDisplay("deleteToLineEnd");
|
|
2130
|
+
const tab = this.getEditorKeyDisplay("tab");
|
|
2131
|
+
// App keybindings
|
|
2132
|
+
const interrupt = this.getAppKeyDisplay("interrupt");
|
|
2133
|
+
const clear = this.getAppKeyDisplay("clear");
|
|
2134
|
+
const exit = this.getAppKeyDisplay("exit");
|
|
2135
|
+
const suspend = this.getAppKeyDisplay("suspend");
|
|
2136
|
+
const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
|
|
2137
|
+
const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
|
|
2138
|
+
const expandTools = this.getAppKeyDisplay("expandTools");
|
|
2139
|
+
const toggleThinking = this.getAppKeyDisplay("toggleThinking");
|
|
2140
|
+
const externalEditor = this.getAppKeyDisplay("externalEditor");
|
|
2141
|
+
const followUp = this.getAppKeyDisplay("followUp");
|
|
2142
|
+
let hotkeys = `
|
|
1925
2143
|
**Navigation**
|
|
1926
2144
|
| Key | Action |
|
|
1927
2145
|
|-----|--------|
|
|
1928
2146
|
| \`Arrow keys\` | Move cursor / browse history (Up when empty) |
|
|
1929
|
-
| \`
|
|
1930
|
-
| \`
|
|
1931
|
-
| \`
|
|
2147
|
+
| \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
|
|
2148
|
+
| \`${cursorLineStart}\` | Start of line |
|
|
2149
|
+
| \`${cursorLineEnd}\` | End of line |
|
|
1932
2150
|
|
|
1933
2151
|
**Editing**
|
|
1934
2152
|
| Key | Action |
|
|
1935
2153
|
|-----|--------|
|
|
1936
|
-
| \`
|
|
1937
|
-
| \`
|
|
1938
|
-
| \`
|
|
1939
|
-
| \`
|
|
1940
|
-
| \`
|
|
2154
|
+
| \`${submit}\` | Send message |
|
|
2155
|
+
| \`${newLine}\` | New line |
|
|
2156
|
+
| \`${deleteWordBackward}\` | Delete word backwards |
|
|
2157
|
+
| \`${deleteToLineStart}\` | Delete to start of line |
|
|
2158
|
+
| \`${deleteToLineEnd}\` | Delete to end of line |
|
|
1941
2159
|
|
|
1942
2160
|
**Other**
|
|
1943
2161
|
| Key | Action |
|
|
1944
2162
|
|-----|--------|
|
|
1945
|
-
| \`
|
|
1946
|
-
| \`
|
|
1947
|
-
| \`
|
|
1948
|
-
| \`
|
|
1949
|
-
| \`
|
|
1950
|
-
| \`
|
|
1951
|
-
| \`
|
|
1952
|
-
| \`
|
|
1953
|
-
| \`
|
|
1954
|
-
| \`
|
|
2163
|
+
| \`${tab}\` | Path completion / accept autocomplete |
|
|
2164
|
+
| \`${interrupt}\` | Cancel autocomplete / abort streaming |
|
|
2165
|
+
| \`${clear}\` | Clear editor (first) / exit (second) |
|
|
2166
|
+
| \`${exit}\` | Exit (when editor is empty) |
|
|
2167
|
+
| \`${suspend}\` | Suspend to background |
|
|
2168
|
+
| \`${cycleThinkingLevel}\` | Cycle thinking level |
|
|
2169
|
+
| \`${cycleModelForward}\` | Cycle models |
|
|
2170
|
+
| \`${expandTools}\` | Toggle tool output expansion |
|
|
2171
|
+
| \`${toggleThinking}\` | Toggle thinking block visibility |
|
|
2172
|
+
| \`${externalEditor}\` | Edit message in external editor |
|
|
2173
|
+
| \`${followUp}\` | Queue follow-up message |
|
|
2174
|
+
| \`Ctrl+V\` | Paste image from clipboard |
|
|
1955
2175
|
| \`/\` | Slash commands |
|
|
1956
2176
|
| \`!\` | Run bash command |
|
|
1957
2177
|
`;
|
|
2178
|
+
// Add hook-registered shortcuts
|
|
2179
|
+
const hookRunner = this.session.hookRunner;
|
|
2180
|
+
if (hookRunner) {
|
|
2181
|
+
const shortcuts = hookRunner.getShortcuts();
|
|
2182
|
+
if (shortcuts.size > 0) {
|
|
2183
|
+
hotkeys += `
|
|
2184
|
+
**Hooks**
|
|
2185
|
+
| Key | Action |
|
|
2186
|
+
|-----|--------|
|
|
2187
|
+
`;
|
|
2188
|
+
for (const [key, shortcut] of shortcuts) {
|
|
2189
|
+
const description = shortcut.description ?? shortcut.hookPath;
|
|
2190
|
+
hotkeys += `| \`${key}\` | ${description} |\n`;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
1958
2194
|
this.chatContainer.addChild(new Spacer(1));
|
|
1959
2195
|
this.chatContainer.addChild(new DynamicBorder());
|
|
1960
2196
|
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|