@mariozechner/pi-coding-agent 0.33.0 → 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 +40 -1
- package/README.md +0 -1
- 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/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/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 +148 -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 +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/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,73 @@ 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
|
+
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
|
+
}
|
|
486
581
|
/**
|
|
487
582
|
* Show a selector for hooks.
|
|
488
583
|
*/
|
|
@@ -616,9 +711,21 @@ export class InteractiveMode {
|
|
|
616
711
|
/**
|
|
617
712
|
* Show a hook error in the UI.
|
|
618
713
|
*/
|
|
619
|
-
showHookError(hookPath, error) {
|
|
620
|
-
const
|
|
714
|
+
showHookError(hookPath, error, stack) {
|
|
715
|
+
const errorMsg = `Hook "${hookPath}" error: ${error}`;
|
|
716
|
+
const errorText = new Text(theme.fg("error", errorMsg), 1, 0);
|
|
621
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
|
+
}
|
|
622
729
|
this.ui.requestRender();
|
|
623
730
|
}
|
|
624
731
|
/**
|
|
@@ -702,33 +809,18 @@ export class InteractiveMode {
|
|
|
702
809
|
return;
|
|
703
810
|
}
|
|
704
811
|
// Write to temp file
|
|
705
|
-
const imageId = ++this.clipboardImageCounter;
|
|
706
812
|
const tmpDir = os.tmpdir();
|
|
707
813
|
const fileName = `pi-clipboard-${crypto.randomUUID()}.png`;
|
|
708
814
|
const filePath = path.join(tmpDir, fileName);
|
|
709
815
|
fs.writeFileSync(filePath, Buffer.from(imageData));
|
|
710
|
-
//
|
|
711
|
-
this.
|
|
712
|
-
this.editor.insertTextAtCursor(`[image #${imageId}]`);
|
|
816
|
+
// Insert file path directly
|
|
817
|
+
this.editor.insertTextAtCursor(filePath);
|
|
713
818
|
this.ui.requestRender();
|
|
714
819
|
}
|
|
715
820
|
catch {
|
|
716
821
|
// Silently ignore clipboard errors (may not have permission, etc.)
|
|
717
822
|
}
|
|
718
823
|
}
|
|
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
824
|
setupEditorSubmitHandler() {
|
|
733
825
|
this.editor.onSubmit = async (text) => {
|
|
734
826
|
text = text.trim();
|
|
@@ -854,8 +946,6 @@ export class InteractiveMode {
|
|
|
854
946
|
return;
|
|
855
947
|
}
|
|
856
948
|
// If streaming, use prompt() with steer behavior
|
|
857
|
-
// Replace image markers with actual file paths
|
|
858
|
-
text = this.replaceImageMarkers(text);
|
|
859
949
|
// This handles hook commands (execute immediately), slash command expansion, and queueing
|
|
860
950
|
if (this.session.isStreaming) {
|
|
861
951
|
this.editor.addToHistory(text);
|
|
@@ -1394,7 +1484,7 @@ export class InteractiveMode {
|
|
|
1394
1484
|
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
1395
1485
|
return;
|
|
1396
1486
|
}
|
|
1397
|
-
const currentText = this.editor.
|
|
1487
|
+
const currentText = this.editor.getExpandedText();
|
|
1398
1488
|
const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
|
|
1399
1489
|
try {
|
|
1400
1490
|
// Write current content to temp file
|
|
@@ -2049,7 +2139,7 @@ export class InteractiveMode {
|
|
|
2049
2139
|
const toggleThinking = this.getAppKeyDisplay("toggleThinking");
|
|
2050
2140
|
const externalEditor = this.getAppKeyDisplay("externalEditor");
|
|
2051
2141
|
const followUp = this.getAppKeyDisplay("followUp");
|
|
2052
|
-
|
|
2142
|
+
let hotkeys = `
|
|
2053
2143
|
**Navigation**
|
|
2054
2144
|
| Key | Action |
|
|
2055
2145
|
|-----|--------|
|
|
@@ -2085,6 +2175,22 @@ export class InteractiveMode {
|
|
|
2085
2175
|
| \`/\` | Slash commands |
|
|
2086
2176
|
| \`!\` | Run bash command |
|
|
2087
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
|
+
}
|
|
2088
2194
|
this.chatContainer.addChild(new Spacer(1));
|
|
2089
2195
|
this.chatContainer.addChild(new DynamicBorder());
|
|
2090
2196
|
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|