@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +13 -8
  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 +2 -3
  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 +222 -28
  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 +43 -10
  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,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 (not provider/id to avoid matching provider name)
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: "branch", description: "Create a new branch from a previous message" },
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
- const fdPath = await ensureTool("fd");
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
- branch: async (entryId) => {
557
- const result = await this.session.branch(entryId);
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("Branched to new session");
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 /branch based on setting
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 === "/branch") {
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), "Working... (esc to interrupt)");
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
- this.ui.requestRender();
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.getUserMessagesForBranching();
2392
+ const userMessages = this.session.getUserMessagesForForking();
2237
2393
  if (userMessages.length === 0) {
2238
- this.showStatus("No messages to branch from");
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.branch(entryId);
2399
+ const result = await this.session.fork(entryId);
2244
2400
  if (result.cancelled) {
2245
- // Extension cancelled the branch
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
- 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
+ }
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, { summarize: wantsSummary });
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 sessions = SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir());
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);