@mariozechner/pi-coding-agent 0.42.5 → 0.44.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 +52 -0
- package/README.md +16 -8
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +1 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/cli/session-picker.d.ts +4 -2
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +3 -3
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/core/agent-session.d.ts +19 -10
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +43 -18
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +3 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/extensions/index.d.ts +2 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +8 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +2 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +11 -5
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +38 -17
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +10 -4
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +179 -66
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +15 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +13 -12
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +8 -8
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -3
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +47 -0
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/scoped-models-selector.js +241 -0
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts +17 -3
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +192 -39
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +4 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +14 -2
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +2 -2
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +8 -7
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +7 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +263 -30
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +22 -8
- 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 +9 -3
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +4 -4
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +6 -6
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +18 -9
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +4 -4
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/extensions.md +64 -10
- package/docs/rpc.md +10 -10
- package/docs/sdk.md +10 -5
- package/docs/session.md +13 -1
- package/docs/skills.md +27 -0
- package/docs/tree.md +9 -5
- package/docs/tui.md +3 -0
- package/examples/extensions/README.md +4 -3
- package/examples/extensions/confirm-destructive.ts +5 -5
- package/examples/extensions/dirty-repo-guard.ts +2 -2
- package/examples/extensions/git-checkpoint.ts +3 -3
- package/examples/extensions/handoff.ts +1 -1
- package/examples/extensions/model-status.ts +31 -0
- package/examples/extensions/notify.ts +25 -0
- package/examples/extensions/preset.ts +3 -3
- package/examples/extensions/todo.ts +1 -1
- package/examples/extensions/tools.ts +9 -8
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +4 -4
- package/dist/utils/fuzzy.d.ts +0 -7
- package/dist/utils/fuzzy.d.ts.map +0 -1
- package/dist/utils/fuzzy.js +0 -86
- package/dist/utils/fuzzy.js.map +0 -1
|
@@ -7,18 +7,18 @@ 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 { getOAuthProviders, } from "@mariozechner/pi-ai";
|
|
10
|
-
import { CombinedAutocompleteProvider, Container, getEditorKeybindings, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
10
|
+
import { CombinedAutocompleteProvider, Container, fuzzyFilter, getEditorKeybindings, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
11
11
|
import { spawn, spawnSync } from "child_process";
|
|
12
12
|
import { APP_NAME, getAuthPath, getDebugLogPath, isBunBinary, VERSION } from "../../config.js";
|
|
13
13
|
import { FooterDataProvider } from "../../core/footer-data-provider.js";
|
|
14
14
|
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
15
15
|
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|
16
|
+
import { resolveModelScope } from "../../core/model-resolver.js";
|
|
16
17
|
import { SessionManager } from "../../core/session-manager.js";
|
|
17
18
|
import { loadProjectContextFiles } from "../../core/system-prompt.js";
|
|
18
19
|
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
|
19
20
|
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
20
21
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
|
21
|
-
import { fuzzyFilter } from "../../utils/fuzzy.js";
|
|
22
22
|
import { ensureTool } from "../../utils/tools-manager.js";
|
|
23
23
|
import { ArminComponent } from "./components/armin.js";
|
|
24
24
|
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
|
@@ -36,6 +36,7 @@ import { FooterComponent } from "./components/footer.js";
|
|
|
36
36
|
import { LoginDialogComponent } from "./components/login-dialog.js";
|
|
37
37
|
import { ModelSelectorComponent } from "./components/model-selector.js";
|
|
38
38
|
import { OAuthSelectorComponent } from "./components/oauth-selector.js";
|
|
39
|
+
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
|
|
39
40
|
import { SessionSelectorComponent } from "./components/session-selector.js";
|
|
40
41
|
import { SettingsSelectorComponent } from "./components/settings-selector.js";
|
|
41
42
|
import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
@@ -56,6 +57,7 @@ export class InteractiveMode {
|
|
|
56
57
|
defaultEditor;
|
|
57
58
|
editor;
|
|
58
59
|
autocompleteProvider;
|
|
60
|
+
fdPath;
|
|
59
61
|
editorContainer;
|
|
60
62
|
footer;
|
|
61
63
|
footerDataProvider;
|
|
@@ -64,6 +66,7 @@ export class InteractiveMode {
|
|
|
64
66
|
isInitialized = false;
|
|
65
67
|
onInputCallback;
|
|
66
68
|
loadingAnimation = undefined;
|
|
69
|
+
defaultWorkingMessage = "Working... (esc to interrupt)";
|
|
67
70
|
lastSigintTime = 0;
|
|
68
71
|
lastEscapeTime = 0;
|
|
69
72
|
changelogMarkdown = undefined;
|
|
@@ -79,6 +82,8 @@ export class InteractiveMode {
|
|
|
79
82
|
toolOutputExpanded = false;
|
|
80
83
|
// Thinking block visibility state
|
|
81
84
|
hideThinkingBlock = false;
|
|
85
|
+
// Skill commands: command name -> skill file path
|
|
86
|
+
skillCommands = new Map();
|
|
82
87
|
// Agent subscription unsubscribe function
|
|
83
88
|
unsubscribe;
|
|
84
89
|
// Track if editor is in bash mode (text starts with !)
|
|
@@ -162,8 +167,8 @@ export class InteractiveMode {
|
|
|
162
167
|
provider: m.provider,
|
|
163
168
|
label: `${m.provider}/${m.id}`,
|
|
164
169
|
}));
|
|
165
|
-
// Fuzzy filter by model ID
|
|
166
|
-
const filtered = fuzzyFilter(items, prefix, (item) => item.id);
|
|
170
|
+
// Fuzzy filter by model ID + provider (allows "opus anthropic" to match)
|
|
171
|
+
const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);
|
|
167
172
|
if (filtered.length === 0)
|
|
168
173
|
return null;
|
|
169
174
|
return filtered.map((item) => ({
|
|
@@ -173,13 +178,15 @@ export class InteractiveMode {
|
|
|
173
178
|
}));
|
|
174
179
|
},
|
|
175
180
|
},
|
|
181
|
+
{ name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
|
|
176
182
|
{ name: "export", description: "Export session to HTML file" },
|
|
177
183
|
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
178
184
|
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
185
|
+
{ name: "name", description: "Set session display name" },
|
|
179
186
|
{ name: "session", description: "Show session info and stats" },
|
|
180
187
|
{ name: "changelog", description: "Show changelog entries" },
|
|
181
188
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
182
|
-
{ name: "
|
|
189
|
+
{ name: "fork", description: "Create a new fork from a previous message" },
|
|
183
190
|
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
184
191
|
{ name: "login", description: "Login with OAuth provider" },
|
|
185
192
|
{ name: "logout", description: "Logout from OAuth provider" },
|
|
@@ -197,18 +204,31 @@ export class InteractiveMode {
|
|
|
197
204
|
name: cmd.name,
|
|
198
205
|
description: cmd.description ?? "(extension command)",
|
|
199
206
|
}));
|
|
207
|
+
// Build skill commands from session.skills (if enabled)
|
|
208
|
+
this.skillCommands.clear();
|
|
209
|
+
const skillCommandList = [];
|
|
210
|
+
if (this.settingsManager.getEnableSkillCommands()) {
|
|
211
|
+
for (const skill of this.session.skills) {
|
|
212
|
+
const commandName = `skill:${skill.name}`;
|
|
213
|
+
this.skillCommands.set(commandName, skill.filePath);
|
|
214
|
+
skillCommandList.push({ name: commandName, description: skill.description });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
200
217
|
// Setup autocomplete
|
|
201
|
-
this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands], process.cwd(), fdPath);
|
|
218
|
+
this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], process.cwd(), fdPath);
|
|
202
219
|
this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
|
|
203
220
|
}
|
|
221
|
+
rebuildAutocomplete() {
|
|
222
|
+
this.setupAutocomplete(this.fdPath);
|
|
223
|
+
}
|
|
204
224
|
async init() {
|
|
205
225
|
if (this.isInitialized)
|
|
206
226
|
return;
|
|
207
227
|
// Load changelog (only show new entries, skip for resumed sessions)
|
|
208
228
|
this.changelogMarkdown = this.getChangelogForDisplay();
|
|
209
229
|
// Setup autocomplete with fd tool for file path completion
|
|
210
|
-
|
|
211
|
-
this.setupAutocomplete(fdPath);
|
|
230
|
+
this.fdPath = await ensureTool("fd");
|
|
231
|
+
this.setupAutocomplete(this.fdPath);
|
|
212
232
|
// Add header with keybindings from config
|
|
213
233
|
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
214
234
|
// Format keybinding for startup display (lowercase, compact)
|
|
@@ -280,7 +300,7 @@ export class InteractiveMode {
|
|
|
280
300
|
theme.fg("muted", " to queue follow-up") +
|
|
281
301
|
"\n" +
|
|
282
302
|
theme.fg("dim", dequeue) +
|
|
283
|
-
theme.fg("muted", " to
|
|
303
|
+
theme.fg("muted", " to edit all queued messages") +
|
|
284
304
|
"\n" +
|
|
285
305
|
theme.fg("dim", "ctrl+v") +
|
|
286
306
|
theme.fg("muted", " to paste image") +
|
|
@@ -313,6 +333,7 @@ export class InteractiveMode {
|
|
|
313
333
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
314
334
|
this.ui.addChild(this.statusContainer);
|
|
315
335
|
this.ui.addChild(this.widgetContainer);
|
|
336
|
+
this.renderWidgets(); // Initialize with default spacer
|
|
316
337
|
this.ui.addChild(this.editorContainer);
|
|
317
338
|
this.ui.addChild(this.footer);
|
|
318
339
|
this.ui.setFocus(this.editor);
|
|
@@ -503,8 +524,14 @@ export class InteractiveMode {
|
|
|
503
524
|
appendEntry: (customType, data) => {
|
|
504
525
|
this.sessionManager.appendCustomEntry(customType, data);
|
|
505
526
|
},
|
|
527
|
+
setSessionName: (name) => {
|
|
528
|
+
this.sessionManager.appendSessionInfo(name);
|
|
529
|
+
},
|
|
530
|
+
getSessionName: () => {
|
|
531
|
+
return this.sessionManager.getSessionName();
|
|
532
|
+
},
|
|
506
533
|
getActiveTools: () => this.session.getActiveToolNames(),
|
|
507
|
-
getAllTools: () => this.session.
|
|
534
|
+
getAllTools: () => this.session.getAllTools(),
|
|
508
535
|
setActiveTools: (toolNames) => this.session.setActiveToolsByName(toolNames),
|
|
509
536
|
setModel: async (model) => {
|
|
510
537
|
const key = await this.session.modelRegistry.getApiKey(model);
|
|
@@ -553,15 +580,15 @@ export class InteractiveMode {
|
|
|
553
580
|
this.ui.requestRender();
|
|
554
581
|
return { cancelled: false };
|
|
555
582
|
},
|
|
556
|
-
|
|
557
|
-
const result = await this.session.
|
|
583
|
+
fork: async (entryId) => {
|
|
584
|
+
const result = await this.session.fork(entryId);
|
|
558
585
|
if (result.cancelled) {
|
|
559
586
|
return { cancelled: true };
|
|
560
587
|
}
|
|
561
588
|
this.chatContainer.clear();
|
|
562
589
|
this.renderInitialMessages();
|
|
563
590
|
this.editor.setText(result.selectedText);
|
|
564
|
-
this.showStatus("
|
|
591
|
+
this.showStatus("Forked to new session");
|
|
565
592
|
return { cancelled: false };
|
|
566
593
|
},
|
|
567
594
|
navigateTree: async (targetId, options) => {
|
|
@@ -765,6 +792,11 @@ export class InteractiveMode {
|
|
|
765
792
|
input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
|
|
766
793
|
notify: (message, type) => this.showExtensionNotify(message, type),
|
|
767
794
|
setStatus: (key, text) => this.setExtensionStatus(key, text),
|
|
795
|
+
setWorkingMessage: (message) => {
|
|
796
|
+
if (this.loadingAnimation) {
|
|
797
|
+
this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
|
|
798
|
+
}
|
|
799
|
+
},
|
|
768
800
|
setWidget: (key, content) => this.setExtensionWidget(key, content),
|
|
769
801
|
setFooter: (factory) => this.setExtensionFooter(factory),
|
|
770
802
|
setHeader: (factory) => this.setExtensionHeader(factory),
|
|
@@ -1065,7 +1097,7 @@ export class InteractiveMode {
|
|
|
1065
1097
|
this.updateEditorBorderColor();
|
|
1066
1098
|
}
|
|
1067
1099
|
else if (!this.editor.getText().trim()) {
|
|
1068
|
-
// Double-escape with empty editor triggers /tree or /
|
|
1100
|
+
// Double-escape with empty editor triggers /tree or /fork based on setting
|
|
1069
1101
|
const now = Date.now();
|
|
1070
1102
|
if (now - this.lastEscapeTime < 500) {
|
|
1071
1103
|
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
@@ -1139,6 +1171,11 @@ export class InteractiveMode {
|
|
|
1139
1171
|
this.editor.setText("");
|
|
1140
1172
|
return;
|
|
1141
1173
|
}
|
|
1174
|
+
if (text === "/scoped-models") {
|
|
1175
|
+
this.editor.setText("");
|
|
1176
|
+
await this.showModelsSelector();
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1142
1179
|
if (text === "/model" || text.startsWith("/model ")) {
|
|
1143
1180
|
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
|
|
1144
1181
|
this.editor.setText("");
|
|
@@ -1160,6 +1197,11 @@ export class InteractiveMode {
|
|
|
1160
1197
|
this.editor.setText("");
|
|
1161
1198
|
return;
|
|
1162
1199
|
}
|
|
1200
|
+
if (text === "/name" || text.startsWith("/name ")) {
|
|
1201
|
+
this.handleNameCommand(text);
|
|
1202
|
+
this.editor.setText("");
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1163
1205
|
if (text === "/session") {
|
|
1164
1206
|
this.handleSessionCommand();
|
|
1165
1207
|
this.editor.setText("");
|
|
@@ -1175,7 +1217,7 @@ export class InteractiveMode {
|
|
|
1175
1217
|
this.editor.setText("");
|
|
1176
1218
|
return;
|
|
1177
1219
|
}
|
|
1178
|
-
if (text === "/
|
|
1220
|
+
if (text === "/fork") {
|
|
1179
1221
|
this.showUserMessageSelector();
|
|
1180
1222
|
this.editor.setText("");
|
|
1181
1223
|
return;
|
|
@@ -1226,6 +1268,19 @@ export class InteractiveMode {
|
|
|
1226
1268
|
await this.shutdown();
|
|
1227
1269
|
return;
|
|
1228
1270
|
}
|
|
1271
|
+
// Handle skill commands (/skill:name [args])
|
|
1272
|
+
if (text.startsWith("/skill:")) {
|
|
1273
|
+
const spaceIndex = text.indexOf(" ");
|
|
1274
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
1275
|
+
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
1276
|
+
const skillPath = this.skillCommands.get(commandName);
|
|
1277
|
+
if (skillPath) {
|
|
1278
|
+
this.editor.addToHistory?.(text);
|
|
1279
|
+
this.editor.setText("");
|
|
1280
|
+
await this.handleSkillCommand(skillPath, args);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1229
1284
|
// Handle bash command (! for normal, !! for excluded from context)
|
|
1230
1285
|
if (text.startsWith("!")) {
|
|
1231
1286
|
const isExcluded = text.startsWith("!!");
|
|
@@ -1300,7 +1355,7 @@ export class InteractiveMode {
|
|
|
1300
1355
|
this.loadingAnimation.stop();
|
|
1301
1356
|
}
|
|
1302
1357
|
this.statusContainer.clear();
|
|
1303
|
-
this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text),
|
|
1358
|
+
this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
|
|
1304
1359
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
1305
1360
|
this.ui.requestRender();
|
|
1306
1361
|
break;
|
|
@@ -1882,7 +1937,8 @@ export class InteractiveMode {
|
|
|
1882
1937
|
}
|
|
1883
1938
|
// Restart TUI
|
|
1884
1939
|
this.ui.start();
|
|
1885
|
-
|
|
1940
|
+
// Force full re-render since external editor uses alternate screen
|
|
1941
|
+
this.ui.requestRender(true);
|
|
1886
1942
|
}
|
|
1887
1943
|
}
|
|
1888
1944
|
// =========================================================================
|
|
@@ -1934,6 +1990,9 @@ export class InteractiveMode {
|
|
|
1934
1990
|
const text = theme.fg("dim", `Follow-up: ${message}`);
|
|
1935
1991
|
this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
|
|
1936
1992
|
}
|
|
1993
|
+
const dequeueHint = this.getAppKeyDisplay("dequeue");
|
|
1994
|
+
const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
|
|
1995
|
+
this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
|
|
1937
1996
|
}
|
|
1938
1997
|
}
|
|
1939
1998
|
restoreQueuedMessagesToEditor(options) {
|
|
@@ -2076,6 +2135,7 @@ export class InteractiveMode {
|
|
|
2076
2135
|
showImages: this.settingsManager.getShowImages(),
|
|
2077
2136
|
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
2078
2137
|
blockImages: this.settingsManager.getBlockImages(),
|
|
2138
|
+
enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
|
|
2079
2139
|
steeringMode: this.session.steeringMode,
|
|
2080
2140
|
followUpMode: this.session.followUpMode,
|
|
2081
2141
|
thinkingLevel: this.session.thinkingLevel,
|
|
@@ -2104,6 +2164,10 @@ export class InteractiveMode {
|
|
|
2104
2164
|
onBlockImagesChange: (blocked) => {
|
|
2105
2165
|
this.settingsManager.setBlockImages(blocked);
|
|
2106
2166
|
},
|
|
2167
|
+
onEnableSkillCommandsChange: (enabled) => {
|
|
2168
|
+
this.settingsManager.setEnableSkillCommands(enabled);
|
|
2169
|
+
this.rebuildAutocomplete();
|
|
2170
|
+
},
|
|
2107
2171
|
onSteeringModeChange: (mode) => {
|
|
2108
2172
|
this.session.setSteeringMode(mode);
|
|
2109
2173
|
},
|
|
@@ -2232,17 +2296,125 @@ export class InteractiveMode {
|
|
|
2232
2296
|
return { component: selector, focus: selector };
|
|
2233
2297
|
});
|
|
2234
2298
|
}
|
|
2299
|
+
async showModelsSelector() {
|
|
2300
|
+
// Get all available models
|
|
2301
|
+
this.session.modelRegistry.refresh();
|
|
2302
|
+
const allModels = this.session.modelRegistry.getAvailable();
|
|
2303
|
+
if (allModels.length === 0) {
|
|
2304
|
+
this.showStatus("No models available");
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
// Check if session has scoped models (from previous session-only changes or CLI --models)
|
|
2308
|
+
const sessionScopedModels = this.session.scopedModels;
|
|
2309
|
+
const hasSessionScope = sessionScopedModels.length > 0;
|
|
2310
|
+
// Build enabled model IDs from session state or settings
|
|
2311
|
+
const enabledModelIds = new Set();
|
|
2312
|
+
let hasFilter = false;
|
|
2313
|
+
if (hasSessionScope) {
|
|
2314
|
+
// Use current session's scoped models
|
|
2315
|
+
for (const sm of sessionScopedModels) {
|
|
2316
|
+
enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);
|
|
2317
|
+
}
|
|
2318
|
+
hasFilter = true;
|
|
2319
|
+
}
|
|
2320
|
+
else {
|
|
2321
|
+
// Fall back to settings
|
|
2322
|
+
const patterns = this.settingsManager.getEnabledModels();
|
|
2323
|
+
if (patterns !== undefined && patterns.length > 0) {
|
|
2324
|
+
hasFilter = true;
|
|
2325
|
+
const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
|
|
2326
|
+
for (const sm of scopedModels) {
|
|
2327
|
+
enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
// Track current enabled state (session-only until persisted)
|
|
2332
|
+
const currentEnabledIds = new Set(enabledModelIds);
|
|
2333
|
+
let currentHasFilter = hasFilter;
|
|
2334
|
+
// Helper to update session's scoped models (session-only, no persist)
|
|
2335
|
+
const updateSessionModels = async (enabledIds) => {
|
|
2336
|
+
if (enabledIds.size > 0 && enabledIds.size < allModels.length) {
|
|
2337
|
+
// Use current session thinking level, not settings default
|
|
2338
|
+
const currentThinkingLevel = this.session.thinkingLevel;
|
|
2339
|
+
const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);
|
|
2340
|
+
this.session.setScopedModels(newScopedModels.map((sm) => ({
|
|
2341
|
+
model: sm.model,
|
|
2342
|
+
thinkingLevel: sm.thinkingLevel ?? currentThinkingLevel,
|
|
2343
|
+
})));
|
|
2344
|
+
}
|
|
2345
|
+
else {
|
|
2346
|
+
// All enabled or none enabled = no filter
|
|
2347
|
+
this.session.setScopedModels([]);
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
this.showSelector((done) => {
|
|
2351
|
+
const selector = new ScopedModelsSelectorComponent({
|
|
2352
|
+
allModels,
|
|
2353
|
+
enabledModelIds: currentEnabledIds,
|
|
2354
|
+
hasEnabledModelsFilter: currentHasFilter,
|
|
2355
|
+
}, {
|
|
2356
|
+
onModelToggle: async (modelId, enabled) => {
|
|
2357
|
+
if (enabled) {
|
|
2358
|
+
currentEnabledIds.add(modelId);
|
|
2359
|
+
}
|
|
2360
|
+
else {
|
|
2361
|
+
currentEnabledIds.delete(modelId);
|
|
2362
|
+
}
|
|
2363
|
+
currentHasFilter = true;
|
|
2364
|
+
await updateSessionModels(currentEnabledIds);
|
|
2365
|
+
},
|
|
2366
|
+
onEnableAll: async (allModelIds) => {
|
|
2367
|
+
currentEnabledIds.clear();
|
|
2368
|
+
for (const id of allModelIds) {
|
|
2369
|
+
currentEnabledIds.add(id);
|
|
2370
|
+
}
|
|
2371
|
+
currentHasFilter = false;
|
|
2372
|
+
await updateSessionModels(currentEnabledIds);
|
|
2373
|
+
},
|
|
2374
|
+
onClearAll: async () => {
|
|
2375
|
+
currentEnabledIds.clear();
|
|
2376
|
+
currentHasFilter = true;
|
|
2377
|
+
await updateSessionModels(currentEnabledIds);
|
|
2378
|
+
},
|
|
2379
|
+
onToggleProvider: async (_provider, modelIds, enabled) => {
|
|
2380
|
+
for (const id of modelIds) {
|
|
2381
|
+
if (enabled) {
|
|
2382
|
+
currentEnabledIds.add(id);
|
|
2383
|
+
}
|
|
2384
|
+
else {
|
|
2385
|
+
currentEnabledIds.delete(id);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
currentHasFilter = true;
|
|
2389
|
+
await updateSessionModels(currentEnabledIds);
|
|
2390
|
+
},
|
|
2391
|
+
onPersist: (enabledIds) => {
|
|
2392
|
+
// Persist to settings
|
|
2393
|
+
const newPatterns = enabledIds.length === allModels.length
|
|
2394
|
+
? undefined // All enabled = clear filter
|
|
2395
|
+
: enabledIds;
|
|
2396
|
+
this.settingsManager.setEnabledModels(newPatterns);
|
|
2397
|
+
this.showStatus("Model selection saved to settings");
|
|
2398
|
+
},
|
|
2399
|
+
onCancel: () => {
|
|
2400
|
+
done();
|
|
2401
|
+
this.ui.requestRender();
|
|
2402
|
+
},
|
|
2403
|
+
});
|
|
2404
|
+
return { component: selector, focus: selector };
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2235
2407
|
showUserMessageSelector() {
|
|
2236
|
-
const userMessages = this.session.
|
|
2408
|
+
const userMessages = this.session.getUserMessagesForForking();
|
|
2237
2409
|
if (userMessages.length === 0) {
|
|
2238
|
-
this.showStatus("No messages to
|
|
2410
|
+
this.showStatus("No messages to fork from");
|
|
2239
2411
|
return;
|
|
2240
2412
|
}
|
|
2241
2413
|
this.showSelector((done) => {
|
|
2242
2414
|
const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
|
|
2243
|
-
const result = await this.session.
|
|
2415
|
+
const result = await this.session.fork(entryId);
|
|
2244
2416
|
if (result.cancelled) {
|
|
2245
|
-
// Extension cancelled the
|
|
2417
|
+
// Extension cancelled the fork
|
|
2246
2418
|
done();
|
|
2247
2419
|
this.ui.requestRender();
|
|
2248
2420
|
return;
|
|
@@ -2259,7 +2431,7 @@ export class InteractiveMode {
|
|
|
2259
2431
|
return { component: selector, focus: selector.getMessageList() };
|
|
2260
2432
|
});
|
|
2261
2433
|
}
|
|
2262
|
-
showTreeSelector() {
|
|
2434
|
+
showTreeSelector(initialSelectedId) {
|
|
2263
2435
|
const tree = this.sessionManager.getTree();
|
|
2264
2436
|
const realLeafId = this.sessionManager.getLeafId();
|
|
2265
2437
|
// Find the visible leaf for display (skip metadata entries like labels)
|
|
@@ -2286,7 +2458,31 @@ export class InteractiveMode {
|
|
|
2286
2458
|
}
|
|
2287
2459
|
// Ask about summarization
|
|
2288
2460
|
done(); // Close selector first
|
|
2289
|
-
|
|
2461
|
+
// Loop until user makes a complete choice or cancels to tree
|
|
2462
|
+
let wantsSummary = false;
|
|
2463
|
+
let customInstructions;
|
|
2464
|
+
while (true) {
|
|
2465
|
+
const summaryChoice = await this.showExtensionSelector("Summarize branch?", [
|
|
2466
|
+
"No summary",
|
|
2467
|
+
"Summarize",
|
|
2468
|
+
"Summarize with custom prompt",
|
|
2469
|
+
]);
|
|
2470
|
+
if (summaryChoice === undefined) {
|
|
2471
|
+
// User pressed escape - re-show tree selector with same selection
|
|
2472
|
+
this.showTreeSelector(entryId);
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
wantsSummary = summaryChoice !== "No summary";
|
|
2476
|
+
if (summaryChoice === "Summarize with custom prompt") {
|
|
2477
|
+
customInstructions = await this.showExtensionEditor("Custom summarization instructions");
|
|
2478
|
+
if (customInstructions === undefined) {
|
|
2479
|
+
// User cancelled - loop back to summary selector
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
// User made a complete choice
|
|
2484
|
+
break;
|
|
2485
|
+
}
|
|
2290
2486
|
// Set up escape handler and loader if summarizing
|
|
2291
2487
|
let summaryLoader;
|
|
2292
2488
|
const originalOnEscape = this.defaultEditor.onEscape;
|
|
@@ -2300,11 +2496,14 @@ export class InteractiveMode {
|
|
|
2300
2496
|
this.ui.requestRender();
|
|
2301
2497
|
}
|
|
2302
2498
|
try {
|
|
2303
|
-
const result = await this.session.navigateTree(entryId, {
|
|
2499
|
+
const result = await this.session.navigateTree(entryId, {
|
|
2500
|
+
summarize: wantsSummary,
|
|
2501
|
+
customInstructions,
|
|
2502
|
+
});
|
|
2304
2503
|
if (result.aborted) {
|
|
2305
|
-
// Summarization aborted - re-show tree selector
|
|
2504
|
+
// Summarization aborted - re-show tree selector with same selection
|
|
2306
2505
|
this.showStatus("Branch summarization cancelled");
|
|
2307
|
-
this.showTreeSelector();
|
|
2506
|
+
this.showTreeSelector(entryId);
|
|
2308
2507
|
return;
|
|
2309
2508
|
}
|
|
2310
2509
|
if (result.cancelled) {
|
|
@@ -2335,14 +2534,13 @@ export class InteractiveMode {
|
|
|
2335
2534
|
}, (entryId, label) => {
|
|
2336
2535
|
this.sessionManager.appendLabelChange(entryId, label);
|
|
2337
2536
|
this.ui.requestRender();
|
|
2338
|
-
});
|
|
2537
|
+
}, initialSelectedId);
|
|
2339
2538
|
return { component: selector, focus: selector };
|
|
2340
2539
|
});
|
|
2341
2540
|
}
|
|
2342
2541
|
showSessionSelector() {
|
|
2343
2542
|
this.showSelector((done) => {
|
|
2344
|
-
const
|
|
2345
|
-
const selector = new SessionSelectorComponent(sessions, async (sessionPath) => {
|
|
2543
|
+
const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), SessionManager.listAll, async (sessionPath) => {
|
|
2346
2544
|
done();
|
|
2347
2545
|
await this.handleResumeSession(sessionPath);
|
|
2348
2546
|
}, () => {
|
|
@@ -2350,7 +2548,7 @@ export class InteractiveMode {
|
|
|
2350
2548
|
this.ui.requestRender();
|
|
2351
2549
|
}, () => {
|
|
2352
2550
|
void this.shutdown();
|
|
2353
|
-
});
|
|
2551
|
+
}, () => this.ui.requestRender());
|
|
2354
2552
|
return { component: selector, focus: selector.getSessionList() };
|
|
2355
2553
|
});
|
|
2356
2554
|
}
|
|
@@ -2601,9 +2799,32 @@ export class InteractiveMode {
|
|
|
2601
2799
|
this.showError(error instanceof Error ? error.message : String(error));
|
|
2602
2800
|
}
|
|
2603
2801
|
}
|
|
2802
|
+
handleNameCommand(text) {
|
|
2803
|
+
const name = text.replace(/^\/name\s*/, "").trim();
|
|
2804
|
+
if (!name) {
|
|
2805
|
+
const currentName = this.sessionManager.getSessionName();
|
|
2806
|
+
if (currentName) {
|
|
2807
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
2808
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name: ${currentName}`), 1, 0));
|
|
2809
|
+
}
|
|
2810
|
+
else {
|
|
2811
|
+
this.showWarning("Usage: /name <name>");
|
|
2812
|
+
}
|
|
2813
|
+
this.ui.requestRender();
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
this.sessionManager.appendSessionInfo(name);
|
|
2817
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
2818
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
|
|
2819
|
+
this.ui.requestRender();
|
|
2820
|
+
}
|
|
2604
2821
|
handleSessionCommand() {
|
|
2605
2822
|
const stats = this.session.getSessionStats();
|
|
2823
|
+
const sessionName = this.sessionManager.getSessionName();
|
|
2606
2824
|
let info = `${theme.bold("Session Info")}\n\n`;
|
|
2825
|
+
if (sessionName) {
|
|
2826
|
+
info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
|
|
2827
|
+
}
|
|
2607
2828
|
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
|
|
2608
2829
|
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
|
|
2609
2830
|
info += `${theme.bold("Messages")}\n`;
|
|
@@ -2630,6 +2851,18 @@ export class InteractiveMode {
|
|
|
2630
2851
|
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
2631
2852
|
this.ui.requestRender();
|
|
2632
2853
|
}
|
|
2854
|
+
async handleSkillCommand(skillPath, args) {
|
|
2855
|
+
try {
|
|
2856
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
2857
|
+
// Strip YAML frontmatter if present
|
|
2858
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
2859
|
+
const message = args ? `${body}\n\n---\n\nUser: ${args}` : body;
|
|
2860
|
+
await this.session.prompt(message);
|
|
2861
|
+
}
|
|
2862
|
+
catch (err) {
|
|
2863
|
+
this.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2633
2866
|
handleChangelogCommand() {
|
|
2634
2867
|
const changelogPath = getChangelogPath();
|
|
2635
2868
|
const allEntries = parseChangelog(changelogPath);
|