@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +14 -9
  3. package/dist/cli/list-models.d.ts.map +1 -1
  4. package/dist/cli/list-models.js +1 -1
  5. package/dist/cli/list-models.js.map +1 -1
  6. package/dist/cli/session-picker.d.ts +4 -2
  7. package/dist/cli/session-picker.d.ts.map +1 -1
  8. package/dist/cli/session-picker.js +3 -3
  9. package/dist/cli/session-picker.js.map +1 -1
  10. package/dist/core/agent-session.d.ts +14 -8
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +37 -15
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  15. package/dist/core/compaction/branch-summarization.js +3 -1
  16. package/dist/core/compaction/branch-summarization.js.map +1 -1
  17. package/dist/core/extensions/index.d.ts +2 -2
  18. package/dist/core/extensions/index.d.ts.map +1 -1
  19. package/dist/core/extensions/index.js.map +1 -1
  20. package/dist/core/extensions/runner.d.ts +2 -2
  21. package/dist/core/extensions/runner.d.ts.map +1 -1
  22. package/dist/core/extensions/runner.js +9 -5
  23. package/dist/core/extensions/runner.js.map +1 -1
  24. package/dist/core/extensions/types.d.ts +25 -14
  25. package/dist/core/extensions/types.d.ts.map +1 -1
  26. package/dist/core/extensions/types.js.map +1 -1
  27. package/dist/core/footer-data-provider.d.ts.map +1 -1
  28. package/dist/core/footer-data-provider.js +10 -4
  29. package/dist/core/footer-data-provider.js.map +1 -1
  30. package/dist/core/index.d.ts +1 -1
  31. package/dist/core/index.d.ts.map +1 -1
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/session-manager.d.ts +11 -2
  34. package/dist/core/session-manager.d.ts.map +1 -1
  35. package/dist/core/session-manager.js +142 -64
  36. package/dist/core/session-manager.js.map +1 -1
  37. package/dist/core/settings-manager.d.ts +7 -3
  38. package/dist/core/settings-manager.d.ts.map +1 -1
  39. package/dist/core/settings-manager.js +15 -0
  40. package/dist/core/settings-manager.js.map +1 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/main.d.ts.map +1 -1
  45. package/dist/main.js +13 -12
  46. package/dist/main.js.map +1 -1
  47. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/extension-editor.js +8 -8
  49. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  50. package/dist/modes/interactive/components/index.d.ts +1 -0
  51. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  52. package/dist/modes/interactive/components/index.js +1 -0
  53. package/dist/modes/interactive/components/index.js.map +1 -1
  54. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/model-selector.js +1 -2
  56. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  57. package/dist/modes/interactive/components/scoped-models-selector.d.ts +47 -0
  58. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -0
  59. package/dist/modes/interactive/components/scoped-models-selector.js +241 -0
  60. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -0
  61. package/dist/modes/interactive/components/session-selector.d.ts +17 -3
  62. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/session-selector.js +167 -35
  64. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  65. package/dist/modes/interactive/components/settings-selector.d.ts +4 -2
  66. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/settings-selector.js +13 -1
  68. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  69. package/dist/modes/interactive/components/tree-selector.d.ts +2 -2
  70. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/tree-selector.js +8 -7
  72. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  73. package/dist/modes/interactive/interactive-mode.d.ts +6 -0
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.js +249 -37
  76. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  77. package/dist/modes/print-mode.d.ts.map +1 -1
  78. package/dist/modes/print-mode.js +2 -2
  79. package/dist/modes/print-mode.js.map +1 -1
  80. package/dist/modes/rpc/rpc-client.d.ts +4 -4
  81. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  82. package/dist/modes/rpc/rpc-client.js +6 -6
  83. package/dist/modes/rpc/rpc-client.js.map +1 -1
  84. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  85. package/dist/modes/rpc/rpc-mode.js +11 -8
  86. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  87. package/dist/modes/rpc/rpc-types.d.ts +4 -4
  88. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  89. package/dist/modes/rpc/rpc-types.js.map +1 -1
  90. package/docs/extensions.md +45 -12
  91. package/docs/rpc.md +10 -10
  92. package/docs/sdk.md +10 -5
  93. package/docs/session.md +1 -1
  94. package/docs/skills.md +27 -0
  95. package/docs/tree.md +9 -5
  96. package/docs/tui.md +2 -0
  97. package/examples/extensions/README.md +3 -3
  98. package/examples/extensions/confirm-destructive.ts +5 -5
  99. package/examples/extensions/dirty-repo-guard.ts +2 -2
  100. package/examples/extensions/git-checkpoint.ts +3 -3
  101. package/examples/extensions/handoff.ts +1 -1
  102. package/examples/extensions/model-status.ts +31 -0
  103. package/examples/extensions/todo.ts +1 -1
  104. package/examples/extensions/tools.ts +2 -2
  105. package/examples/extensions/with-deps/package-lock.json +2 -2
  106. package/examples/extensions/with-deps/package.json +1 -1
  107. package/examples/sdk/11-sessions.ts +1 -1
  108. package/package.json +4 -4
  109. package/dist/utils/fuzzy.d.ts +0 -7
  110. package/dist/utils/fuzzy.d.ts.map +0 -1
  111. package/dist/utils/fuzzy.js +0 -86
  112. 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
- { name: "model", description: "Select model (opens selector UI)" },
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: "branch", description: "Create a new branch from a previous message" },
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
- const fdPath = await ensureTool("fd");
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
- branch: async (entryId) => {
531
- const result = await this.session.branch(entryId);
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("Branched to new session");
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 /branch based on setting
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 === "/branch") {
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), "Working... (esc to interrupt)");
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
- this.ui.requestRender();
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.getUserMessagesForBranching();
2392
+ const userMessages = this.session.getUserMessagesForForking();
2219
2393
  if (userMessages.length === 0) {
2220
- this.showStatus("No messages to branch from");
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.branch(entryId);
2399
+ const result = await this.session.fork(entryId);
2226
2400
  if (result.cancelled) {
2227
- // Extension cancelled the branch
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
- const wantsSummary = await this.showExtensionConfirm("Summarize branch?", "Create a summary of the branch you're leaving?");
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, { summarize: wantsSummary });
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 sessions = SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir());
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.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
2627
- this.ui.addChild(new Spacer(1));
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();