@mariozechner/pi-coding-agent 0.37.8 → 0.39.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 +115 -4
- package/README.md +11 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +23 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +75 -35
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts +6 -0
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +77 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extensions/index.d.ts +3 -3
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +8 -6
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +94 -211
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +27 -30
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +102 -45
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +155 -30
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts +5 -3
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +6 -4
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-resolver.d.ts +4 -2
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +8 -9
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts +8 -5
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +39 -87
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +8 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +9 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -5
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +25 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +103 -73
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts +17 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +12 -5
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +18 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +68 -18
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +15 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +22 -10
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +7 -7
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +21 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +80 -72
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +14 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +12 -5
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts +15 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +9 -4
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +58 -116
- package/dist/main.js.map +1 -1
- package/dist/modes/index.d.ts +2 -2
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +7 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
- package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
- package/dist/modes/interactive/components/countdown-timer.js +33 -0
- package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts +10 -2
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +18 -14
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +10 -2
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +18 -22
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +50 -23
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +44 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +440 -139
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +7 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +34 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +14 -7
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +45 -21
- 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 +111 -101
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -1
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +35 -7
- package/dist/utils/clipboard.js.map +1 -1
- package/docs/extensions.md +211 -15
- package/docs/sdk.md +68 -9
- package/docs/tui.md +81 -4
- package/examples/extensions/README.md +3 -0
- package/examples/extensions/claude-rules.ts +5 -2
- package/examples/extensions/handoff.ts +1 -1
- package/examples/extensions/interactive-shell.ts +196 -0
- package/examples/extensions/mac-system-theme.ts +25 -0
- package/examples/extensions/modal-editor.ts +85 -0
- package/examples/extensions/overlay-test.ts +145 -0
- package/examples/extensions/pirate.ts +7 -4
- package/examples/extensions/preset.ts +3 -3
- package/examples/extensions/qna.ts +1 -1
- package/examples/extensions/rainbow-editor.ts +95 -0
- package/examples/extensions/shutdown-command.ts +63 -0
- package/examples/extensions/snake.ts +1 -1
- package/examples/extensions/ssh.ts +220 -0
- package/examples/extensions/timed-confirm.ts +32 -25
- package/examples/extensions/todo.ts +1 -1
- package/examples/extensions/tool-override.ts +143 -0
- package/examples/extensions/tools.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/04-skills.ts +4 -1
- package/package.json +6 -6
|
@@ -6,18 +6,19 @@ import * as crypto from "node:crypto";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { getOAuthProviders } from "@mariozechner/pi-ai";
|
|
9
|
+
import { getOAuthProviders, } from "@mariozechner/pi-ai";
|
|
10
10
|
import { CombinedAutocompleteProvider, Container, getEditorKeybindings, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
11
11
|
import { spawn, spawnSync } from "child_process";
|
|
12
|
-
import { APP_NAME, getAuthPath, getDebugLogPath } from "../../config.js";
|
|
12
|
+
import { APP_NAME, getAuthPath, getDebugLogPath, isBunBinary, VERSION } from "../../config.js";
|
|
13
13
|
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
14
14
|
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|
15
15
|
import { SessionManager } from "../../core/session-manager.js";
|
|
16
|
-
import { loadSkills } from "../../core/skills.js";
|
|
17
16
|
import { loadProjectContextFiles } from "../../core/system-prompt.js";
|
|
18
|
-
import {
|
|
17
|
+
import { allTools } from "../../core/tools/index.js";
|
|
18
|
+
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
|
19
19
|
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
20
20
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
|
21
|
+
import { ensureTool } from "../../utils/tools-manager.js";
|
|
21
22
|
import { ArminComponent } from "./components/armin.js";
|
|
22
23
|
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
|
23
24
|
import { BashExecutionComponent } from "./components/bash-execution.js";
|
|
@@ -40,18 +41,20 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
|
40
41
|
import { TreeSelectorComponent } from "./components/tree-selector.js";
|
|
41
42
|
import { UserMessageComponent } from "./components/user-message.js";
|
|
42
43
|
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
|
43
|
-
import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme, } from "./theme/theme.js";
|
|
44
|
+
import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setTheme, setThemeInstance, Theme, theme, } from "./theme/theme.js";
|
|
44
45
|
function isExpandable(obj) {
|
|
45
46
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
46
47
|
}
|
|
47
48
|
export class InteractiveMode {
|
|
48
|
-
|
|
49
|
+
options;
|
|
49
50
|
session;
|
|
50
51
|
ui;
|
|
51
52
|
chatContainer;
|
|
52
53
|
pendingMessagesContainer;
|
|
53
54
|
statusContainer;
|
|
55
|
+
defaultEditor;
|
|
54
56
|
editor;
|
|
57
|
+
autocompleteProvider;
|
|
55
58
|
editorContainer;
|
|
56
59
|
footer;
|
|
57
60
|
keybindings;
|
|
@@ -90,6 +93,8 @@ export class InteractiveMode {
|
|
|
90
93
|
retryEscapeHandler;
|
|
91
94
|
// Messages queued while compaction is running
|
|
92
95
|
compactionQueuedMessages = [];
|
|
96
|
+
// Shutdown state
|
|
97
|
+
shutdownRequested = false;
|
|
93
98
|
// Extension UI state
|
|
94
99
|
extensionSelector = undefined;
|
|
95
100
|
extensionInput = undefined;
|
|
@@ -113,22 +118,28 @@ export class InteractiveMode {
|
|
|
113
118
|
get settingsManager() {
|
|
114
119
|
return this.session.settingsManager;
|
|
115
120
|
}
|
|
116
|
-
constructor(session,
|
|
117
|
-
this.
|
|
121
|
+
constructor(session, options = {}) {
|
|
122
|
+
this.options = options;
|
|
118
123
|
this.session = session;
|
|
119
|
-
this.version =
|
|
120
|
-
this.changelogMarkdown = changelogMarkdown;
|
|
124
|
+
this.version = VERSION;
|
|
121
125
|
this.ui = new TUI(new ProcessTerminal());
|
|
122
126
|
this.chatContainer = new Container();
|
|
123
127
|
this.pendingMessagesContainer = new Container();
|
|
124
128
|
this.statusContainer = new Container();
|
|
125
129
|
this.widgetContainer = new Container();
|
|
126
130
|
this.keybindings = KeybindingsManager.create();
|
|
127
|
-
this.
|
|
131
|
+
this.defaultEditor = new CustomEditor(getEditorTheme(), this.keybindings);
|
|
132
|
+
this.editor = this.defaultEditor;
|
|
128
133
|
this.editorContainer = new Container();
|
|
129
134
|
this.editorContainer.addChild(this.editor);
|
|
130
135
|
this.footer = new FooterComponent(session);
|
|
131
136
|
this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
137
|
+
// Load hide thinking block setting
|
|
138
|
+
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
139
|
+
// Initialize theme with watcher for interactive mode
|
|
140
|
+
initTheme(this.settingsManager.getTheme(), true);
|
|
141
|
+
}
|
|
142
|
+
setupAutocomplete(fdPath) {
|
|
132
143
|
// Define commands for autocomplete
|
|
133
144
|
const slashCommands = [
|
|
134
145
|
{ name: "settings", description: "Open settings menu" },
|
|
@@ -147,8 +158,6 @@ export class InteractiveMode {
|
|
|
147
158
|
{ name: "compact", description: "Manually compact the session context" },
|
|
148
159
|
{ name: "resume", description: "Resume a different session" },
|
|
149
160
|
];
|
|
150
|
-
// Load hide thinking block setting
|
|
151
|
-
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
152
161
|
// Convert prompt templates to SlashCommand format for autocomplete
|
|
153
162
|
const templateCommands = this.session.promptTemplates.map((cmd) => ({
|
|
154
163
|
name: cmd.name,
|
|
@@ -160,12 +169,17 @@ export class InteractiveMode {
|
|
|
160
169
|
description: cmd.description ?? "(extension command)",
|
|
161
170
|
}));
|
|
162
171
|
// Setup autocomplete
|
|
163
|
-
|
|
164
|
-
this.
|
|
172
|
+
this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands], process.cwd(), fdPath);
|
|
173
|
+
this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
|
|
165
174
|
}
|
|
166
175
|
async init() {
|
|
167
176
|
if (this.isInitialized)
|
|
168
177
|
return;
|
|
178
|
+
// Load changelog (only show new entries, skip for resumed sessions)
|
|
179
|
+
this.changelogMarkdown = this.getChangelogForDisplay();
|
|
180
|
+
// Setup autocomplete with fd tool for file path completion
|
|
181
|
+
const fdPath = await ensureTool("fd");
|
|
182
|
+
this.setupAutocomplete(fdPath);
|
|
169
183
|
// Add header with keybindings from config
|
|
170
184
|
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
171
185
|
// Format keybinding for startup display (lowercase, compact)
|
|
@@ -293,6 +307,112 @@ export class InteractiveMode {
|
|
|
293
307
|
this.ui.requestRender();
|
|
294
308
|
});
|
|
295
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Run the interactive mode. This is the main entry point.
|
|
312
|
+
* Initializes the UI, shows warnings, processes initial messages, and starts the interactive loop.
|
|
313
|
+
*/
|
|
314
|
+
async run() {
|
|
315
|
+
await this.init();
|
|
316
|
+
// Start version check asynchronously
|
|
317
|
+
this.checkForNewVersion().then((newVersion) => {
|
|
318
|
+
if (newVersion) {
|
|
319
|
+
this.showNewVersionNotification(newVersion);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
this.renderInitialMessages();
|
|
323
|
+
// Show startup warnings
|
|
324
|
+
const { migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages } = this.options;
|
|
325
|
+
if (migratedProviders && migratedProviders.length > 0) {
|
|
326
|
+
this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
|
|
327
|
+
}
|
|
328
|
+
const modelsJsonError = this.session.modelRegistry.getError();
|
|
329
|
+
if (modelsJsonError) {
|
|
330
|
+
this.showError(`models.json error: ${modelsJsonError}`);
|
|
331
|
+
}
|
|
332
|
+
if (modelFallbackMessage) {
|
|
333
|
+
this.showWarning(modelFallbackMessage);
|
|
334
|
+
}
|
|
335
|
+
// Process initial messages
|
|
336
|
+
if (initialMessage) {
|
|
337
|
+
try {
|
|
338
|
+
await this.session.prompt(initialMessage, { images: initialImages });
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
342
|
+
this.showError(errorMessage);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (initialMessages) {
|
|
346
|
+
for (const message of initialMessages) {
|
|
347
|
+
try {
|
|
348
|
+
await this.session.prompt(message);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
352
|
+
this.showError(errorMessage);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Main interactive loop
|
|
357
|
+
while (true) {
|
|
358
|
+
const userInput = await this.getUserInput();
|
|
359
|
+
try {
|
|
360
|
+
await this.session.prompt(userInput);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
364
|
+
this.showError(errorMessage);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Check npm registry for a newer version.
|
|
370
|
+
*/
|
|
371
|
+
async checkForNewVersion() {
|
|
372
|
+
if (process.env.PI_SKIP_VERSION_CHECK)
|
|
373
|
+
return undefined;
|
|
374
|
+
try {
|
|
375
|
+
const response = await fetch("https://registry.npmjs.org/@mariozechner/pi-coding-agent/latest");
|
|
376
|
+
if (!response.ok)
|
|
377
|
+
return undefined;
|
|
378
|
+
const data = (await response.json());
|
|
379
|
+
const latestVersion = data.version;
|
|
380
|
+
if (latestVersion && latestVersion !== this.version) {
|
|
381
|
+
return latestVersion;
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get changelog entries to display on startup.
|
|
391
|
+
* Only shows new entries since last seen version, skips for resumed sessions.
|
|
392
|
+
*/
|
|
393
|
+
getChangelogForDisplay() {
|
|
394
|
+
// Skip changelog for resumed/continued sessions (already have messages)
|
|
395
|
+
if (this.session.state.messages.length > 0) {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
const lastVersion = this.settingsManager.getLastChangelogVersion();
|
|
399
|
+
const changelogPath = getChangelogPath();
|
|
400
|
+
const entries = parseChangelog(changelogPath);
|
|
401
|
+
if (!lastVersion) {
|
|
402
|
+
if (entries.length > 0) {
|
|
403
|
+
this.settingsManager.setLastChangelogVersion(VERSION);
|
|
404
|
+
return entries.map((e) => e.content).join("\n\n");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const newEntries = getNewEntries(entries, lastVersion);
|
|
409
|
+
if (newEntries.length > 0) {
|
|
410
|
+
this.settingsManager.setLastChangelogVersion(VERSION);
|
|
411
|
+
return newEntries.map((e) => e.content).join("\n\n");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
296
416
|
// =========================================================================
|
|
297
417
|
// Extension System
|
|
298
418
|
// =========================================================================
|
|
@@ -307,40 +427,34 @@ export class InteractiveMode {
|
|
|
307
427
|
this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded context:\n") + contextList, 0, 0));
|
|
308
428
|
this.chatContainer.addChild(new Spacer(1));
|
|
309
429
|
}
|
|
310
|
-
// Show loaded skills
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
.join("\n");
|
|
324
|
-
this.chatContainer.addChild(new Text(theme.fg("warning", "Skill warnings:\n") + warningList, 0, 0));
|
|
325
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
326
|
-
}
|
|
430
|
+
// Show loaded skills (already discovered by SDK)
|
|
431
|
+
const skills = this.session.skills;
|
|
432
|
+
if (skills.length > 0) {
|
|
433
|
+
const skillList = skills.map((s) => theme.fg("dim", ` ${s.filePath}`)).join("\n");
|
|
434
|
+
this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded skills:\n") + skillList, 0, 0));
|
|
435
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
436
|
+
}
|
|
437
|
+
// Show skill warnings if any
|
|
438
|
+
const skillWarnings = this.session.skillWarnings;
|
|
439
|
+
if (skillWarnings.length > 0) {
|
|
440
|
+
const warningList = skillWarnings.map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`)).join("\n");
|
|
441
|
+
this.chatContainer.addChild(new Text(theme.fg("warning", "Skill warnings:\n") + warningList, 0, 0));
|
|
442
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
327
443
|
}
|
|
328
|
-
// Create and set extension UI context
|
|
329
|
-
const uiContext = this.createExtensionUIContext();
|
|
330
|
-
this.setExtensionUIContext(uiContext, true);
|
|
331
444
|
const extensionRunner = this.session.extensionRunner;
|
|
332
445
|
if (!extensionRunner) {
|
|
333
446
|
return; // No extensions loaded
|
|
334
447
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
448
|
+
// Create extension UI context
|
|
449
|
+
const uiContext = this.createExtensionUIContext();
|
|
450
|
+
extensionRunner.initialize(
|
|
451
|
+
// ExtensionActions - for pi.* API
|
|
452
|
+
{
|
|
453
|
+
sendMessage: (message, options) => {
|
|
338
454
|
const wasStreaming = this.session.isStreaming;
|
|
339
455
|
this.session
|
|
340
456
|
.sendCustomMessage(message, options)
|
|
341
457
|
.then(() => {
|
|
342
|
-
// For non-streaming cases with display=true, update UI
|
|
343
|
-
// (streaming cases update via message_end event)
|
|
344
458
|
if (!wasStreaming && message.display) {
|
|
345
459
|
this.rebuildChatFromMessages();
|
|
346
460
|
}
|
|
@@ -349,34 +463,53 @@ export class InteractiveMode {
|
|
|
349
463
|
this.showError(`Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
464
|
});
|
|
351
465
|
},
|
|
352
|
-
|
|
466
|
+
sendUserMessage: (content, options) => {
|
|
353
467
|
this.session.sendUserMessage(content, options).catch((err) => {
|
|
354
468
|
this.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
355
469
|
});
|
|
356
470
|
},
|
|
357
|
-
|
|
471
|
+
appendEntry: (customType, data) => {
|
|
358
472
|
this.sessionManager.appendCustomEntry(customType, data);
|
|
359
473
|
},
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
474
|
+
getActiveTools: () => this.session.getActiveToolNames(),
|
|
475
|
+
getAllTools: () => this.session.getAllToolNames(),
|
|
476
|
+
setActiveTools: (toolNames) => this.session.setActiveToolsByName(toolNames),
|
|
477
|
+
setModel: async (model) => {
|
|
478
|
+
const key = await this.session.modelRegistry.getApiKey(model);
|
|
479
|
+
if (!key)
|
|
480
|
+
return false;
|
|
481
|
+
await this.session.setModel(model);
|
|
482
|
+
return true;
|
|
483
|
+
},
|
|
484
|
+
getThinkingLevel: () => this.session.thinkingLevel,
|
|
485
|
+
setThinkingLevel: (level) => this.session.setThinkingLevel(level),
|
|
486
|
+
},
|
|
487
|
+
// ExtensionContextActions - for ctx.* in event handlers
|
|
488
|
+
{
|
|
489
|
+
getModel: () => this.session.model,
|
|
490
|
+
isIdle: () => !this.session.isStreaming,
|
|
491
|
+
abort: () => this.session.abort(),
|
|
492
|
+
hasPendingMessages: () => this.session.pendingMessageCount > 0,
|
|
493
|
+
shutdown: () => {
|
|
494
|
+
this.shutdownRequested = true;
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
// ExtensionCommandContextActions - for ctx.* in command handlers
|
|
498
|
+
{
|
|
499
|
+
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
500
|
+
newSession: async (options) => {
|
|
365
501
|
if (this.loadingAnimation) {
|
|
366
502
|
this.loadingAnimation.stop();
|
|
367
503
|
this.loadingAnimation = undefined;
|
|
368
504
|
}
|
|
369
505
|
this.statusContainer.clear();
|
|
370
|
-
// Create new session
|
|
371
506
|
const success = await this.session.newSession({ parentSession: options?.parentSession });
|
|
372
507
|
if (!success) {
|
|
373
508
|
return { cancelled: true };
|
|
374
509
|
}
|
|
375
|
-
// Call setup callback if provided
|
|
376
510
|
if (options?.setup) {
|
|
377
511
|
await options.setup(this.sessionManager);
|
|
378
512
|
}
|
|
379
|
-
// Clear UI state
|
|
380
513
|
this.chatContainer.clear();
|
|
381
514
|
this.pendingMessagesContainer.clear();
|
|
382
515
|
this.compactionQueuedMessages = [];
|
|
@@ -388,24 +521,22 @@ export class InteractiveMode {
|
|
|
388
521
|
this.ui.requestRender();
|
|
389
522
|
return { cancelled: false };
|
|
390
523
|
},
|
|
391
|
-
|
|
524
|
+
branch: async (entryId) => {
|
|
392
525
|
const result = await this.session.branch(entryId);
|
|
393
526
|
if (result.cancelled) {
|
|
394
527
|
return { cancelled: true };
|
|
395
528
|
}
|
|
396
|
-
// Update UI
|
|
397
529
|
this.chatContainer.clear();
|
|
398
530
|
this.renderInitialMessages();
|
|
399
531
|
this.editor.setText(result.selectedText);
|
|
400
532
|
this.showStatus("Branched to new session");
|
|
401
533
|
return { cancelled: false };
|
|
402
534
|
},
|
|
403
|
-
|
|
535
|
+
navigateTree: async (targetId, options) => {
|
|
404
536
|
const result = await this.session.navigateTree(targetId, { summarize: options?.summarize });
|
|
405
537
|
if (result.cancelled) {
|
|
406
538
|
return { cancelled: true };
|
|
407
539
|
}
|
|
408
|
-
// Update UI
|
|
409
540
|
this.chatContainer.clear();
|
|
410
541
|
this.renderInitialMessages();
|
|
411
542
|
if (result.editorText) {
|
|
@@ -414,24 +545,7 @@ export class InteractiveMode {
|
|
|
414
545
|
this.showStatus("Navigated to selected point");
|
|
415
546
|
return { cancelled: false };
|
|
416
547
|
},
|
|
417
|
-
|
|
418
|
-
const key = await this.session.modelRegistry.getApiKey(model);
|
|
419
|
-
if (!key)
|
|
420
|
-
return false;
|
|
421
|
-
await this.session.setModel(model);
|
|
422
|
-
return true;
|
|
423
|
-
},
|
|
424
|
-
getThinkingLevelHandler: () => this.session.thinkingLevel,
|
|
425
|
-
setThinkingLevelHandler: (level) => this.session.setThinkingLevel(level),
|
|
426
|
-
isIdle: () => !this.session.isStreaming,
|
|
427
|
-
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
428
|
-
abort: () => {
|
|
429
|
-
this.session.abort();
|
|
430
|
-
},
|
|
431
|
-
hasPendingMessages: () => this.session.pendingMessageCount > 0,
|
|
432
|
-
uiContext,
|
|
433
|
-
hasUI: true,
|
|
434
|
-
});
|
|
548
|
+
}, uiContext);
|
|
435
549
|
// Subscribe to extension errors
|
|
436
550
|
extensionRunner.onError((error) => {
|
|
437
551
|
this.showExtensionError(error.extensionPath, error.error, error.stack);
|
|
@@ -445,6 +559,14 @@ export class InteractiveMode {
|
|
|
445
559
|
this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded extensions:\n") + extList, 0, 0));
|
|
446
560
|
this.chatContainer.addChild(new Spacer(1));
|
|
447
561
|
}
|
|
562
|
+
// Warn about built-in tool overrides
|
|
563
|
+
const builtInToolNames = new Set(Object.keys(allTools));
|
|
564
|
+
const registeredTools = extensionRunner.getAllRegisteredTools();
|
|
565
|
+
for (const tool of registeredTools) {
|
|
566
|
+
if (builtInToolNames.has(tool.definition.name)) {
|
|
567
|
+
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: Extension "${tool.extensionPath}" overrides built-in tool "${tool.definition.name}"`), 0, 0));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
448
570
|
// Emit session_start event
|
|
449
571
|
await extensionRunner.emit({
|
|
450
572
|
type: "session_start",
|
|
@@ -476,9 +598,12 @@ export class InteractiveMode {
|
|
|
476
598
|
isIdle: () => !this.session.isStreaming,
|
|
477
599
|
abort: () => this.session.abort(),
|
|
478
600
|
hasPendingMessages: () => this.session.pendingMessageCount > 0,
|
|
601
|
+
shutdown: () => {
|
|
602
|
+
this.shutdownRequested = true;
|
|
603
|
+
},
|
|
479
604
|
});
|
|
480
|
-
// Set up the extension shortcut handler on the editor
|
|
481
|
-
this.
|
|
605
|
+
// Set up the extension shortcut handler on the default editor
|
|
606
|
+
this.defaultEditor.onExtensionShortcut = (data) => {
|
|
482
607
|
for (const [shortcutStr, shortcut] of shortcuts) {
|
|
483
608
|
// Cast to KeyId - extension shortcuts use the same format
|
|
484
609
|
if (matchesKey(data, shortcutStr)) {
|
|
@@ -618,13 +743,28 @@ export class InteractiveMode {
|
|
|
618
743
|
setFooter: (factory) => this.setExtensionFooter(factory),
|
|
619
744
|
setHeader: (factory) => this.setExtensionHeader(factory),
|
|
620
745
|
setTitle: (title) => this.ui.terminal.setTitle(title),
|
|
621
|
-
custom: (factory) => this.showExtensionCustom(factory),
|
|
746
|
+
custom: (factory, options) => this.showExtensionCustom(factory, options),
|
|
622
747
|
setEditorText: (text) => this.editor.setText(text),
|
|
623
748
|
getEditorText: () => this.editor.getText(),
|
|
624
749
|
editor: (title, prefill) => this.showExtensionEditor(title, prefill),
|
|
750
|
+
setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
|
|
625
751
|
get theme() {
|
|
626
752
|
return theme;
|
|
627
753
|
},
|
|
754
|
+
getAllThemes: () => getAvailableThemesWithPaths(),
|
|
755
|
+
getTheme: (name) => getThemeByName(name),
|
|
756
|
+
setTheme: (themeOrName) => {
|
|
757
|
+
if (themeOrName instanceof Theme) {
|
|
758
|
+
setThemeInstance(themeOrName);
|
|
759
|
+
this.ui.requestRender();
|
|
760
|
+
return { success: true };
|
|
761
|
+
}
|
|
762
|
+
const result = setTheme(themeOrName, true);
|
|
763
|
+
if (result.success) {
|
|
764
|
+
this.ui.requestRender();
|
|
765
|
+
}
|
|
766
|
+
return result;
|
|
767
|
+
},
|
|
628
768
|
};
|
|
629
769
|
}
|
|
630
770
|
/**
|
|
@@ -649,7 +789,7 @@ export class InteractiveMode {
|
|
|
649
789
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
650
790
|
this.hideExtensionSelector();
|
|
651
791
|
resolve(undefined);
|
|
652
|
-
});
|
|
792
|
+
}, { tui: this.ui, timeout: opts?.timeout });
|
|
653
793
|
this.editorContainer.clear();
|
|
654
794
|
this.editorContainer.addChild(this.extensionSelector);
|
|
655
795
|
this.ui.setFocus(this.extensionSelector);
|
|
@@ -660,6 +800,7 @@ export class InteractiveMode {
|
|
|
660
800
|
* Hide the extension selector.
|
|
661
801
|
*/
|
|
662
802
|
hideExtensionSelector() {
|
|
803
|
+
this.extensionSelector?.dispose();
|
|
663
804
|
this.editorContainer.clear();
|
|
664
805
|
this.editorContainer.addChild(this.editor);
|
|
665
806
|
this.extensionSelector = undefined;
|
|
@@ -695,7 +836,7 @@ export class InteractiveMode {
|
|
|
695
836
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
696
837
|
this.hideExtensionInput();
|
|
697
838
|
resolve(undefined);
|
|
698
|
-
});
|
|
839
|
+
}, { tui: this.ui, timeout: opts?.timeout });
|
|
699
840
|
this.editorContainer.clear();
|
|
700
841
|
this.editorContainer.addChild(this.extensionInput);
|
|
701
842
|
this.ui.setFocus(this.extensionInput);
|
|
@@ -706,6 +847,7 @@ export class InteractiveMode {
|
|
|
706
847
|
* Hide the extension input.
|
|
707
848
|
*/
|
|
708
849
|
hideExtensionInput() {
|
|
850
|
+
this.extensionInput?.dispose();
|
|
709
851
|
this.editorContainer.clear();
|
|
710
852
|
this.editorContainer.addChild(this.editor);
|
|
711
853
|
this.extensionInput = undefined;
|
|
@@ -740,6 +882,54 @@ export class InteractiveMode {
|
|
|
740
882
|
this.ui.setFocus(this.editor);
|
|
741
883
|
this.ui.requestRender();
|
|
742
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Set a custom editor component from an extension.
|
|
887
|
+
* Pass undefined to restore the default editor.
|
|
888
|
+
*/
|
|
889
|
+
setCustomEditorComponent(factory) {
|
|
890
|
+
// Save text from current editor before switching
|
|
891
|
+
const currentText = this.editor.getText();
|
|
892
|
+
this.editorContainer.clear();
|
|
893
|
+
if (factory) {
|
|
894
|
+
// Create the custom editor with tui, theme, and keybindings
|
|
895
|
+
const newEditor = factory(this.ui, getEditorTheme(), this.keybindings);
|
|
896
|
+
// Wire up callbacks from the default editor
|
|
897
|
+
newEditor.onSubmit = this.defaultEditor.onSubmit;
|
|
898
|
+
newEditor.onChange = this.defaultEditor.onChange;
|
|
899
|
+
// Copy text from previous editor
|
|
900
|
+
newEditor.setText(currentText);
|
|
901
|
+
// Copy appearance settings if supported
|
|
902
|
+
if (newEditor.borderColor !== undefined) {
|
|
903
|
+
newEditor.borderColor = this.defaultEditor.borderColor;
|
|
904
|
+
}
|
|
905
|
+
// Set autocomplete if supported
|
|
906
|
+
if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
|
|
907
|
+
newEditor.setAutocompleteProvider(this.autocompleteProvider);
|
|
908
|
+
}
|
|
909
|
+
// If extending CustomEditor, copy app-level handlers
|
|
910
|
+
// Use duck typing since instanceof fails across jiti module boundaries
|
|
911
|
+
const customEditor = newEditor;
|
|
912
|
+
if ("actionHandlers" in customEditor && customEditor.actionHandlers instanceof Map) {
|
|
913
|
+
customEditor.onEscape = this.defaultEditor.onEscape;
|
|
914
|
+
customEditor.onCtrlD = this.defaultEditor.onCtrlD;
|
|
915
|
+
customEditor.onPasteImage = this.defaultEditor.onPasteImage;
|
|
916
|
+
customEditor.onExtensionShortcut = this.defaultEditor.onExtensionShortcut;
|
|
917
|
+
// Copy action handlers (clear, suspend, model switching, etc.)
|
|
918
|
+
for (const [action, handler] of this.defaultEditor.actionHandlers) {
|
|
919
|
+
customEditor.actionHandlers.set(action, handler);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
this.editor = newEditor;
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
// Restore default editor with text from custom editor
|
|
926
|
+
this.defaultEditor.setText(currentText);
|
|
927
|
+
this.editor = this.defaultEditor;
|
|
928
|
+
}
|
|
929
|
+
this.editorContainer.addChild(this.editor);
|
|
930
|
+
this.ui.setFocus(this.editor);
|
|
931
|
+
this.ui.requestRender();
|
|
932
|
+
}
|
|
743
933
|
/**
|
|
744
934
|
* Show a notification for extensions.
|
|
745
935
|
*/
|
|
@@ -754,28 +944,59 @@ export class InteractiveMode {
|
|
|
754
944
|
this.showStatus(message);
|
|
755
945
|
}
|
|
756
946
|
}
|
|
757
|
-
/**
|
|
758
|
-
|
|
759
|
-
*/
|
|
760
|
-
async showExtensionCustom(factory) {
|
|
947
|
+
/** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
|
|
948
|
+
async showExtensionCustom(factory, options) {
|
|
761
949
|
const savedText = this.editor.getText();
|
|
762
|
-
|
|
950
|
+
const isOverlay = options?.overlay ?? false;
|
|
951
|
+
const restoreEditor = () => {
|
|
952
|
+
this.editorContainer.clear();
|
|
953
|
+
this.editorContainer.addChild(this.editor);
|
|
954
|
+
this.editor.setText(savedText);
|
|
955
|
+
this.ui.setFocus(this.editor);
|
|
956
|
+
this.ui.requestRender();
|
|
957
|
+
};
|
|
958
|
+
return new Promise((resolve, reject) => {
|
|
763
959
|
let component;
|
|
960
|
+
let closed = false;
|
|
764
961
|
const close = (result) => {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
962
|
+
if (closed)
|
|
963
|
+
return;
|
|
964
|
+
closed = true;
|
|
965
|
+
if (isOverlay)
|
|
966
|
+
this.ui.hideOverlay();
|
|
967
|
+
else
|
|
968
|
+
restoreEditor();
|
|
969
|
+
// Note: both branches above already call requestRender
|
|
771
970
|
resolve(result);
|
|
971
|
+
try {
|
|
972
|
+
component?.dispose?.();
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
/* ignore dispose errors */
|
|
976
|
+
}
|
|
772
977
|
};
|
|
773
|
-
Promise.resolve(factory(this.ui, theme, close))
|
|
978
|
+
Promise.resolve(factory(this.ui, theme, this.keybindings, close))
|
|
979
|
+
.then((c) => {
|
|
980
|
+
if (closed)
|
|
981
|
+
return;
|
|
774
982
|
component = c;
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
983
|
+
if (isOverlay) {
|
|
984
|
+
const w = component.width;
|
|
985
|
+
this.ui.showOverlay(component, w ? { width: w } : undefined);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
this.editorContainer.clear();
|
|
989
|
+
this.editorContainer.addChild(component);
|
|
990
|
+
this.ui.setFocus(component);
|
|
991
|
+
this.ui.requestRender();
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
.catch((err) => {
|
|
995
|
+
if (closed)
|
|
996
|
+
return;
|
|
997
|
+
if (!isOverlay)
|
|
998
|
+
restoreEditor();
|
|
999
|
+
reject(err);
|
|
779
1000
|
});
|
|
780
1001
|
});
|
|
781
1002
|
}
|
|
@@ -803,7 +1024,9 @@ export class InteractiveMode {
|
|
|
803
1024
|
// Key Handlers
|
|
804
1025
|
// =========================================================================
|
|
805
1026
|
setupKeyHandlers() {
|
|
806
|
-
this.editor
|
|
1027
|
+
// Set up handlers on defaultEditor - they use this.editor for text access
|
|
1028
|
+
// so they work correctly regardless of which editor is active
|
|
1029
|
+
this.defaultEditor.onEscape = () => {
|
|
807
1030
|
if (this.loadingAnimation) {
|
|
808
1031
|
// Abort and restore queued messages to editor
|
|
809
1032
|
const { steering, followUp } = this.session.clearQueue();
|
|
@@ -841,20 +1064,20 @@ export class InteractiveMode {
|
|
|
841
1064
|
}
|
|
842
1065
|
};
|
|
843
1066
|
// Register app action handlers
|
|
844
|
-
this.
|
|
845
|
-
this.
|
|
846
|
-
this.
|
|
847
|
-
this.
|
|
848
|
-
this.
|
|
849
|
-
this.
|
|
1067
|
+
this.defaultEditor.onAction("clear", () => this.handleCtrlC());
|
|
1068
|
+
this.defaultEditor.onCtrlD = () => this.handleCtrlD();
|
|
1069
|
+
this.defaultEditor.onAction("suspend", () => this.handleCtrlZ());
|
|
1070
|
+
this.defaultEditor.onAction("cycleThinkingLevel", () => this.cycleThinkingLevel());
|
|
1071
|
+
this.defaultEditor.onAction("cycleModelForward", () => this.cycleModel("forward"));
|
|
1072
|
+
this.defaultEditor.onAction("cycleModelBackward", () => this.cycleModel("backward"));
|
|
850
1073
|
// Global debug handler on TUI (works regardless of focus)
|
|
851
1074
|
this.ui.onDebug = () => this.handleDebugCommand();
|
|
852
|
-
this.
|
|
853
|
-
this.
|
|
854
|
-
this.
|
|
855
|
-
this.
|
|
856
|
-
this.
|
|
857
|
-
this.
|
|
1075
|
+
this.defaultEditor.onAction("selectModel", () => this.showModelSelector());
|
|
1076
|
+
this.defaultEditor.onAction("expandTools", () => this.toggleToolOutputExpansion());
|
|
1077
|
+
this.defaultEditor.onAction("toggleThinking", () => this.toggleThinkingBlockVisibility());
|
|
1078
|
+
this.defaultEditor.onAction("externalEditor", () => this.openExternalEditor());
|
|
1079
|
+
this.defaultEditor.onAction("followUp", () => this.handleFollowUp());
|
|
1080
|
+
this.defaultEditor.onChange = (text) => {
|
|
858
1081
|
const wasBashMode = this.isBashMode;
|
|
859
1082
|
this.isBashMode = text.trimStart().startsWith("!");
|
|
860
1083
|
if (wasBashMode !== this.isBashMode) {
|
|
@@ -862,7 +1085,7 @@ export class InteractiveMode {
|
|
|
862
1085
|
}
|
|
863
1086
|
};
|
|
864
1087
|
// Handle clipboard image paste (triggered on Ctrl+V)
|
|
865
|
-
this.
|
|
1088
|
+
this.defaultEditor.onPasteImage = () => {
|
|
866
1089
|
this.handleClipboardImagePaste();
|
|
867
1090
|
};
|
|
868
1091
|
}
|
|
@@ -879,7 +1102,7 @@ export class InteractiveMode {
|
|
|
879
1102
|
const filePath = path.join(tmpDir, fileName);
|
|
880
1103
|
fs.writeFileSync(filePath, Buffer.from(image.bytes));
|
|
881
1104
|
// Insert file path directly
|
|
882
|
-
this.editor.insertTextAtCursor(filePath);
|
|
1105
|
+
this.editor.insertTextAtCursor?.(filePath);
|
|
883
1106
|
this.ui.requestRender();
|
|
884
1107
|
}
|
|
885
1108
|
catch {
|
|
@@ -887,7 +1110,7 @@ export class InteractiveMode {
|
|
|
887
1110
|
}
|
|
888
1111
|
}
|
|
889
1112
|
setupEditorSubmitHandler() {
|
|
890
|
-
this.
|
|
1113
|
+
this.defaultEditor.onSubmit = async (text) => {
|
|
891
1114
|
text = text.trim();
|
|
892
1115
|
if (!text)
|
|
893
1116
|
return;
|
|
@@ -993,7 +1216,7 @@ export class InteractiveMode {
|
|
|
993
1216
|
this.editor.setText(text);
|
|
994
1217
|
return;
|
|
995
1218
|
}
|
|
996
|
-
this.editor.addToHistory(text);
|
|
1219
|
+
this.editor.addToHistory?.(text);
|
|
997
1220
|
await this.handleBashCommand(command, isExcluded);
|
|
998
1221
|
this.isBashMode = false;
|
|
999
1222
|
this.updateEditorBorderColor();
|
|
@@ -1003,7 +1226,7 @@ export class InteractiveMode {
|
|
|
1003
1226
|
// Queue input during compaction (extension commands execute immediately)
|
|
1004
1227
|
if (this.session.isCompacting) {
|
|
1005
1228
|
if (this.isExtensionCommand(text)) {
|
|
1006
|
-
this.editor.addToHistory(text);
|
|
1229
|
+
this.editor.addToHistory?.(text);
|
|
1007
1230
|
this.editor.setText("");
|
|
1008
1231
|
await this.session.prompt(text);
|
|
1009
1232
|
}
|
|
@@ -1015,7 +1238,7 @@ export class InteractiveMode {
|
|
|
1015
1238
|
// If streaming, use prompt() with steer behavior
|
|
1016
1239
|
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
|
1017
1240
|
if (this.session.isStreaming) {
|
|
1018
|
-
this.editor.addToHistory(text);
|
|
1241
|
+
this.editor.addToHistory?.(text);
|
|
1019
1242
|
this.editor.setText("");
|
|
1020
1243
|
await this.session.prompt(text, { streamingBehavior: "steer" });
|
|
1021
1244
|
this.updatePendingMessagesDisplay();
|
|
@@ -1028,7 +1251,7 @@ export class InteractiveMode {
|
|
|
1028
1251
|
if (this.onInputCallback) {
|
|
1029
1252
|
this.onInputCallback(text);
|
|
1030
1253
|
}
|
|
1031
|
-
this.editor.addToHistory(text);
|
|
1254
|
+
this.editor.addToHistory?.(text);
|
|
1032
1255
|
};
|
|
1033
1256
|
}
|
|
1034
1257
|
subscribeToAgent() {
|
|
@@ -1043,6 +1266,16 @@ export class InteractiveMode {
|
|
|
1043
1266
|
this.footer.invalidate();
|
|
1044
1267
|
switch (event.type) {
|
|
1045
1268
|
case "agent_start":
|
|
1269
|
+
// Restore main escape handler if retry handler is still active
|
|
1270
|
+
// (retry success event fires later, but we need main handler now)
|
|
1271
|
+
if (this.retryEscapeHandler) {
|
|
1272
|
+
this.defaultEditor.onEscape = this.retryEscapeHandler;
|
|
1273
|
+
this.retryEscapeHandler = undefined;
|
|
1274
|
+
}
|
|
1275
|
+
if (this.retryLoader) {
|
|
1276
|
+
this.retryLoader.stop();
|
|
1277
|
+
this.retryLoader = undefined;
|
|
1278
|
+
}
|
|
1046
1279
|
if (this.loadingAnimation) {
|
|
1047
1280
|
this.loadingAnimation.stop();
|
|
1048
1281
|
}
|
|
@@ -1100,11 +1333,20 @@ export class InteractiveMode {
|
|
|
1100
1333
|
break;
|
|
1101
1334
|
if (this.streamingComponent && event.message.role === "assistant") {
|
|
1102
1335
|
this.streamingMessage = event.message;
|
|
1336
|
+
let errorMessage;
|
|
1337
|
+
if (this.streamingMessage.stopReason === "aborted") {
|
|
1338
|
+
const retryAttempt = this.session.retryAttempt;
|
|
1339
|
+
errorMessage =
|
|
1340
|
+
retryAttempt > 0
|
|
1341
|
+
? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
|
|
1342
|
+
: "Operation aborted";
|
|
1343
|
+
this.streamingMessage.errorMessage = errorMessage;
|
|
1344
|
+
}
|
|
1103
1345
|
this.streamingComponent.updateContent(this.streamingMessage);
|
|
1104
1346
|
if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1347
|
+
if (!errorMessage) {
|
|
1348
|
+
errorMessage = this.streamingMessage.errorMessage || "Error";
|
|
1349
|
+
}
|
|
1108
1350
|
for (const [, component] of this.pendingTools.entries()) {
|
|
1109
1351
|
component.updateResult({
|
|
1110
1352
|
content: [{ type: "text", text: errorMessage }],
|
|
@@ -1166,13 +1408,14 @@ export class InteractiveMode {
|
|
|
1166
1408
|
this.streamingMessage = undefined;
|
|
1167
1409
|
}
|
|
1168
1410
|
this.pendingTools.clear();
|
|
1411
|
+
await this.checkShutdownRequested();
|
|
1169
1412
|
this.ui.requestRender();
|
|
1170
1413
|
break;
|
|
1171
1414
|
case "auto_compaction_start": {
|
|
1172
1415
|
// Keep editor active; submissions are queued during compaction.
|
|
1173
1416
|
// Set up escape to abort auto-compaction
|
|
1174
|
-
this.autoCompactionEscapeHandler = this.
|
|
1175
|
-
this.
|
|
1417
|
+
this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
|
|
1418
|
+
this.defaultEditor.onEscape = () => {
|
|
1176
1419
|
this.session.abortCompaction();
|
|
1177
1420
|
};
|
|
1178
1421
|
// Show compacting indicator with reason
|
|
@@ -1186,7 +1429,7 @@ export class InteractiveMode {
|
|
|
1186
1429
|
case "auto_compaction_end": {
|
|
1187
1430
|
// Restore escape handler
|
|
1188
1431
|
if (this.autoCompactionEscapeHandler) {
|
|
1189
|
-
this.
|
|
1432
|
+
this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
|
|
1190
1433
|
this.autoCompactionEscapeHandler = undefined;
|
|
1191
1434
|
}
|
|
1192
1435
|
// Stop loader
|
|
@@ -1218,8 +1461,8 @@ export class InteractiveMode {
|
|
|
1218
1461
|
}
|
|
1219
1462
|
case "auto_retry_start": {
|
|
1220
1463
|
// Set up escape to abort retry
|
|
1221
|
-
this.retryEscapeHandler = this.
|
|
1222
|
-
this.
|
|
1464
|
+
this.retryEscapeHandler = this.defaultEditor.onEscape;
|
|
1465
|
+
this.defaultEditor.onEscape = () => {
|
|
1223
1466
|
this.session.abortRetry();
|
|
1224
1467
|
};
|
|
1225
1468
|
// Show retry indicator
|
|
@@ -1233,7 +1476,7 @@ export class InteractiveMode {
|
|
|
1233
1476
|
case "auto_retry_end": {
|
|
1234
1477
|
// Restore escape handler
|
|
1235
1478
|
if (this.retryEscapeHandler) {
|
|
1236
|
-
this.
|
|
1479
|
+
this.defaultEditor.onEscape = this.retryEscapeHandler;
|
|
1237
1480
|
this.retryEscapeHandler = undefined;
|
|
1238
1481
|
}
|
|
1239
1482
|
// Stop loader
|
|
@@ -1321,7 +1564,7 @@ export class InteractiveMode {
|
|
|
1321
1564
|
const userComponent = new UserMessageComponent(textContent);
|
|
1322
1565
|
this.chatContainer.addChild(userComponent);
|
|
1323
1566
|
if (options?.populateHistory) {
|
|
1324
|
-
this.editor.addToHistory(textContent);
|
|
1567
|
+
this.editor.addToHistory?.(textContent);
|
|
1325
1568
|
}
|
|
1326
1569
|
}
|
|
1327
1570
|
break;
|
|
@@ -1363,7 +1606,17 @@ export class InteractiveMode {
|
|
|
1363
1606
|
component.setExpanded(this.toolOutputExpanded);
|
|
1364
1607
|
this.chatContainer.addChild(component);
|
|
1365
1608
|
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
1366
|
-
|
|
1609
|
+
let errorMessage;
|
|
1610
|
+
if (message.stopReason === "aborted") {
|
|
1611
|
+
const retryAttempt = this.session.retryAttempt;
|
|
1612
|
+
errorMessage =
|
|
1613
|
+
retryAttempt > 0
|
|
1614
|
+
? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
|
|
1615
|
+
: "Operation aborted";
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
errorMessage = message.errorMessage || "Error";
|
|
1619
|
+
}
|
|
1367
1620
|
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
1368
1621
|
}
|
|
1369
1622
|
else {
|
|
@@ -1437,7 +1690,11 @@ export class InteractiveMode {
|
|
|
1437
1690
|
* Gracefully shutdown the agent.
|
|
1438
1691
|
* Emits shutdown event to extensions, then exits.
|
|
1439
1692
|
*/
|
|
1693
|
+
isShuttingDown = false;
|
|
1440
1694
|
async shutdown() {
|
|
1695
|
+
if (this.isShuttingDown)
|
|
1696
|
+
return;
|
|
1697
|
+
this.isShuttingDown = true;
|
|
1441
1698
|
// Emit shutdown event to extensions
|
|
1442
1699
|
const extensionRunner = this.session.extensionRunner;
|
|
1443
1700
|
if (extensionRunner?.hasHandlers("session_shutdown")) {
|
|
@@ -1448,6 +1705,14 @@ export class InteractiveMode {
|
|
|
1448
1705
|
this.stop();
|
|
1449
1706
|
process.exit(0);
|
|
1450
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Check if shutdown was requested and perform shutdown if so.
|
|
1710
|
+
*/
|
|
1711
|
+
async checkShutdownRequested() {
|
|
1712
|
+
if (!this.shutdownRequested)
|
|
1713
|
+
return;
|
|
1714
|
+
await this.shutdown();
|
|
1715
|
+
}
|
|
1451
1716
|
handleCtrlZ() {
|
|
1452
1717
|
// Set up handler to restore TUI when resumed
|
|
1453
1718
|
process.once("SIGCONT", () => {
|
|
@@ -1466,7 +1731,7 @@ export class InteractiveMode {
|
|
|
1466
1731
|
// Queue input during compaction (extension commands execute immediately)
|
|
1467
1732
|
if (this.session.isCompacting) {
|
|
1468
1733
|
if (this.isExtensionCommand(text)) {
|
|
1469
|
-
this.editor.addToHistory(text);
|
|
1734
|
+
this.editor.addToHistory?.(text);
|
|
1470
1735
|
this.editor.setText("");
|
|
1471
1736
|
await this.session.prompt(text);
|
|
1472
1737
|
}
|
|
@@ -1478,7 +1743,7 @@ export class InteractiveMode {
|
|
|
1478
1743
|
// Alt+Enter queues a follow-up message (waits until agent finishes)
|
|
1479
1744
|
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
|
1480
1745
|
if (this.session.isStreaming) {
|
|
1481
|
-
this.editor.addToHistory(text);
|
|
1746
|
+
this.editor.addToHistory?.(text);
|
|
1482
1747
|
this.editor.setText("");
|
|
1483
1748
|
await this.session.prompt(text, { streamingBehavior: "followUp" });
|
|
1484
1749
|
this.updatePendingMessagesDisplay();
|
|
@@ -1558,7 +1823,7 @@ export class InteractiveMode {
|
|
|
1558
1823
|
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
1559
1824
|
return;
|
|
1560
1825
|
}
|
|
1561
|
-
const currentText = this.editor.getExpandedText();
|
|
1826
|
+
const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
|
|
1562
1827
|
const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
|
|
1563
1828
|
try {
|
|
1564
1829
|
// Write current content to temp file
|
|
@@ -1609,12 +1874,14 @@ export class InteractiveMode {
|
|
|
1609
1874
|
this.ui.requestRender();
|
|
1610
1875
|
}
|
|
1611
1876
|
showNewVersionNotification(newVersion) {
|
|
1877
|
+
const updateInstruction = isBunBinary
|
|
1878
|
+
? theme.fg("muted", `New version ${newVersion} is available. Download from: `) +
|
|
1879
|
+
theme.fg("accent", "https://github.com/badlogic/pi-mono/releases/latest")
|
|
1880
|
+
: theme.fg("muted", `New version ${newVersion} is available. Run: `) +
|
|
1881
|
+
theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent");
|
|
1612
1882
|
this.chatContainer.addChild(new Spacer(1));
|
|
1613
1883
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
1614
|
-
this.chatContainer.addChild(new Text(theme.bold(theme.fg("warning", "Update Available"))
|
|
1615
|
-
"\n" +
|
|
1616
|
-
theme.fg("muted", `New version ${newVersion} is available. Run: `) +
|
|
1617
|
-
theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent"), 1, 0));
|
|
1884
|
+
this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0));
|
|
1618
1885
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
1619
1886
|
this.ui.requestRender();
|
|
1620
1887
|
}
|
|
@@ -1642,7 +1909,7 @@ export class InteractiveMode {
|
|
|
1642
1909
|
}
|
|
1643
1910
|
queueCompactionMessage(text, mode) {
|
|
1644
1911
|
this.compactionQueuedMessages.push({ text, mode });
|
|
1645
|
-
this.editor.addToHistory(text);
|
|
1912
|
+
this.editor.addToHistory?.(text);
|
|
1646
1913
|
this.editor.setText("");
|
|
1647
1914
|
this.updatePendingMessagesDisplay();
|
|
1648
1915
|
this.showStatus("Queued message for after compaction");
|
|
@@ -1917,9 +2184,9 @@ export class InteractiveMode {
|
|
|
1917
2184
|
const wantsSummary = await this.showExtensionConfirm("Summarize branch?", "Create a summary of the branch you're leaving?");
|
|
1918
2185
|
// Set up escape handler and loader if summarizing
|
|
1919
2186
|
let summaryLoader;
|
|
1920
|
-
const originalOnEscape = this.
|
|
2187
|
+
const originalOnEscape = this.defaultEditor.onEscape;
|
|
1921
2188
|
if (wantsSummary) {
|
|
1922
|
-
this.
|
|
2189
|
+
this.defaultEditor.onEscape = () => {
|
|
1923
2190
|
this.session.abortBranchSummary();
|
|
1924
2191
|
};
|
|
1925
2192
|
this.chatContainer.addChild(new Spacer(1));
|
|
@@ -1955,7 +2222,7 @@ export class InteractiveMode {
|
|
|
1955
2222
|
summaryLoader.stop();
|
|
1956
2223
|
this.statusContainer.clear();
|
|
1957
2224
|
}
|
|
1958
|
-
this.
|
|
2225
|
+
this.defaultEditor.onEscape = originalOnEscape;
|
|
1959
2226
|
}
|
|
1960
2227
|
}, () => {
|
|
1961
2228
|
done();
|
|
@@ -2438,6 +2705,40 @@ export class InteractiveMode {
|
|
|
2438
2705
|
this.ui.requestRender();
|
|
2439
2706
|
}
|
|
2440
2707
|
async handleBashCommand(command, excludeFromContext = false) {
|
|
2708
|
+
const extensionRunner = this.session.extensionRunner;
|
|
2709
|
+
// Emit user_bash event to let extensions intercept
|
|
2710
|
+
const eventResult = extensionRunner
|
|
2711
|
+
? await extensionRunner.emitUserBash({
|
|
2712
|
+
type: "user_bash",
|
|
2713
|
+
command,
|
|
2714
|
+
excludeFromContext,
|
|
2715
|
+
cwd: process.cwd(),
|
|
2716
|
+
})
|
|
2717
|
+
: undefined;
|
|
2718
|
+
// If extension returned a full result, use it directly
|
|
2719
|
+
if (eventResult?.result) {
|
|
2720
|
+
const result = eventResult.result;
|
|
2721
|
+
// Create UI component for display
|
|
2722
|
+
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
2723
|
+
if (this.session.isStreaming) {
|
|
2724
|
+
this.pendingMessagesContainer.addChild(this.bashComponent);
|
|
2725
|
+
this.pendingBashComponents.push(this.bashComponent);
|
|
2726
|
+
}
|
|
2727
|
+
else {
|
|
2728
|
+
this.chatContainer.addChild(this.bashComponent);
|
|
2729
|
+
}
|
|
2730
|
+
// Show output and complete
|
|
2731
|
+
if (result.output) {
|
|
2732
|
+
this.bashComponent.appendOutput(result.output);
|
|
2733
|
+
}
|
|
2734
|
+
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
|
|
2735
|
+
// Record the result in session
|
|
2736
|
+
this.session.recordBashResult(command, result, { excludeFromContext });
|
|
2737
|
+
this.bashComponent = undefined;
|
|
2738
|
+
this.ui.requestRender();
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
// Normal execution path (possibly with custom operations)
|
|
2441
2742
|
const isDeferred = this.session.isStreaming;
|
|
2442
2743
|
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
2443
2744
|
if (isDeferred) {
|
|
@@ -2456,7 +2757,7 @@ export class InteractiveMode {
|
|
|
2456
2757
|
this.bashComponent.appendOutput(chunk);
|
|
2457
2758
|
this.ui.requestRender();
|
|
2458
2759
|
}
|
|
2459
|
-
}, { excludeFromContext });
|
|
2760
|
+
}, { excludeFromContext, operations: eventResult?.operations });
|
|
2460
2761
|
if (this.bashComponent) {
|
|
2461
2762
|
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
|
|
2462
2763
|
}
|
|
@@ -2487,8 +2788,8 @@ export class InteractiveMode {
|
|
|
2487
2788
|
}
|
|
2488
2789
|
this.statusContainer.clear();
|
|
2489
2790
|
// Set up escape handler during compaction
|
|
2490
|
-
const originalOnEscape = this.
|
|
2491
|
-
this.
|
|
2791
|
+
const originalOnEscape = this.defaultEditor.onEscape;
|
|
2792
|
+
this.defaultEditor.onEscape = () => {
|
|
2492
2793
|
this.session.abortCompaction();
|
|
2493
2794
|
};
|
|
2494
2795
|
// Show compacting status
|
|
@@ -2518,7 +2819,7 @@ export class InteractiveMode {
|
|
|
2518
2819
|
finally {
|
|
2519
2820
|
compactingLoader.stop();
|
|
2520
2821
|
this.statusContainer.clear();
|
|
2521
|
-
this.
|
|
2822
|
+
this.defaultEditor.onEscape = originalOnEscape;
|
|
2522
2823
|
}
|
|
2523
2824
|
void this.flushCompactionQueue({ willRetry: false });
|
|
2524
2825
|
}
|