@mariozechner/pi-coding-agent 0.42.5 → 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 +32 -0
- package/README.md +13 -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 +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 +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 +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 +222 -28
- 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 +43 -10
- 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,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,14 @@ 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" },
|
|
179
185
|
{ name: "session", description: "Show session info and stats" },
|
|
180
186
|
{ name: "changelog", description: "Show changelog entries" },
|
|
181
187
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
182
|
-
{ name: "
|
|
188
|
+
{ name: "fork", description: "Create a new fork from a previous message" },
|
|
183
189
|
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
184
190
|
{ name: "login", description: "Login with OAuth provider" },
|
|
185
191
|
{ name: "logout", description: "Logout from OAuth provider" },
|
|
@@ -197,18 +203,31 @@ export class InteractiveMode {
|
|
|
197
203
|
name: cmd.name,
|
|
198
204
|
description: cmd.description ?? "(extension command)",
|
|
199
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
|
+
}
|
|
200
216
|
// Setup autocomplete
|
|
201
|
-
this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands], process.cwd(), fdPath);
|
|
217
|
+
this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], process.cwd(), fdPath);
|
|
202
218
|
this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
|
|
203
219
|
}
|
|
220
|
+
rebuildAutocomplete() {
|
|
221
|
+
this.setupAutocomplete(this.fdPath);
|
|
222
|
+
}
|
|
204
223
|
async init() {
|
|
205
224
|
if (this.isInitialized)
|
|
206
225
|
return;
|
|
207
226
|
// Load changelog (only show new entries, skip for resumed sessions)
|
|
208
227
|
this.changelogMarkdown = this.getChangelogForDisplay();
|
|
209
228
|
// Setup autocomplete with fd tool for file path completion
|
|
210
|
-
|
|
211
|
-
this.setupAutocomplete(fdPath);
|
|
229
|
+
this.fdPath = await ensureTool("fd");
|
|
230
|
+
this.setupAutocomplete(this.fdPath);
|
|
212
231
|
// Add header with keybindings from config
|
|
213
232
|
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
214
233
|
// Format keybinding for startup display (lowercase, compact)
|
|
@@ -553,15 +572,15 @@ export class InteractiveMode {
|
|
|
553
572
|
this.ui.requestRender();
|
|
554
573
|
return { cancelled: false };
|
|
555
574
|
},
|
|
556
|
-
|
|
557
|
-
const result = await this.session.
|
|
575
|
+
fork: async (entryId) => {
|
|
576
|
+
const result = await this.session.fork(entryId);
|
|
558
577
|
if (result.cancelled) {
|
|
559
578
|
return { cancelled: true };
|
|
560
579
|
}
|
|
561
580
|
this.chatContainer.clear();
|
|
562
581
|
this.renderInitialMessages();
|
|
563
582
|
this.editor.setText(result.selectedText);
|
|
564
|
-
this.showStatus("
|
|
583
|
+
this.showStatus("Forked to new session");
|
|
565
584
|
return { cancelled: false };
|
|
566
585
|
},
|
|
567
586
|
navigateTree: async (targetId, options) => {
|
|
@@ -765,6 +784,11 @@ export class InteractiveMode {
|
|
|
765
784
|
input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
|
|
766
785
|
notify: (message, type) => this.showExtensionNotify(message, type),
|
|
767
786
|
setStatus: (key, text) => this.setExtensionStatus(key, text),
|
|
787
|
+
setWorkingMessage: (message) => {
|
|
788
|
+
if (this.loadingAnimation) {
|
|
789
|
+
this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
|
|
790
|
+
}
|
|
791
|
+
},
|
|
768
792
|
setWidget: (key, content) => this.setExtensionWidget(key, content),
|
|
769
793
|
setFooter: (factory) => this.setExtensionFooter(factory),
|
|
770
794
|
setHeader: (factory) => this.setExtensionHeader(factory),
|
|
@@ -1065,7 +1089,7 @@ export class InteractiveMode {
|
|
|
1065
1089
|
this.updateEditorBorderColor();
|
|
1066
1090
|
}
|
|
1067
1091
|
else if (!this.editor.getText().trim()) {
|
|
1068
|
-
// Double-escape with empty editor triggers /tree or /
|
|
1092
|
+
// Double-escape with empty editor triggers /tree or /fork based on setting
|
|
1069
1093
|
const now = Date.now();
|
|
1070
1094
|
if (now - this.lastEscapeTime < 500) {
|
|
1071
1095
|
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
@@ -1139,6 +1163,11 @@ export class InteractiveMode {
|
|
|
1139
1163
|
this.editor.setText("");
|
|
1140
1164
|
return;
|
|
1141
1165
|
}
|
|
1166
|
+
if (text === "/scoped-models") {
|
|
1167
|
+
this.editor.setText("");
|
|
1168
|
+
await this.showModelsSelector();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1142
1171
|
if (text === "/model" || text.startsWith("/model ")) {
|
|
1143
1172
|
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
|
|
1144
1173
|
this.editor.setText("");
|
|
@@ -1175,7 +1204,7 @@ export class InteractiveMode {
|
|
|
1175
1204
|
this.editor.setText("");
|
|
1176
1205
|
return;
|
|
1177
1206
|
}
|
|
1178
|
-
if (text === "/
|
|
1207
|
+
if (text === "/fork") {
|
|
1179
1208
|
this.showUserMessageSelector();
|
|
1180
1209
|
this.editor.setText("");
|
|
1181
1210
|
return;
|
|
@@ -1226,6 +1255,19 @@ export class InteractiveMode {
|
|
|
1226
1255
|
await this.shutdown();
|
|
1227
1256
|
return;
|
|
1228
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
|
+
}
|
|
1229
1271
|
// Handle bash command (! for normal, !! for excluded from context)
|
|
1230
1272
|
if (text.startsWith("!")) {
|
|
1231
1273
|
const isExcluded = text.startsWith("!!");
|
|
@@ -1300,7 +1342,7 @@ export class InteractiveMode {
|
|
|
1300
1342
|
this.loadingAnimation.stop();
|
|
1301
1343
|
}
|
|
1302
1344
|
this.statusContainer.clear();
|
|
1303
|
-
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);
|
|
1304
1346
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
1305
1347
|
this.ui.requestRender();
|
|
1306
1348
|
break;
|
|
@@ -1882,7 +1924,8 @@ export class InteractiveMode {
|
|
|
1882
1924
|
}
|
|
1883
1925
|
// Restart TUI
|
|
1884
1926
|
this.ui.start();
|
|
1885
|
-
|
|
1927
|
+
// Force full re-render since external editor uses alternate screen
|
|
1928
|
+
this.ui.requestRender(true);
|
|
1886
1929
|
}
|
|
1887
1930
|
}
|
|
1888
1931
|
// =========================================================================
|
|
@@ -2076,6 +2119,7 @@ export class InteractiveMode {
|
|
|
2076
2119
|
showImages: this.settingsManager.getShowImages(),
|
|
2077
2120
|
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
2078
2121
|
blockImages: this.settingsManager.getBlockImages(),
|
|
2122
|
+
enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
|
|
2079
2123
|
steeringMode: this.session.steeringMode,
|
|
2080
2124
|
followUpMode: this.session.followUpMode,
|
|
2081
2125
|
thinkingLevel: this.session.thinkingLevel,
|
|
@@ -2104,6 +2148,10 @@ export class InteractiveMode {
|
|
|
2104
2148
|
onBlockImagesChange: (blocked) => {
|
|
2105
2149
|
this.settingsManager.setBlockImages(blocked);
|
|
2106
2150
|
},
|
|
2151
|
+
onEnableSkillCommandsChange: (enabled) => {
|
|
2152
|
+
this.settingsManager.setEnableSkillCommands(enabled);
|
|
2153
|
+
this.rebuildAutocomplete();
|
|
2154
|
+
},
|
|
2107
2155
|
onSteeringModeChange: (mode) => {
|
|
2108
2156
|
this.session.setSteeringMode(mode);
|
|
2109
2157
|
},
|
|
@@ -2232,17 +2280,125 @@ export class InteractiveMode {
|
|
|
2232
2280
|
return { component: selector, focus: selector };
|
|
2233
2281
|
});
|
|
2234
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
|
+
}
|
|
2235
2391
|
showUserMessageSelector() {
|
|
2236
|
-
const userMessages = this.session.
|
|
2392
|
+
const userMessages = this.session.getUserMessagesForForking();
|
|
2237
2393
|
if (userMessages.length === 0) {
|
|
2238
|
-
this.showStatus("No messages to
|
|
2394
|
+
this.showStatus("No messages to fork from");
|
|
2239
2395
|
return;
|
|
2240
2396
|
}
|
|
2241
2397
|
this.showSelector((done) => {
|
|
2242
2398
|
const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
|
|
2243
|
-
const result = await this.session.
|
|
2399
|
+
const result = await this.session.fork(entryId);
|
|
2244
2400
|
if (result.cancelled) {
|
|
2245
|
-
// Extension cancelled the
|
|
2401
|
+
// Extension cancelled the fork
|
|
2246
2402
|
done();
|
|
2247
2403
|
this.ui.requestRender();
|
|
2248
2404
|
return;
|
|
@@ -2259,7 +2415,7 @@ export class InteractiveMode {
|
|
|
2259
2415
|
return { component: selector, focus: selector.getMessageList() };
|
|
2260
2416
|
});
|
|
2261
2417
|
}
|
|
2262
|
-
showTreeSelector() {
|
|
2418
|
+
showTreeSelector(initialSelectedId) {
|
|
2263
2419
|
const tree = this.sessionManager.getTree();
|
|
2264
2420
|
const realLeafId = this.sessionManager.getLeafId();
|
|
2265
2421
|
// Find the visible leaf for display (skip metadata entries like labels)
|
|
@@ -2286,7 +2442,31 @@ export class InteractiveMode {
|
|
|
2286
2442
|
}
|
|
2287
2443
|
// Ask about summarization
|
|
2288
2444
|
done(); // Close selector first
|
|
2289
|
-
|
|
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
|
+
}
|
|
2290
2470
|
// Set up escape handler and loader if summarizing
|
|
2291
2471
|
let summaryLoader;
|
|
2292
2472
|
const originalOnEscape = this.defaultEditor.onEscape;
|
|
@@ -2300,11 +2480,14 @@ export class InteractiveMode {
|
|
|
2300
2480
|
this.ui.requestRender();
|
|
2301
2481
|
}
|
|
2302
2482
|
try {
|
|
2303
|
-
const result = await this.session.navigateTree(entryId, {
|
|
2483
|
+
const result = await this.session.navigateTree(entryId, {
|
|
2484
|
+
summarize: wantsSummary,
|
|
2485
|
+
customInstructions,
|
|
2486
|
+
});
|
|
2304
2487
|
if (result.aborted) {
|
|
2305
|
-
// Summarization aborted - re-show tree selector
|
|
2488
|
+
// Summarization aborted - re-show tree selector with same selection
|
|
2306
2489
|
this.showStatus("Branch summarization cancelled");
|
|
2307
|
-
this.showTreeSelector();
|
|
2490
|
+
this.showTreeSelector(entryId);
|
|
2308
2491
|
return;
|
|
2309
2492
|
}
|
|
2310
2493
|
if (result.cancelled) {
|
|
@@ -2335,14 +2518,13 @@ export class InteractiveMode {
|
|
|
2335
2518
|
}, (entryId, label) => {
|
|
2336
2519
|
this.sessionManager.appendLabelChange(entryId, label);
|
|
2337
2520
|
this.ui.requestRender();
|
|
2338
|
-
});
|
|
2521
|
+
}, initialSelectedId);
|
|
2339
2522
|
return { component: selector, focus: selector };
|
|
2340
2523
|
});
|
|
2341
2524
|
}
|
|
2342
2525
|
showSessionSelector() {
|
|
2343
2526
|
this.showSelector((done) => {
|
|
2344
|
-
const
|
|
2345
|
-
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) => {
|
|
2346
2528
|
done();
|
|
2347
2529
|
await this.handleResumeSession(sessionPath);
|
|
2348
2530
|
}, () => {
|
|
@@ -2350,7 +2532,7 @@ export class InteractiveMode {
|
|
|
2350
2532
|
this.ui.requestRender();
|
|
2351
2533
|
}, () => {
|
|
2352
2534
|
void this.shutdown();
|
|
2353
|
-
});
|
|
2535
|
+
}, () => this.ui.requestRender());
|
|
2354
2536
|
return { component: selector, focus: selector.getSessionList() };
|
|
2355
2537
|
});
|
|
2356
2538
|
}
|
|
@@ -2630,6 +2812,18 @@ export class InteractiveMode {
|
|
|
2630
2812
|
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
2631
2813
|
this.ui.requestRender();
|
|
2632
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
|
+
}
|
|
2633
2827
|
handleChangelogCommand() {
|
|
2634
2828
|
const changelogPath = getChangelogPath();
|
|
2635
2829
|
const allEntries = parseChangelog(changelogPath);
|