@mariozechner/pi-coding-agent 0.42.4 → 0.43.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 +41 -0
- package/README.md +14 -9
- 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 +14 -8
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +37 -15
- 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/runner.d.ts +2 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +9 -5
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +25 -14
- 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 +11 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +142 -64
- 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 +1 -1
- 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 +1 -2
- 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 +167 -35
- 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 +13 -1
- 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 +6 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +249 -37
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +2 -2
- 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 +11 -8
- 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 +45 -12
- package/docs/rpc.md +10 -10
- package/docs/sdk.md +10 -5
- package/docs/session.md +1 -1
- package/docs/skills.md +27 -0
- package/docs/tree.md +9 -5
- package/docs/tui.md +2 -0
- package/examples/extensions/README.md +3 -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/todo.ts +1 -1
- package/examples/extensions/tools.ts +2 -2
- 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,15 +7,15 @@ 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
|
-
import { allTools } from "../../core/tools/index.js";
|
|
19
19
|
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
|
20
20
|
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
21
21
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.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 !)
|
|
@@ -146,14 +151,41 @@ export class InteractiveMode {
|
|
|
146
151
|
// Define commands for autocomplete
|
|
147
152
|
const slashCommands = [
|
|
148
153
|
{ name: "settings", description: "Open settings menu" },
|
|
149
|
-
{
|
|
154
|
+
{
|
|
155
|
+
name: "model",
|
|
156
|
+
description: "Select model (opens selector UI)",
|
|
157
|
+
getArgumentCompletions: (prefix) => {
|
|
158
|
+
// Get available models (scoped or from registry)
|
|
159
|
+
const models = this.session.scopedModels.length > 0
|
|
160
|
+
? this.session.scopedModels.map((s) => s.model)
|
|
161
|
+
: this.session.modelRegistry.getAvailable();
|
|
162
|
+
if (models.length === 0)
|
|
163
|
+
return null;
|
|
164
|
+
// Create items with provider/id format
|
|
165
|
+
const items = models.map((m) => ({
|
|
166
|
+
id: m.id,
|
|
167
|
+
provider: m.provider,
|
|
168
|
+
label: `${m.provider}/${m.id}`,
|
|
169
|
+
}));
|
|
170
|
+
// Fuzzy filter by model ID + provider (allows "opus anthropic" to match)
|
|
171
|
+
const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);
|
|
172
|
+
if (filtered.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
return filtered.map((item) => ({
|
|
175
|
+
value: item.label,
|
|
176
|
+
label: item.id,
|
|
177
|
+
description: item.provider,
|
|
178
|
+
}));
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{ name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
|
|
150
182
|
{ name: "export", description: "Export session to HTML file" },
|
|
151
183
|
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
152
184
|
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
153
185
|
{ name: "session", description: "Show session info and stats" },
|
|
154
186
|
{ name: "changelog", description: "Show changelog entries" },
|
|
155
187
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
156
|
-
{ name: "
|
|
188
|
+
{ name: "fork", description: "Create a new fork from a previous message" },
|
|
157
189
|
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
158
190
|
{ name: "login", description: "Login with OAuth provider" },
|
|
159
191
|
{ name: "logout", description: "Logout from OAuth provider" },
|
|
@@ -171,18 +203,31 @@ export class InteractiveMode {
|
|
|
171
203
|
name: cmd.name,
|
|
172
204
|
description: cmd.description ?? "(extension command)",
|
|
173
205
|
}));
|
|
206
|
+
// Build skill commands from session.skills (if enabled)
|
|
207
|
+
this.skillCommands.clear();
|
|
208
|
+
const skillCommandList = [];
|
|
209
|
+
if (this.settingsManager.getEnableSkillCommands()) {
|
|
210
|
+
for (const skill of this.session.skills) {
|
|
211
|
+
const commandName = `skill:${skill.name}`;
|
|
212
|
+
this.skillCommands.set(commandName, skill.filePath);
|
|
213
|
+
skillCommandList.push({ name: commandName, description: skill.description });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
174
216
|
// Setup autocomplete
|
|
175
|
-
this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands], process.cwd(), fdPath);
|
|
217
|
+
this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], process.cwd(), fdPath);
|
|
176
218
|
this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
|
|
177
219
|
}
|
|
220
|
+
rebuildAutocomplete() {
|
|
221
|
+
this.setupAutocomplete(this.fdPath);
|
|
222
|
+
}
|
|
178
223
|
async init() {
|
|
179
224
|
if (this.isInitialized)
|
|
180
225
|
return;
|
|
181
226
|
// Load changelog (only show new entries, skip for resumed sessions)
|
|
182
227
|
this.changelogMarkdown = this.getChangelogForDisplay();
|
|
183
228
|
// Setup autocomplete with fd tool for file path completion
|
|
184
|
-
|
|
185
|
-
this.setupAutocomplete(fdPath);
|
|
229
|
+
this.fdPath = await ensureTool("fd");
|
|
230
|
+
this.setupAutocomplete(this.fdPath);
|
|
186
231
|
// Add header with keybindings from config
|
|
187
232
|
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
188
233
|
// Format keybinding for startup display (lowercase, compact)
|
|
@@ -527,15 +572,15 @@ export class InteractiveMode {
|
|
|
527
572
|
this.ui.requestRender();
|
|
528
573
|
return { cancelled: false };
|
|
529
574
|
},
|
|
530
|
-
|
|
531
|
-
const result = await this.session.
|
|
575
|
+
fork: async (entryId) => {
|
|
576
|
+
const result = await this.session.fork(entryId);
|
|
532
577
|
if (result.cancelled) {
|
|
533
578
|
return { cancelled: true };
|
|
534
579
|
}
|
|
535
580
|
this.chatContainer.clear();
|
|
536
581
|
this.renderInitialMessages();
|
|
537
582
|
this.editor.setText(result.selectedText);
|
|
538
|
-
this.showStatus("
|
|
583
|
+
this.showStatus("Forked to new session");
|
|
539
584
|
return { cancelled: false };
|
|
540
585
|
},
|
|
541
586
|
navigateTree: async (targetId, options) => {
|
|
@@ -565,14 +610,6 @@ export class InteractiveMode {
|
|
|
565
610
|
this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded extensions:\n") + extList, 0, 0));
|
|
566
611
|
this.chatContainer.addChild(new Spacer(1));
|
|
567
612
|
}
|
|
568
|
-
// Warn about built-in tool overrides
|
|
569
|
-
const builtInToolNames = new Set(Object.keys(allTools));
|
|
570
|
-
const registeredTools = extensionRunner.getAllRegisteredTools();
|
|
571
|
-
for (const tool of registeredTools) {
|
|
572
|
-
if (builtInToolNames.has(tool.definition.name)) {
|
|
573
|
-
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: Extension "${tool.extensionPath}" overrides built-in tool "${tool.definition.name}"`), 0, 0));
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
613
|
// Emit session_start event
|
|
577
614
|
await extensionRunner.emit({
|
|
578
615
|
type: "session_start",
|
|
@@ -747,6 +784,11 @@ export class InteractiveMode {
|
|
|
747
784
|
input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
|
|
748
785
|
notify: (message, type) => this.showExtensionNotify(message, type),
|
|
749
786
|
setStatus: (key, text) => this.setExtensionStatus(key, text),
|
|
787
|
+
setWorkingMessage: (message) => {
|
|
788
|
+
if (this.loadingAnimation) {
|
|
789
|
+
this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
|
|
790
|
+
}
|
|
791
|
+
},
|
|
750
792
|
setWidget: (key, content) => this.setExtensionWidget(key, content),
|
|
751
793
|
setFooter: (factory) => this.setExtensionFooter(factory),
|
|
752
794
|
setHeader: (factory) => this.setExtensionHeader(factory),
|
|
@@ -1047,7 +1089,7 @@ export class InteractiveMode {
|
|
|
1047
1089
|
this.updateEditorBorderColor();
|
|
1048
1090
|
}
|
|
1049
1091
|
else if (!this.editor.getText().trim()) {
|
|
1050
|
-
// Double-escape with empty editor triggers /tree or /
|
|
1092
|
+
// Double-escape with empty editor triggers /tree or /fork based on setting
|
|
1051
1093
|
const now = Date.now();
|
|
1052
1094
|
if (now - this.lastEscapeTime < 500) {
|
|
1053
1095
|
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
@@ -1121,6 +1163,11 @@ export class InteractiveMode {
|
|
|
1121
1163
|
this.editor.setText("");
|
|
1122
1164
|
return;
|
|
1123
1165
|
}
|
|
1166
|
+
if (text === "/scoped-models") {
|
|
1167
|
+
this.editor.setText("");
|
|
1168
|
+
await this.showModelsSelector();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1124
1171
|
if (text === "/model" || text.startsWith("/model ")) {
|
|
1125
1172
|
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
|
|
1126
1173
|
this.editor.setText("");
|
|
@@ -1157,7 +1204,7 @@ export class InteractiveMode {
|
|
|
1157
1204
|
this.editor.setText("");
|
|
1158
1205
|
return;
|
|
1159
1206
|
}
|
|
1160
|
-
if (text === "/
|
|
1207
|
+
if (text === "/fork") {
|
|
1161
1208
|
this.showUserMessageSelector();
|
|
1162
1209
|
this.editor.setText("");
|
|
1163
1210
|
return;
|
|
@@ -1208,6 +1255,19 @@ export class InteractiveMode {
|
|
|
1208
1255
|
await this.shutdown();
|
|
1209
1256
|
return;
|
|
1210
1257
|
}
|
|
1258
|
+
// Handle skill commands (/skill:name [args])
|
|
1259
|
+
if (text.startsWith("/skill:")) {
|
|
1260
|
+
const spaceIndex = text.indexOf(" ");
|
|
1261
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
1262
|
+
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
1263
|
+
const skillPath = this.skillCommands.get(commandName);
|
|
1264
|
+
if (skillPath) {
|
|
1265
|
+
this.editor.addToHistory?.(text);
|
|
1266
|
+
this.editor.setText("");
|
|
1267
|
+
await this.handleSkillCommand(skillPath, args);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1211
1271
|
// Handle bash command (! for normal, !! for excluded from context)
|
|
1212
1272
|
if (text.startsWith("!")) {
|
|
1213
1273
|
const isExcluded = text.startsWith("!!");
|
|
@@ -1282,7 +1342,7 @@ export class InteractiveMode {
|
|
|
1282
1342
|
this.loadingAnimation.stop();
|
|
1283
1343
|
}
|
|
1284
1344
|
this.statusContainer.clear();
|
|
1285
|
-
this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text),
|
|
1345
|
+
this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
|
|
1286
1346
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
1287
1347
|
this.ui.requestRender();
|
|
1288
1348
|
break;
|
|
@@ -1864,7 +1924,8 @@ export class InteractiveMode {
|
|
|
1864
1924
|
}
|
|
1865
1925
|
// Restart TUI
|
|
1866
1926
|
this.ui.start();
|
|
1867
|
-
|
|
1927
|
+
// Force full re-render since external editor uses alternate screen
|
|
1928
|
+
this.ui.requestRender(true);
|
|
1868
1929
|
}
|
|
1869
1930
|
}
|
|
1870
1931
|
// =========================================================================
|
|
@@ -2058,6 +2119,7 @@ export class InteractiveMode {
|
|
|
2058
2119
|
showImages: this.settingsManager.getShowImages(),
|
|
2059
2120
|
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
2060
2121
|
blockImages: this.settingsManager.getBlockImages(),
|
|
2122
|
+
enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
|
|
2061
2123
|
steeringMode: this.session.steeringMode,
|
|
2062
2124
|
followUpMode: this.session.followUpMode,
|
|
2063
2125
|
thinkingLevel: this.session.thinkingLevel,
|
|
@@ -2086,6 +2148,10 @@ export class InteractiveMode {
|
|
|
2086
2148
|
onBlockImagesChange: (blocked) => {
|
|
2087
2149
|
this.settingsManager.setBlockImages(blocked);
|
|
2088
2150
|
},
|
|
2151
|
+
onEnableSkillCommandsChange: (enabled) => {
|
|
2152
|
+
this.settingsManager.setEnableSkillCommands(enabled);
|
|
2153
|
+
this.rebuildAutocomplete();
|
|
2154
|
+
},
|
|
2089
2155
|
onSteeringModeChange: (mode) => {
|
|
2090
2156
|
this.session.setSteeringMode(mode);
|
|
2091
2157
|
},
|
|
@@ -2214,17 +2280,125 @@ export class InteractiveMode {
|
|
|
2214
2280
|
return { component: selector, focus: selector };
|
|
2215
2281
|
});
|
|
2216
2282
|
}
|
|
2283
|
+
async showModelsSelector() {
|
|
2284
|
+
// Get all available models
|
|
2285
|
+
this.session.modelRegistry.refresh();
|
|
2286
|
+
const allModels = this.session.modelRegistry.getAvailable();
|
|
2287
|
+
if (allModels.length === 0) {
|
|
2288
|
+
this.showStatus("No models available");
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
// Check if session has scoped models (from previous session-only changes or CLI --models)
|
|
2292
|
+
const sessionScopedModels = this.session.scopedModels;
|
|
2293
|
+
const hasSessionScope = sessionScopedModels.length > 0;
|
|
2294
|
+
// Build enabled model IDs from session state or settings
|
|
2295
|
+
const enabledModelIds = new Set();
|
|
2296
|
+
let hasFilter = false;
|
|
2297
|
+
if (hasSessionScope) {
|
|
2298
|
+
// Use current session's scoped models
|
|
2299
|
+
for (const sm of sessionScopedModels) {
|
|
2300
|
+
enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);
|
|
2301
|
+
}
|
|
2302
|
+
hasFilter = true;
|
|
2303
|
+
}
|
|
2304
|
+
else {
|
|
2305
|
+
// Fall back to settings
|
|
2306
|
+
const patterns = this.settingsManager.getEnabledModels();
|
|
2307
|
+
if (patterns !== undefined && patterns.length > 0) {
|
|
2308
|
+
hasFilter = true;
|
|
2309
|
+
const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
|
|
2310
|
+
for (const sm of scopedModels) {
|
|
2311
|
+
enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
// Track current enabled state (session-only until persisted)
|
|
2316
|
+
const currentEnabledIds = new Set(enabledModelIds);
|
|
2317
|
+
let currentHasFilter = hasFilter;
|
|
2318
|
+
// Helper to update session's scoped models (session-only, no persist)
|
|
2319
|
+
const updateSessionModels = async (enabledIds) => {
|
|
2320
|
+
if (enabledIds.size > 0 && enabledIds.size < allModels.length) {
|
|
2321
|
+
// Use current session thinking level, not settings default
|
|
2322
|
+
const currentThinkingLevel = this.session.thinkingLevel;
|
|
2323
|
+
const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);
|
|
2324
|
+
this.session.setScopedModels(newScopedModels.map((sm) => ({
|
|
2325
|
+
model: sm.model,
|
|
2326
|
+
thinkingLevel: sm.thinkingLevel ?? currentThinkingLevel,
|
|
2327
|
+
})));
|
|
2328
|
+
}
|
|
2329
|
+
else {
|
|
2330
|
+
// All enabled or none enabled = no filter
|
|
2331
|
+
this.session.setScopedModels([]);
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
this.showSelector((done) => {
|
|
2335
|
+
const selector = new ScopedModelsSelectorComponent({
|
|
2336
|
+
allModels,
|
|
2337
|
+
enabledModelIds: currentEnabledIds,
|
|
2338
|
+
hasEnabledModelsFilter: currentHasFilter,
|
|
2339
|
+
}, {
|
|
2340
|
+
onModelToggle: async (modelId, enabled) => {
|
|
2341
|
+
if (enabled) {
|
|
2342
|
+
currentEnabledIds.add(modelId);
|
|
2343
|
+
}
|
|
2344
|
+
else {
|
|
2345
|
+
currentEnabledIds.delete(modelId);
|
|
2346
|
+
}
|
|
2347
|
+
currentHasFilter = true;
|
|
2348
|
+
await updateSessionModels(currentEnabledIds);
|
|
2349
|
+
},
|
|
2350
|
+
onEnableAll: async (allModelIds) => {
|
|
2351
|
+
currentEnabledIds.clear();
|
|
2352
|
+
for (const id of allModelIds) {
|
|
2353
|
+
currentEnabledIds.add(id);
|
|
2354
|
+
}
|
|
2355
|
+
currentHasFilter = false;
|
|
2356
|
+
await updateSessionModels(currentEnabledIds);
|
|
2357
|
+
},
|
|
2358
|
+
onClearAll: async () => {
|
|
2359
|
+
currentEnabledIds.clear();
|
|
2360
|
+
currentHasFilter = true;
|
|
2361
|
+
await updateSessionModels(currentEnabledIds);
|
|
2362
|
+
},
|
|
2363
|
+
onToggleProvider: async (_provider, modelIds, enabled) => {
|
|
2364
|
+
for (const id of modelIds) {
|
|
2365
|
+
if (enabled) {
|
|
2366
|
+
currentEnabledIds.add(id);
|
|
2367
|
+
}
|
|
2368
|
+
else {
|
|
2369
|
+
currentEnabledIds.delete(id);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
currentHasFilter = true;
|
|
2373
|
+
await updateSessionModels(currentEnabledIds);
|
|
2374
|
+
},
|
|
2375
|
+
onPersist: (enabledIds) => {
|
|
2376
|
+
// Persist to settings
|
|
2377
|
+
const newPatterns = enabledIds.length === allModels.length
|
|
2378
|
+
? undefined // All enabled = clear filter
|
|
2379
|
+
: enabledIds;
|
|
2380
|
+
this.settingsManager.setEnabledModels(newPatterns);
|
|
2381
|
+
this.showStatus("Model selection saved to settings");
|
|
2382
|
+
},
|
|
2383
|
+
onCancel: () => {
|
|
2384
|
+
done();
|
|
2385
|
+
this.ui.requestRender();
|
|
2386
|
+
},
|
|
2387
|
+
});
|
|
2388
|
+
return { component: selector, focus: selector };
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2217
2391
|
showUserMessageSelector() {
|
|
2218
|
-
const userMessages = this.session.
|
|
2392
|
+
const userMessages = this.session.getUserMessagesForForking();
|
|
2219
2393
|
if (userMessages.length === 0) {
|
|
2220
|
-
this.showStatus("No messages to
|
|
2394
|
+
this.showStatus("No messages to fork from");
|
|
2221
2395
|
return;
|
|
2222
2396
|
}
|
|
2223
2397
|
this.showSelector((done) => {
|
|
2224
2398
|
const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
|
|
2225
|
-
const result = await this.session.
|
|
2399
|
+
const result = await this.session.fork(entryId);
|
|
2226
2400
|
if (result.cancelled) {
|
|
2227
|
-
// Extension cancelled the
|
|
2401
|
+
// Extension cancelled the fork
|
|
2228
2402
|
done();
|
|
2229
2403
|
this.ui.requestRender();
|
|
2230
2404
|
return;
|
|
@@ -2241,7 +2415,7 @@ export class InteractiveMode {
|
|
|
2241
2415
|
return { component: selector, focus: selector.getMessageList() };
|
|
2242
2416
|
});
|
|
2243
2417
|
}
|
|
2244
|
-
showTreeSelector() {
|
|
2418
|
+
showTreeSelector(initialSelectedId) {
|
|
2245
2419
|
const tree = this.sessionManager.getTree();
|
|
2246
2420
|
const realLeafId = this.sessionManager.getLeafId();
|
|
2247
2421
|
// Find the visible leaf for display (skip metadata entries like labels)
|
|
@@ -2268,7 +2442,31 @@ export class InteractiveMode {
|
|
|
2268
2442
|
}
|
|
2269
2443
|
// Ask about summarization
|
|
2270
2444
|
done(); // Close selector first
|
|
2271
|
-
|
|
2445
|
+
// Loop until user makes a complete choice or cancels to tree
|
|
2446
|
+
let wantsSummary = false;
|
|
2447
|
+
let customInstructions;
|
|
2448
|
+
while (true) {
|
|
2449
|
+
const summaryChoice = await this.showExtensionSelector("Summarize branch?", [
|
|
2450
|
+
"No summary",
|
|
2451
|
+
"Summarize",
|
|
2452
|
+
"Summarize with custom prompt",
|
|
2453
|
+
]);
|
|
2454
|
+
if (summaryChoice === undefined) {
|
|
2455
|
+
// User pressed escape - re-show tree selector with same selection
|
|
2456
|
+
this.showTreeSelector(entryId);
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
wantsSummary = summaryChoice !== "No summary";
|
|
2460
|
+
if (summaryChoice === "Summarize with custom prompt") {
|
|
2461
|
+
customInstructions = await this.showExtensionEditor("Custom summarization instructions");
|
|
2462
|
+
if (customInstructions === undefined) {
|
|
2463
|
+
// User cancelled - loop back to summary selector
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
// User made a complete choice
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2272
2470
|
// Set up escape handler and loader if summarizing
|
|
2273
2471
|
let summaryLoader;
|
|
2274
2472
|
const originalOnEscape = this.defaultEditor.onEscape;
|
|
@@ -2282,11 +2480,14 @@ export class InteractiveMode {
|
|
|
2282
2480
|
this.ui.requestRender();
|
|
2283
2481
|
}
|
|
2284
2482
|
try {
|
|
2285
|
-
const result = await this.session.navigateTree(entryId, {
|
|
2483
|
+
const result = await this.session.navigateTree(entryId, {
|
|
2484
|
+
summarize: wantsSummary,
|
|
2485
|
+
customInstructions,
|
|
2486
|
+
});
|
|
2286
2487
|
if (result.aborted) {
|
|
2287
|
-
// Summarization aborted - re-show tree selector
|
|
2488
|
+
// Summarization aborted - re-show tree selector with same selection
|
|
2288
2489
|
this.showStatus("Branch summarization cancelled");
|
|
2289
|
-
this.showTreeSelector();
|
|
2490
|
+
this.showTreeSelector(entryId);
|
|
2290
2491
|
return;
|
|
2291
2492
|
}
|
|
2292
2493
|
if (result.cancelled) {
|
|
@@ -2317,14 +2518,13 @@ export class InteractiveMode {
|
|
|
2317
2518
|
}, (entryId, label) => {
|
|
2318
2519
|
this.sessionManager.appendLabelChange(entryId, label);
|
|
2319
2520
|
this.ui.requestRender();
|
|
2320
|
-
});
|
|
2521
|
+
}, initialSelectedId);
|
|
2321
2522
|
return { component: selector, focus: selector };
|
|
2322
2523
|
});
|
|
2323
2524
|
}
|
|
2324
2525
|
showSessionSelector() {
|
|
2325
2526
|
this.showSelector((done) => {
|
|
2326
|
-
const
|
|
2327
|
-
const selector = new SessionSelectorComponent(sessions, async (sessionPath) => {
|
|
2527
|
+
const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), SessionManager.listAll, async (sessionPath) => {
|
|
2328
2528
|
done();
|
|
2329
2529
|
await this.handleResumeSession(sessionPath);
|
|
2330
2530
|
}, () => {
|
|
@@ -2332,7 +2532,7 @@ export class InteractiveMode {
|
|
|
2332
2532
|
this.ui.requestRender();
|
|
2333
2533
|
}, () => {
|
|
2334
2534
|
void this.shutdown();
|
|
2335
|
-
});
|
|
2535
|
+
}, () => this.ui.requestRender());
|
|
2336
2536
|
return { component: selector, focus: selector.getSessionList() };
|
|
2337
2537
|
});
|
|
2338
2538
|
}
|
|
@@ -2612,6 +2812,18 @@ export class InteractiveMode {
|
|
|
2612
2812
|
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
2613
2813
|
this.ui.requestRender();
|
|
2614
2814
|
}
|
|
2815
|
+
async handleSkillCommand(skillPath, args) {
|
|
2816
|
+
try {
|
|
2817
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
2818
|
+
// Strip YAML frontmatter if present
|
|
2819
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
2820
|
+
const message = args ? `${body}\n\n---\n\nUser: ${args}` : body;
|
|
2821
|
+
await this.session.prompt(message);
|
|
2822
|
+
}
|
|
2823
|
+
catch (err) {
|
|
2824
|
+
this.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2615
2827
|
handleChangelogCommand() {
|
|
2616
2828
|
const changelogPath = getChangelogPath();
|
|
2617
2829
|
const allEntries = parseChangelog(changelogPath);
|
|
@@ -2623,8 +2835,8 @@ export class InteractiveMode {
|
|
|
2623
2835
|
: "No changelog entries found.";
|
|
2624
2836
|
this.chatContainer.addChild(new Spacer(1));
|
|
2625
2837
|
this.chatContainer.addChild(new DynamicBorder());
|
|
2626
|
-
this.
|
|
2627
|
-
this.
|
|
2838
|
+
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
|
2839
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
2628
2840
|
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
|
|
2629
2841
|
this.chatContainer.addChild(new DynamicBorder());
|
|
2630
2842
|
this.ui.requestRender();
|