@mariozechner/pi-coding-agent 0.33.0 → 0.34.1
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 +49 -0
- package/README.md +23 -2
- 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 +2 -0
- package/dist/core/custom-tools/loader.js.map +1 -1
- 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 +101 -9
- package/dist/core/hooks/runner.js.map +1 -1
- package/dist/core/hooks/types.d.ts +141 -3
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- 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 +2 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +6 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +19 -6
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +149 -42
- 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 +25 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +11 -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 +121 -4
- 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/tools.ts +145 -0
- package/package.json +4 -4
|
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import Clipboard from "@crosscopy/clipboard";
|
|
10
|
-
import { CombinedAutocompleteProvider, Container, getEditorKeybindings, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
10
|
+
import { CombinedAutocompleteProvider, Container, getEditorKeybindings, Input, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
11
11
|
import { exec, spawn, spawnSync } from "child_process";
|
|
12
12
|
import { APP_NAME, getAuthPath, getDebugLogPath } from "../../config.js";
|
|
13
13
|
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
@@ -90,11 +90,11 @@ export class InteractiveMode {
|
|
|
90
90
|
hookSelector = undefined;
|
|
91
91
|
hookInput = undefined;
|
|
92
92
|
hookEditor = undefined;
|
|
93
|
+
// Hook widgets (components rendered above the editor)
|
|
94
|
+
hookWidgets = new Map();
|
|
95
|
+
widgetContainer;
|
|
93
96
|
// Custom tools for custom rendering
|
|
94
97
|
customTools;
|
|
95
|
-
// Clipboard image tracking: imageId -> temp file path
|
|
96
|
-
clipboardImages = new Map();
|
|
97
|
-
clipboardImageCounter = 0;
|
|
98
98
|
// Convenience accessors
|
|
99
99
|
get agent() {
|
|
100
100
|
return this.session.agent;
|
|
@@ -115,6 +115,7 @@ export class InteractiveMode {
|
|
|
115
115
|
this.chatContainer = new Container();
|
|
116
116
|
this.pendingMessagesContainer = new Container();
|
|
117
117
|
this.statusContainer = new Container();
|
|
118
|
+
this.widgetContainer = new Container();
|
|
118
119
|
this.keybindings = KeybindingsManager.create();
|
|
119
120
|
this.editor = new CustomEditor(getEditorTheme(), this.keybindings);
|
|
120
121
|
this.editorContainer = new Container();
|
|
@@ -254,6 +255,7 @@ export class InteractiveMode {
|
|
|
254
255
|
this.ui.addChild(this.chatContainer);
|
|
255
256
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
256
257
|
this.ui.addChild(this.statusContainer);
|
|
258
|
+
this.ui.addChild(this.widgetContainer);
|
|
257
259
|
this.ui.addChild(new Spacer(1));
|
|
258
260
|
this.ui.addChild(this.editorContainer);
|
|
259
261
|
this.ui.addChild(this.footer);
|
|
@@ -322,20 +324,7 @@ export class InteractiveMode {
|
|
|
322
324
|
this.chatContainer.addChild(new Spacer(1));
|
|
323
325
|
}
|
|
324
326
|
// Create and set hook & tool UI context
|
|
325
|
-
const uiContext =
|
|
326
|
-
select: (title, options) => this.showHookSelector(title, options),
|
|
327
|
-
confirm: (title, message) => this.showHookConfirm(title, message),
|
|
328
|
-
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
|
329
|
-
notify: (message, type) => this.showHookNotify(message, type),
|
|
330
|
-
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
331
|
-
custom: (factory) => this.showHookCustom(factory),
|
|
332
|
-
setEditorText: (text) => this.editor.setText(text),
|
|
333
|
-
getEditorText: () => this.editor.getText(),
|
|
334
|
-
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
|
335
|
-
get theme() {
|
|
336
|
-
return theme;
|
|
337
|
-
},
|
|
338
|
-
};
|
|
327
|
+
const uiContext = this.createHookUIContext();
|
|
339
328
|
this.setToolUIContext(uiContext, true);
|
|
340
329
|
// Notify custom tools of session start
|
|
341
330
|
await this.emitCustomToolSessionEvent({
|
|
@@ -366,6 +355,9 @@ export class InteractiveMode {
|
|
|
366
355
|
appendEntryHandler: (customType, data) => {
|
|
367
356
|
this.sessionManager.appendCustomEntry(customType, data);
|
|
368
357
|
},
|
|
358
|
+
getActiveToolsHandler: () => this.session.getActiveToolNames(),
|
|
359
|
+
getAllToolsHandler: () => this.session.getAllToolNames(),
|
|
360
|
+
setActiveToolsHandler: (toolNames) => this.session.setActiveToolsByName(toolNames),
|
|
369
361
|
newSessionHandler: async (options) => {
|
|
370
362
|
// Stop any loading animation
|
|
371
363
|
if (this.loadingAnimation) {
|
|
@@ -430,8 +422,10 @@ export class InteractiveMode {
|
|
|
430
422
|
});
|
|
431
423
|
// Subscribe to hook errors
|
|
432
424
|
hookRunner.onError((error) => {
|
|
433
|
-
this.showHookError(error.hookPath, error.error);
|
|
425
|
+
this.showHookError(error.hookPath, error.error, error.stack);
|
|
434
426
|
});
|
|
427
|
+
// Set up hook-registered shortcuts
|
|
428
|
+
this.setupHookShortcuts(hookRunner);
|
|
435
429
|
// Show loaded hooks
|
|
436
430
|
const hookPaths = hookRunner.getHookPaths();
|
|
437
431
|
if (hookPaths.length > 0) {
|
|
@@ -476,6 +470,40 @@ export class InteractiveMode {
|
|
|
476
470
|
this.chatContainer.addChild(errorText);
|
|
477
471
|
this.ui.requestRender();
|
|
478
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
|
+
}
|
|
479
507
|
/**
|
|
480
508
|
* Set hook status text in the footer.
|
|
481
509
|
*/
|
|
@@ -483,6 +511,74 @@ export class InteractiveMode {
|
|
|
483
511
|
this.footer.setHookStatus(key, text);
|
|
484
512
|
this.ui.requestRender();
|
|
485
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
|
+
setTitle: (title) => this.ui.terminal.setTitle(title),
|
|
573
|
+
custom: (factory) => this.showHookCustom(factory),
|
|
574
|
+
setEditorText: (text) => this.editor.setText(text),
|
|
575
|
+
getEditorText: () => this.editor.getText(),
|
|
576
|
+
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
|
577
|
+
get theme() {
|
|
578
|
+
return theme;
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
486
582
|
/**
|
|
487
583
|
* Show a selector for hooks.
|
|
488
584
|
*/
|
|
@@ -616,9 +712,21 @@ export class InteractiveMode {
|
|
|
616
712
|
/**
|
|
617
713
|
* Show a hook error in the UI.
|
|
618
714
|
*/
|
|
619
|
-
showHookError(hookPath, error) {
|
|
620
|
-
const
|
|
715
|
+
showHookError(hookPath, error, stack) {
|
|
716
|
+
const errorMsg = `Hook "${hookPath}" error: ${error}`;
|
|
717
|
+
const errorText = new Text(theme.fg("error", errorMsg), 1, 0);
|
|
621
718
|
this.chatContainer.addChild(errorText);
|
|
719
|
+
if (stack) {
|
|
720
|
+
// Show stack trace in dim color, indented
|
|
721
|
+
const stackLines = stack
|
|
722
|
+
.split("\n")
|
|
723
|
+
.slice(1) // Skip first line (duplicates error message)
|
|
724
|
+
.map((line) => theme.fg("dim", ` ${line.trim()}`))
|
|
725
|
+
.join("\n");
|
|
726
|
+
if (stackLines) {
|
|
727
|
+
this.chatContainer.addChild(new Text(stackLines, 1, 0));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
622
730
|
this.ui.requestRender();
|
|
623
731
|
}
|
|
624
732
|
/**
|
|
@@ -702,33 +810,18 @@ export class InteractiveMode {
|
|
|
702
810
|
return;
|
|
703
811
|
}
|
|
704
812
|
// Write to temp file
|
|
705
|
-
const imageId = ++this.clipboardImageCounter;
|
|
706
813
|
const tmpDir = os.tmpdir();
|
|
707
814
|
const fileName = `pi-clipboard-${crypto.randomUUID()}.png`;
|
|
708
815
|
const filePath = path.join(tmpDir, fileName);
|
|
709
816
|
fs.writeFileSync(filePath, Buffer.from(imageData));
|
|
710
|
-
//
|
|
711
|
-
this.
|
|
712
|
-
this.editor.insertTextAtCursor(`[image #${imageId}]`);
|
|
817
|
+
// Insert file path directly
|
|
818
|
+
this.editor.insertTextAtCursor(filePath);
|
|
713
819
|
this.ui.requestRender();
|
|
714
820
|
}
|
|
715
821
|
catch {
|
|
716
822
|
// Silently ignore clipboard errors (may not have permission, etc.)
|
|
717
823
|
}
|
|
718
824
|
}
|
|
719
|
-
/**
|
|
720
|
-
* Replace [image #N] markers with actual file paths and clear the image map.
|
|
721
|
-
*/
|
|
722
|
-
replaceImageMarkers(text) {
|
|
723
|
-
let result = text;
|
|
724
|
-
for (const [imageId, filePath] of this.clipboardImages) {
|
|
725
|
-
const marker = `[image #${imageId}]`;
|
|
726
|
-
result = result.replace(marker, filePath);
|
|
727
|
-
}
|
|
728
|
-
this.clipboardImages.clear();
|
|
729
|
-
this.clipboardImageCounter = 0;
|
|
730
|
-
return result;
|
|
731
|
-
}
|
|
732
825
|
setupEditorSubmitHandler() {
|
|
733
826
|
this.editor.onSubmit = async (text) => {
|
|
734
827
|
text = text.trim();
|
|
@@ -854,8 +947,6 @@ export class InteractiveMode {
|
|
|
854
947
|
return;
|
|
855
948
|
}
|
|
856
949
|
// If streaming, use prompt() with steer behavior
|
|
857
|
-
// Replace image markers with actual file paths
|
|
858
|
-
text = this.replaceImageMarkers(text);
|
|
859
950
|
// This handles hook commands (execute immediately), slash command expansion, and queueing
|
|
860
951
|
if (this.session.isStreaming) {
|
|
861
952
|
this.editor.addToHistory(text);
|
|
@@ -1394,7 +1485,7 @@ export class InteractiveMode {
|
|
|
1394
1485
|
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
1395
1486
|
return;
|
|
1396
1487
|
}
|
|
1397
|
-
const currentText = this.editor.
|
|
1488
|
+
const currentText = this.editor.getExpandedText();
|
|
1398
1489
|
const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
|
|
1399
1490
|
try {
|
|
1400
1491
|
// Write current content to temp file
|
|
@@ -2049,7 +2140,7 @@ export class InteractiveMode {
|
|
|
2049
2140
|
const toggleThinking = this.getAppKeyDisplay("toggleThinking");
|
|
2050
2141
|
const externalEditor = this.getAppKeyDisplay("externalEditor");
|
|
2051
2142
|
const followUp = this.getAppKeyDisplay("followUp");
|
|
2052
|
-
|
|
2143
|
+
let hotkeys = `
|
|
2053
2144
|
**Navigation**
|
|
2054
2145
|
| Key | Action |
|
|
2055
2146
|
|-----|--------|
|
|
@@ -2085,6 +2176,22 @@ export class InteractiveMode {
|
|
|
2085
2176
|
| \`/\` | Slash commands |
|
|
2086
2177
|
| \`!\` | Run bash command |
|
|
2087
2178
|
`;
|
|
2179
|
+
// Add hook-registered shortcuts
|
|
2180
|
+
const hookRunner = this.session.hookRunner;
|
|
2181
|
+
if (hookRunner) {
|
|
2182
|
+
const shortcuts = hookRunner.getShortcuts();
|
|
2183
|
+
if (shortcuts.size > 0) {
|
|
2184
|
+
hotkeys += `
|
|
2185
|
+
**Hooks**
|
|
2186
|
+
| Key | Action |
|
|
2187
|
+
|-----|--------|
|
|
2188
|
+
`;
|
|
2189
|
+
for (const [key, shortcut] of shortcuts) {
|
|
2190
|
+
const description = shortcut.description ?? shortcut.hookPath;
|
|
2191
|
+
hotkeys += `| \`${key}\` | ${description} |\n`;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2088
2195
|
this.chatContainer.addChild(new Spacer(1));
|
|
2089
2196
|
this.chatContainer.addChild(new DynamicBorder());
|
|
2090
2197
|
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|