@mariozechner/pi-coding-agent 0.49.3 → 0.50.1

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 (207) hide show
  1. package/CHANGELOG.md +110 -1
  2. package/README.md +310 -1230
  3. package/dist/cli/args.d.ts +5 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +57 -23
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/config-selector.d.ts +14 -0
  8. package/dist/cli/config-selector.d.ts.map +1 -0
  9. package/dist/cli/config-selector.js +31 -0
  10. package/dist/cli/config-selector.js.map +1 -0
  11. package/dist/cli/session-picker.d.ts.map +1 -1
  12. package/dist/cli/session-picker.js +1 -1
  13. package/dist/cli/session-picker.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +60 -37
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +272 -69
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/auth-storage.d.ts +8 -18
  19. package/dist/core/auth-storage.d.ts.map +1 -1
  20. package/dist/core/auth-storage.js +39 -55
  21. package/dist/core/auth-storage.js.map +1 -1
  22. package/dist/core/bash-executor.d.ts.map +1 -1
  23. package/dist/core/bash-executor.js +2 -1
  24. package/dist/core/bash-executor.js.map +1 -1
  25. package/dist/core/diagnostics.d.ts +15 -0
  26. package/dist/core/diagnostics.d.ts.map +1 -0
  27. package/dist/core/diagnostics.js +2 -0
  28. package/dist/core/diagnostics.js.map +1 -0
  29. package/dist/core/export-html/template.css +9 -0
  30. package/dist/core/export-html/template.js +6 -4
  31. package/dist/core/extensions/index.d.ts +1 -1
  32. package/dist/core/extensions/index.d.ts.map +1 -1
  33. package/dist/core/extensions/index.js.map +1 -1
  34. package/dist/core/extensions/loader.d.ts +1 -1
  35. package/dist/core/extensions/loader.d.ts.map +1 -1
  36. package/dist/core/extensions/loader.js +10 -1
  37. package/dist/core/extensions/loader.js.map +1 -1
  38. package/dist/core/extensions/runner.d.ts +9 -3
  39. package/dist/core/extensions/runner.d.ts.map +1 -1
  40. package/dist/core/extensions/runner.js +39 -12
  41. package/dist/core/extensions/runner.js.map +1 -1
  42. package/dist/core/extensions/types.d.ts +112 -1
  43. package/dist/core/extensions/types.d.ts.map +1 -1
  44. package/dist/core/extensions/types.js.map +1 -1
  45. package/dist/core/footer-data-provider.d.ts +9 -2
  46. package/dist/core/footer-data-provider.d.ts.map +1 -1
  47. package/dist/core/footer-data-provider.js +13 -0
  48. package/dist/core/footer-data-provider.js.map +1 -1
  49. package/dist/core/model-registry.d.ts +42 -2
  50. package/dist/core/model-registry.d.ts.map +1 -1
  51. package/dist/core/model-registry.js +154 -44
  52. package/dist/core/model-registry.js.map +1 -1
  53. package/dist/core/model-resolver.d.ts.map +1 -1
  54. package/dist/core/model-resolver.js +3 -2
  55. package/dist/core/model-resolver.js.map +1 -1
  56. package/dist/core/package-manager.d.ts +130 -0
  57. package/dist/core/package-manager.d.ts.map +1 -0
  58. package/dist/core/package-manager.js +1177 -0
  59. package/dist/core/package-manager.js.map +1 -0
  60. package/dist/core/prompt-templates.d.ts +6 -0
  61. package/dist/core/prompt-templates.d.ts.map +1 -1
  62. package/dist/core/prompt-templates.js +114 -54
  63. package/dist/core/prompt-templates.js.map +1 -1
  64. package/dist/core/resource-loader.d.ts +160 -0
  65. package/dist/core/resource-loader.d.ts.map +1 -0
  66. package/dist/core/resource-loader.js +604 -0
  67. package/dist/core/resource-loader.js.map +1 -0
  68. package/dist/core/sdk.d.ts +14 -105
  69. package/dist/core/sdk.d.ts.map +1 -1
  70. package/dist/core/sdk.js +52 -304
  71. package/dist/core/sdk.js.map +1 -1
  72. package/dist/core/session-manager.d.ts.map +1 -1
  73. package/dist/core/session-manager.js +45 -1
  74. package/dist/core/session-manager.js.map +1 -1
  75. package/dist/core/settings-manager.d.ts +34 -16
  76. package/dist/core/settings-manager.d.ts.map +1 -1
  77. package/dist/core/settings-manager.js +104 -25
  78. package/dist/core/settings-manager.js.map +1 -1
  79. package/dist/core/skills.d.ts +18 -10
  80. package/dist/core/skills.d.ts.map +1 -1
  81. package/dist/core/skills.js +126 -93
  82. package/dist/core/skills.js.map +1 -1
  83. package/dist/core/system-prompt.d.ts +3 -27
  84. package/dist/core/system-prompt.d.ts.map +1 -1
  85. package/dist/core/system-prompt.js +16 -103
  86. package/dist/core/system-prompt.js.map +1 -1
  87. package/dist/core/tools/bash.d.ts.map +1 -1
  88. package/dist/core/tools/bash.js +2 -1
  89. package/dist/core/tools/bash.js.map +1 -1
  90. package/dist/core/tools/read.d.ts.map +1 -1
  91. package/dist/core/tools/read.js +4 -4
  92. package/dist/core/tools/read.js.map +1 -1
  93. package/dist/index.d.ts +12 -7
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +8 -6
  96. package/dist/index.js.map +1 -1
  97. package/dist/main.d.ts.map +1 -1
  98. package/dist/main.js +209 -97
  99. package/dist/main.js.map +1 -1
  100. package/dist/modes/interactive/components/bordered-loader.d.ts +5 -1
  101. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/bordered-loader.js +29 -9
  103. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  104. package/dist/modes/interactive/components/config-selector.d.ts +71 -0
  105. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -0
  106. package/dist/modes/interactive/components/config-selector.js +468 -0
  107. package/dist/modes/interactive/components/config-selector.js.map +1 -0
  108. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/footer.js +4 -0
  110. package/dist/modes/interactive/components/footer.js.map +1 -1
  111. package/dist/modes/interactive/components/index.d.ts +1 -0
  112. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/index.js +1 -0
  114. package/dist/modes/interactive/components/index.js.map +1 -1
  115. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  116. package/dist/modes/interactive/components/oauth-selector.js +3 -4
  117. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  118. package/dist/modes/interactive/components/session-selector.d.ts +18 -1
  119. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  120. package/dist/modes/interactive/components/session-selector.js +195 -87
  121. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  122. package/dist/modes/interactive/components/skill-invocation-message.d.ts +17 -0
  123. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -0
  124. package/dist/modes/interactive/components/skill-invocation-message.js +47 -0
  125. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -0
  126. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  127. package/dist/modes/interactive/components/tool-execution.js +5 -5
  128. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  129. package/dist/modes/interactive/interactive-mode.d.ts +42 -2
  130. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  131. package/dist/modes/interactive/interactive-mode.js +538 -204
  132. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  133. package/dist/modes/interactive/theme/dark.json +1 -1
  134. package/dist/modes/interactive/theme/light.json +1 -1
  135. package/dist/modes/interactive/theme/theme-schema.json +8 -1
  136. package/dist/modes/interactive/theme/theme.d.ts +8 -1
  137. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  138. package/dist/modes/interactive/theme/theme.js +72 -25
  139. package/dist/modes/interactive/theme/theme.js.map +1 -1
  140. package/dist/modes/print-mode.d.ts.map +1 -1
  141. package/dist/modes/print-mode.js +7 -74
  142. package/dist/modes/print-mode.js.map +1 -1
  143. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  144. package/dist/modes/rpc/rpc-mode.js +17 -82
  145. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  146. package/dist/utils/git.d.ts +2 -0
  147. package/dist/utils/git.d.ts.map +1 -0
  148. package/dist/utils/git.js +6 -0
  149. package/dist/utils/git.js.map +1 -0
  150. package/dist/utils/shell.d.ts +1 -0
  151. package/dist/utils/shell.d.ts.map +1 -1
  152. package/dist/utils/shell.js +14 -1
  153. package/dist/utils/shell.js.map +1 -1
  154. package/dist/utils/sleep.d.ts +5 -0
  155. package/dist/utils/sleep.d.ts.map +1 -0
  156. package/dist/utils/sleep.js +17 -0
  157. package/dist/utils/sleep.js.map +1 -0
  158. package/docs/compaction.md +23 -21
  159. package/docs/custom-provider.md +538 -0
  160. package/docs/development.md +69 -0
  161. package/docs/extensions.md +182 -118
  162. package/docs/images/doom-extension.png +0 -0
  163. package/docs/images/interactive-mode.png +0 -0
  164. package/docs/images/tree-view.png +0 -0
  165. package/docs/json.md +79 -0
  166. package/docs/keybindings.md +162 -0
  167. package/docs/models.md +193 -0
  168. package/docs/packages.md +168 -0
  169. package/docs/prompt-templates.md +67 -0
  170. package/docs/providers.md +147 -0
  171. package/docs/sdk.md +111 -178
  172. package/docs/session.md +167 -16
  173. package/docs/settings.md +216 -0
  174. package/docs/shell-aliases.md +13 -0
  175. package/docs/skills.md +111 -202
  176. package/docs/terminal-setup.md +65 -0
  177. package/docs/themes.md +295 -0
  178. package/docs/tui.md +36 -5
  179. package/docs/windows.md +17 -0
  180. package/examples/README.md +1 -0
  181. package/examples/extensions/README.md +22 -2
  182. package/examples/extensions/bookmark.ts +50 -0
  183. package/examples/extensions/custom-provider-anthropic/index.ts +604 -0
  184. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  185. package/examples/extensions/custom-provider-anthropic/package.json +19 -0
  186. package/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
  187. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  188. package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
  189. package/examples/extensions/doom-overlay/doom/build.sh +1 -1
  190. package/examples/extensions/event-bus.ts +43 -0
  191. package/examples/extensions/message-renderer.ts +59 -0
  192. package/examples/extensions/session-name.ts +27 -0
  193. package/examples/extensions/with-deps/package-lock.json +2 -2
  194. package/examples/extensions/with-deps/package.json +1 -1
  195. package/examples/sdk/02-custom-model.ts +3 -3
  196. package/examples/sdk/03-custom-prompt.ts +20 -9
  197. package/examples/sdk/04-skills.ts +26 -27
  198. package/examples/sdk/06-extensions.ts +15 -6
  199. package/examples/sdk/07-context-files.ts +22 -18
  200. package/examples/sdk/08-prompt-templates.ts +19 -14
  201. package/examples/sdk/09-api-keys-and-oauth.ts +5 -12
  202. package/examples/sdk/10-settings.ts +3 -3
  203. package/examples/sdk/12-full-control.ts +16 -7
  204. package/examples/sdk/README.md +24 -30
  205. package/package.json +4 -4
  206. package/docs/theme.md +0 -617
  207. package/examples/extensions/chalk-logger.ts +0 -26
@@ -10,12 +10,12 @@ import { getOAuthProviders, } from "@mariozechner/pi-ai";
10
10
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, 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, getShareViewerUrl, isBunBinary, isBunRuntime, VERSION, } from "../../config.js";
13
+ import { parseSkillBlock } from "../../core/agent-session.js";
13
14
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
14
15
  import { KeybindingsManager } from "../../core/keybindings.js";
15
16
  import { createCompactionSummaryMessage } from "../../core/messages.js";
16
17
  import { resolveModelScope } from "../../core/model-resolver.js";
17
18
  import { SessionManager } from "../../core/session-manager.js";
18
- import { loadProjectContextFiles } from "../../core/system-prompt.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";
@@ -40,11 +40,12 @@ import { OAuthSelectorComponent } from "./components/oauth-selector.js";
40
40
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
41
41
  import { SessionSelectorComponent } from "./components/session-selector.js";
42
42
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
43
+ import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
43
44
  import { ToolExecutionComponent } from "./components/tool-execution.js";
44
45
  import { TreeSelectorComponent } from "./components/tree-selector.js";
45
46
  import { UserMessageComponent } from "./components/user-message.js";
46
47
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
47
- import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setTheme, setThemeInstance, Theme, theme, } from "./theme/theme.js";
48
+ import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, Theme, theme, } from "./theme/theme.js";
48
49
  function isExpandable(obj) {
49
50
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
50
51
  }
@@ -65,9 +66,9 @@ export class InteractiveMode {
65
66
  keybindings;
66
67
  version;
67
68
  isInitialized = false;
68
- hasRenderedInitialMessages = false;
69
69
  onInputCallback;
70
70
  loadingAnimation = undefined;
71
+ pendingWorkingMessage = undefined;
71
72
  defaultWorkingMessage = "Working...";
72
73
  lastSigintTime = 0;
73
74
  lastEscapeTime = 0;
@@ -150,7 +151,8 @@ export class InteractiveMode {
150
151
  this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
151
152
  // Load hide thinking block setting
152
153
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
153
- // Initialize theme with watcher for interactive mode
154
+ // Register themes from resource loader and initialize
155
+ setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
154
156
  initTheme(this.settingsManager.getTheme(), true);
155
157
  }
156
158
  setupAutocomplete(fdPath) {
@@ -199,6 +201,7 @@ export class InteractiveMode {
199
201
  { name: "new", description: "Start a new session" },
200
202
  { name: "compact", description: "Manually compact the session context" },
201
203
  { name: "resume", description: "Resume a different session" },
204
+ { name: "reload", description: "Reload extensions, skills, prompts, and themes" },
202
205
  ];
203
206
  // Convert prompt templates to SlashCommand format for autocomplete
204
207
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
@@ -215,7 +218,7 @@ export class InteractiveMode {
215
218
  this.skillCommands.clear();
216
219
  const skillCommandList = [];
217
220
  if (this.settingsManager.getEnableSkillCommands()) {
218
- for (const skill of this.session.skills) {
221
+ for (const skill of this.session.resourceLoader.getSkills().skills) {
219
222
  const commandName = `skill:${skill.name}`;
220
223
  this.skillCommands.set(commandName, skill.filePath);
221
224
  skillCommandList.push({ name: commandName, description: skill.description });
@@ -237,7 +240,7 @@ export class InteractiveMode {
237
240
  this.fdPath = await ensureTool("fd");
238
241
  this.setupAutocomplete(this.fdPath);
239
242
  // Add header with keybindings from config (unless silenced)
240
- if (!this.settingsManager.getQuietStartup()) {
243
+ if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
241
244
  const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
242
245
  // Build startup instructions using keybinding hint helpers
243
246
  const kb = this.keybindings;
@@ -249,11 +252,11 @@ export class InteractiveMode {
249
252
  hint("exit", "to exit (empty)"),
250
253
  hint("suspend", "to suspend"),
251
254
  keyHint("deleteToLineEnd", "to delete to end"),
252
- hint("cycleThinkingLevel", "to cycle thinking"),
255
+ hint("cycleThinkingLevel", "to cycle thinking level"),
253
256
  rawKeyHint(`${appKey(kb, "cycleModelForward")}/${appKey(kb, "cycleModelBackward")}`, "to cycle models"),
254
257
  hint("selectModel", "to select model"),
255
258
  hint("expandTools", "to expand tools"),
256
- hint("toggleThinking", "to toggle thinking"),
259
+ hint("toggleThinking", "to expand thinking"),
257
260
  hint("externalEditor", "for external editor"),
258
261
  rawKeyHint("/", "for commands"),
259
262
  rawKeyHint("!", "to run bash"),
@@ -328,6 +331,8 @@ export class InteractiveMode {
328
331
  this.footerDataProvider.onBranchChange(() => {
329
332
  this.ui.requestRender();
330
333
  });
334
+ // Initialize available provider count for footer display
335
+ await this.updateAvailableProviderCount();
331
336
  }
332
337
  /**
333
338
  * Update terminal title with session name and cwd.
@@ -456,199 +461,358 @@ export class InteractiveMode {
456
461
  // =========================================================================
457
462
  // Extension System
458
463
  // =========================================================================
464
+ formatDisplayPath(p) {
465
+ const home = os.homedir();
466
+ let result = p;
467
+ // Replace home directory with ~
468
+ if (result.startsWith(home)) {
469
+ result = `~${result.slice(home.length)}`;
470
+ }
471
+ return result;
472
+ }
459
473
  /**
460
- * Initialize the extension system with TUI-based UI context.
474
+ * Get a short path relative to the package root for display.
461
475
  */
462
- async initExtensions() {
463
- // Show discovery info unless silenced
464
- if (!this.settingsManager.getQuietStartup()) {
465
- // Show loaded project context files
466
- const contextFiles = loadProjectContextFiles();
467
- if (contextFiles.length > 0) {
468
- const contextList = contextFiles.map((f) => theme.fg("dim", ` ${f.path}`)).join("\n");
469
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded context:\n") + contextList, 0, 0));
470
- this.chatContainer.addChild(new Spacer(1));
476
+ getShortPath(fullPath, source) {
477
+ // For npm packages, show path relative to node_modules/pkg/
478
+ const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
479
+ if (npmMatch && source.startsWith("npm:")) {
480
+ return npmMatch[2];
481
+ }
482
+ // For git packages, show path relative to repo root
483
+ const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
484
+ if (gitMatch && source.startsWith("git:")) {
485
+ return gitMatch[1];
486
+ }
487
+ // For local/auto, just use formatDisplayPath
488
+ return this.formatDisplayPath(fullPath);
489
+ }
490
+ getDisplaySourceInfo(source, scope) {
491
+ if (source === "local") {
492
+ if (scope === "user") {
493
+ return { label: "user", color: "muted" };
494
+ }
495
+ if (scope === "project") {
496
+ return { label: "project", color: "muted" };
497
+ }
498
+ if (scope === "temporary") {
499
+ return { label: "path", scopeLabel: "temp", color: "muted" };
500
+ }
501
+ return { label: "path", color: "muted" };
502
+ }
503
+ if (source === "cli") {
504
+ return { label: "path", scopeLabel: scope === "temporary" ? "temp" : undefined, color: "muted" };
505
+ }
506
+ const scopeLabel = scope === "user" ? "user" : scope === "project" ? "project" : scope === "temporary" ? "temp" : undefined;
507
+ return { label: source, scopeLabel, color: "accent" };
508
+ }
509
+ getScopeGroup(source, scope) {
510
+ if (source === "cli" || scope === "temporary")
511
+ return "path";
512
+ if (scope === "user")
513
+ return "user";
514
+ if (scope === "project")
515
+ return "project";
516
+ return "path";
517
+ }
518
+ isPackageSource(source) {
519
+ return source.startsWith("npm:") || source.startsWith("git:");
520
+ }
521
+ buildScopeGroups(paths, metadata) {
522
+ const groups = {
523
+ user: { scope: "user", paths: [], packages: new Map() },
524
+ project: { scope: "project", paths: [], packages: new Map() },
525
+ path: { scope: "path", paths: [], packages: new Map() },
526
+ };
527
+ for (const p of paths) {
528
+ const meta = this.findMetadata(p, metadata);
529
+ const source = meta?.source ?? "local";
530
+ const scope = meta?.scope ?? "project";
531
+ const groupKey = this.getScopeGroup(source, scope);
532
+ const group = groups[groupKey];
533
+ if (this.isPackageSource(source)) {
534
+ const list = group.packages.get(source) ?? [];
535
+ list.push(p);
536
+ group.packages.set(source, list);
471
537
  }
472
- // Show loaded skills (already discovered by SDK)
473
- const skills = this.session.skills;
474
- if (skills.length > 0) {
475
- const skillList = skills.map((s) => theme.fg("dim", ` ${s.filePath}`)).join("\n");
476
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded skills:\n") + skillList, 0, 0));
477
- this.chatContainer.addChild(new Spacer(1));
538
+ else {
539
+ group.paths.push(p);
478
540
  }
479
- // Show skill warnings if any
480
- const skillWarnings = this.session.skillWarnings;
481
- if (skillWarnings.length > 0) {
482
- const warningList = skillWarnings
483
- .map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`))
484
- .join("\n");
485
- this.chatContainer.addChild(new Text(theme.fg("warning", "Skill warnings:\n") + warningList, 0, 0));
486
- this.chatContainer.addChild(new Spacer(1));
541
+ }
542
+ return [groups.user, groups.project, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
543
+ }
544
+ formatScopeGroups(groups, options) {
545
+ const lines = [];
546
+ for (const group of groups) {
547
+ lines.push(` ${theme.fg("accent", group.scope)}`);
548
+ const sortedPaths = [...group.paths].sort((a, b) => a.localeCompare(b));
549
+ for (const p of sortedPaths) {
550
+ lines.push(theme.fg("dim", ` ${options.formatPath(p)}`));
487
551
  }
488
- // Show loaded prompt templates
489
- const templates = this.session.promptTemplates;
490
- if (templates.length > 0) {
491
- const templateList = templates.map((t) => theme.fg("dim", ` /${t.name} ${t.source}`)).join("\n");
492
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded prompt templates:\n") + templateList, 0, 0));
493
- this.chatContainer.addChild(new Spacer(1));
552
+ const sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));
553
+ for (const [source, paths] of sortedPackages) {
554
+ lines.push(` ${theme.fg("mdLink", source)}`);
555
+ const sortedPackagePaths = [...paths].sort((a, b) => a.localeCompare(b));
556
+ for (const p of sortedPackagePaths) {
557
+ lines.push(theme.fg("dim", ` ${options.formatPackagePath(p, source)}`));
558
+ }
494
559
  }
495
560
  }
496
- const extensionRunner = this.session.extensionRunner;
497
- if (!extensionRunner) {
498
- return; // No extensions loaded
561
+ return lines.join("\n");
562
+ }
563
+ /**
564
+ * Find metadata for a path, checking parent directories if exact match fails.
565
+ * Package manager stores metadata for directories, but we display file paths.
566
+ */
567
+ findMetadata(p, metadata) {
568
+ // Try exact match first
569
+ const exact = metadata.get(p);
570
+ if (exact)
571
+ return exact;
572
+ // Try parent directories (package manager stores directory paths)
573
+ let current = p;
574
+ while (current.includes("/")) {
575
+ current = current.substring(0, current.lastIndexOf("/"));
576
+ const parent = metadata.get(current);
577
+ if (parent)
578
+ return parent;
579
+ }
580
+ return undefined;
581
+ }
582
+ /**
583
+ * Format a path with its source/scope info from metadata.
584
+ */
585
+ formatPathWithSource(p, metadata) {
586
+ const meta = this.findMetadata(p, metadata);
587
+ if (meta) {
588
+ const shortPath = this.getShortPath(p, meta.source);
589
+ const { label, scopeLabel } = this.getDisplaySourceInfo(meta.source, meta.scope);
590
+ const labelText = scopeLabel ? `${label} (${scopeLabel})` : label;
591
+ return `${labelText} ${shortPath}`;
592
+ }
593
+ return this.formatDisplayPath(p);
594
+ }
595
+ /**
596
+ * Format resource diagnostics with nice collision display using metadata.
597
+ */
598
+ formatDiagnostics(diagnostics, metadata) {
599
+ const lines = [];
600
+ // Group collision diagnostics by name
601
+ const collisions = new Map();
602
+ const otherDiagnostics = [];
603
+ for (const d of diagnostics) {
604
+ if (d.type === "collision" && d.collision) {
605
+ const list = collisions.get(d.collision.name) ?? [];
606
+ list.push(d);
607
+ collisions.set(d.collision.name, list);
608
+ }
609
+ else {
610
+ otherDiagnostics.push(d);
611
+ }
612
+ }
613
+ // Format collision diagnostics grouped by name
614
+ for (const [name, collisionList] of collisions) {
615
+ const first = collisionList[0]?.collision;
616
+ if (!first)
617
+ continue;
618
+ lines.push(theme.fg("warning", ` "${name}" collision:`));
619
+ // Show winner
620
+ lines.push(theme.fg("dim", ` ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, metadata)}`));
621
+ // Show all losers
622
+ for (const d of collisionList) {
623
+ if (d.collision) {
624
+ lines.push(theme.fg("dim", ` ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, metadata)} (skipped)`));
625
+ }
626
+ }
627
+ }
628
+ // Format other diagnostics (skill name collisions, parse errors, etc.)
629
+ for (const d of otherDiagnostics) {
630
+ if (d.path) {
631
+ // Use metadata-aware formatting for paths
632
+ const sourceInfo = this.formatPathWithSource(d.path, metadata);
633
+ lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${sourceInfo}`));
634
+ lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`));
635
+ }
636
+ else {
637
+ lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`));
638
+ }
639
+ }
640
+ return lines.join("\n");
641
+ }
642
+ showLoadedResources(options) {
643
+ const shouldShow = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
644
+ if (!shouldShow) {
645
+ return;
499
646
  }
500
- // Create extension UI context
647
+ const metadata = this.session.resourceLoader.getPathMetadata();
648
+ const sectionHeader = (name, color = "mdHeading") => theme.fg(color, `[${name}]`);
649
+ const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
650
+ if (contextFiles.length > 0) {
651
+ const contextList = contextFiles.map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`)).join("\n");
652
+ this.chatContainer.addChild(new Text(`${sectionHeader("Context")}\n${contextList}`, 0, 0));
653
+ this.chatContainer.addChild(new Spacer(1));
654
+ }
655
+ const skills = this.session.resourceLoader.getSkills().skills;
656
+ if (skills.length > 0) {
657
+ const skillPaths = skills.map((s) => s.filePath);
658
+ const groups = this.buildScopeGroups(skillPaths, metadata);
659
+ const skillList = this.formatScopeGroups(groups, {
660
+ formatPath: (p) => this.formatDisplayPath(p),
661
+ formatPackagePath: (p, source) => this.getShortPath(p, source),
662
+ });
663
+ this.chatContainer.addChild(new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0));
664
+ this.chatContainer.addChild(new Spacer(1));
665
+ }
666
+ const skillDiagnostics = this.session.resourceLoader.getSkills().diagnostics;
667
+ if (skillDiagnostics.length > 0) {
668
+ const warningLines = this.formatDiagnostics(skillDiagnostics, metadata);
669
+ this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
670
+ this.chatContainer.addChild(new Spacer(1));
671
+ }
672
+ const templates = this.session.promptTemplates;
673
+ if (templates.length > 0) {
674
+ const templatePaths = templates.map((t) => t.filePath);
675
+ const groups = this.buildScopeGroups(templatePaths, metadata);
676
+ const templateByPath = new Map(templates.map((t) => [t.filePath, t]));
677
+ const templateList = this.formatScopeGroups(groups, {
678
+ formatPath: (p) => {
679
+ const template = templateByPath.get(p);
680
+ return template ? `/${template.name}` : this.formatDisplayPath(p);
681
+ },
682
+ formatPackagePath: (p) => {
683
+ const template = templateByPath.get(p);
684
+ return template ? `/${template.name}` : this.formatDisplayPath(p);
685
+ },
686
+ });
687
+ this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0));
688
+ this.chatContainer.addChild(new Spacer(1));
689
+ }
690
+ const promptDiagnostics = this.session.resourceLoader.getPrompts().diagnostics;
691
+ if (promptDiagnostics.length > 0) {
692
+ const warningLines = this.formatDiagnostics(promptDiagnostics, metadata);
693
+ this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
694
+ this.chatContainer.addChild(new Spacer(1));
695
+ }
696
+ const extensionPaths = options?.extensionPaths ?? [];
697
+ if (extensionPaths.length > 0) {
698
+ const groups = this.buildScopeGroups(extensionPaths, metadata);
699
+ const extList = this.formatScopeGroups(groups, {
700
+ formatPath: (p) => this.formatDisplayPath(p),
701
+ formatPackagePath: (p, source) => this.getShortPath(p, source),
702
+ });
703
+ this.chatContainer.addChild(new Text(`${sectionHeader("Extensions", "mdHeading")}\n${extList}`, 0, 0));
704
+ this.chatContainer.addChild(new Spacer(1));
705
+ }
706
+ const extensionDiagnostics = [];
707
+ const extensionErrors = this.session.resourceLoader.getExtensions().errors;
708
+ if (extensionErrors.length > 0) {
709
+ for (const error of extensionErrors) {
710
+ extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
711
+ }
712
+ }
713
+ const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];
714
+ extensionDiagnostics.push(...shortcutDiagnostics);
715
+ if (extensionDiagnostics.length > 0) {
716
+ const warningLines = this.formatDiagnostics(extensionDiagnostics, metadata);
717
+ this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
718
+ this.chatContainer.addChild(new Spacer(1));
719
+ }
720
+ // Show loaded themes (excluding built-in)
721
+ const loadedThemes = this.session.resourceLoader.getThemes().themes;
722
+ const customThemes = loadedThemes.filter((t) => t.sourcePath);
723
+ if (customThemes.length > 0) {
724
+ const themePaths = customThemes.map((t) => t.sourcePath);
725
+ const groups = this.buildScopeGroups(themePaths, metadata);
726
+ const themeList = this.formatScopeGroups(groups, {
727
+ formatPath: (p) => this.formatDisplayPath(p),
728
+ formatPackagePath: (p, source) => this.getShortPath(p, source),
729
+ });
730
+ this.chatContainer.addChild(new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0));
731
+ this.chatContainer.addChild(new Spacer(1));
732
+ }
733
+ const themeDiagnostics = this.session.resourceLoader.getThemes().diagnostics;
734
+ if (themeDiagnostics.length > 0) {
735
+ const warningLines = this.formatDiagnostics(themeDiagnostics, metadata);
736
+ this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
737
+ this.chatContainer.addChild(new Spacer(1));
738
+ }
739
+ }
740
+ /**
741
+ * Initialize the extension system with TUI-based UI context.
742
+ */
743
+ async initExtensions() {
501
744
  const uiContext = this.createExtensionUIContext();
502
- extensionRunner.initialize(
503
- // ExtensionActions - for pi.* API
504
- {
505
- sendMessage: (message, options) => {
506
- const wasStreaming = this.session.isStreaming;
507
- this.session
508
- .sendCustomMessage(message, options)
509
- .then(() => {
510
- // Don't rebuild if initial render hasn't happened yet
511
- // (renderInitialMessages will handle it)
512
- if (!wasStreaming && message.display && this.hasRenderedInitialMessages) {
513
- this.rebuildChatFromMessages();
745
+ await this.session.bindExtensions({
746
+ uiContext,
747
+ commandContextActions: {
748
+ waitForIdle: () => this.session.agent.waitForIdle(),
749
+ newSession: async (options) => {
750
+ if (this.loadingAnimation) {
751
+ this.loadingAnimation.stop();
752
+ this.loadingAnimation = undefined;
514
753
  }
515
- })
516
- .catch((err) => {
517
- this.showError(`Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`);
518
- });
519
- },
520
- sendUserMessage: (content, options) => {
521
- this.session.sendUserMessage(content, options).catch((err) => {
522
- this.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
523
- });
524
- },
525
- appendEntry: (customType, data) => {
526
- this.sessionManager.appendCustomEntry(customType, data);
527
- },
528
- setSessionName: (name) => {
529
- this.sessionManager.appendSessionInfo(name);
530
- this.updateTerminalTitle();
531
- },
532
- getSessionName: () => {
533
- return this.sessionManager.getSessionName();
534
- },
535
- setLabel: (entryId, label) => {
536
- this.sessionManager.appendLabelChange(entryId, label);
537
- },
538
- getActiveTools: () => this.session.getActiveToolNames(),
539
- getAllTools: () => this.session.getAllTools(),
540
- setActiveTools: (toolNames) => this.session.setActiveToolsByName(toolNames),
541
- setModel: async (model) => {
542
- const key = await this.session.modelRegistry.getApiKey(model);
543
- if (!key)
544
- return false;
545
- await this.session.setModel(model);
546
- return true;
547
- },
548
- getThinkingLevel: () => this.session.thinkingLevel,
549
- setThinkingLevel: (level) => this.session.setThinkingLevel(level),
550
- },
551
- // ExtensionContextActions - for ctx.* in event handlers
552
- {
553
- getModel: () => this.session.model,
554
- isIdle: () => !this.session.isStreaming,
555
- abort: () => this.session.abort(),
556
- hasPendingMessages: () => this.session.pendingMessageCount > 0,
557
- shutdown: () => {
558
- this.shutdownRequested = true;
559
- },
560
- getContextUsage: () => this.session.getContextUsage(),
561
- compact: (options) => {
562
- void (async () => {
563
- try {
564
- const result = await this.executeCompaction(options?.customInstructions, false);
565
- if (result) {
566
- options?.onComplete?.(result);
567
- }
754
+ this.statusContainer.clear();
755
+ // Delegate to AgentSession (handles setup + agent state sync)
756
+ const success = await this.session.newSession(options);
757
+ if (!success) {
758
+ return { cancelled: true };
568
759
  }
569
- catch (error) {
570
- const err = error instanceof Error ? error : new Error(String(error));
571
- options?.onError?.(err);
760
+ // Clear UI state
761
+ this.chatContainer.clear();
762
+ this.pendingMessagesContainer.clear();
763
+ this.compactionQueuedMessages = [];
764
+ this.streamingComponent = undefined;
765
+ this.streamingMessage = undefined;
766
+ this.pendingTools.clear();
767
+ // Render any messages added via setup, or show empty session
768
+ this.renderInitialMessages();
769
+ this.ui.requestRender();
770
+ return { cancelled: false };
771
+ },
772
+ fork: async (entryId) => {
773
+ const result = await this.session.fork(entryId);
774
+ if (result.cancelled) {
775
+ return { cancelled: true };
572
776
  }
573
- })();
574
- },
575
- },
576
- // ExtensionCommandContextActions - for ctx.* in command handlers
577
- {
578
- waitForIdle: () => this.session.agent.waitForIdle(),
579
- newSession: async (options) => {
580
- if (this.loadingAnimation) {
581
- this.loadingAnimation.stop();
582
- this.loadingAnimation = undefined;
583
- }
584
- this.statusContainer.clear();
585
- const success = await this.session.newSession({ parentSession: options?.parentSession });
586
- if (!success) {
587
- return { cancelled: true };
588
- }
589
- if (options?.setup) {
590
- await options.setup(this.sessionManager);
591
- }
592
- this.chatContainer.clear();
593
- this.pendingMessagesContainer.clear();
594
- this.compactionQueuedMessages = [];
595
- this.streamingComponent = undefined;
596
- this.streamingMessage = undefined;
597
- this.pendingTools.clear();
598
- this.chatContainer.addChild(new Spacer(1));
599
- this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
600
- this.ui.requestRender();
601
- return { cancelled: false };
777
+ this.chatContainer.clear();
778
+ this.renderInitialMessages();
779
+ this.editor.setText(result.selectedText);
780
+ this.showStatus("Forked to new session");
781
+ return { cancelled: false };
782
+ },
783
+ navigateTree: async (targetId, options) => {
784
+ const result = await this.session.navigateTree(targetId, {
785
+ summarize: options?.summarize,
786
+ customInstructions: options?.customInstructions,
787
+ replaceInstructions: options?.replaceInstructions,
788
+ label: options?.label,
789
+ });
790
+ if (result.cancelled) {
791
+ return { cancelled: true };
792
+ }
793
+ this.chatContainer.clear();
794
+ this.renderInitialMessages();
795
+ if (result.editorText) {
796
+ this.editor.setText(result.editorText);
797
+ }
798
+ this.showStatus("Navigated to selected point");
799
+ return { cancelled: false };
800
+ },
602
801
  },
603
- fork: async (entryId) => {
604
- const result = await this.session.fork(entryId);
605
- if (result.cancelled) {
606
- return { cancelled: true };
607
- }
608
- this.chatContainer.clear();
609
- this.renderInitialMessages();
610
- this.editor.setText(result.selectedText);
611
- this.showStatus("Forked to new session");
612
- return { cancelled: false };
802
+ shutdownHandler: () => {
803
+ this.shutdownRequested = true;
613
804
  },
614
- navigateTree: async (targetId, options) => {
615
- const result = await this.session.navigateTree(targetId, {
616
- summarize: options?.summarize,
617
- customInstructions: options?.customInstructions,
618
- replaceInstructions: options?.replaceInstructions,
619
- label: options?.label,
620
- });
621
- if (result.cancelled) {
622
- return { cancelled: true };
623
- }
624
- this.chatContainer.clear();
625
- this.renderInitialMessages();
626
- if (result.editorText) {
627
- this.editor.setText(result.editorText);
628
- }
629
- this.showStatus("Navigated to selected point");
630
- return { cancelled: false };
805
+ onError: (error) => {
806
+ this.showExtensionError(error.extensionPath, error.error, error.stack);
631
807
  },
632
- }, uiContext);
633
- // Subscribe to extension errors
634
- extensionRunner.onError((error) => {
635
- this.showExtensionError(error.extensionPath, error.error, error.stack);
636
808
  });
637
- // Set up extension-registered shortcuts
638
- this.setupExtensionShortcuts(extensionRunner);
639
- // Show loaded extensions (unless silenced)
640
- if (!this.settingsManager.getQuietStartup()) {
641
- const extensionPaths = extensionRunner.getExtensionPaths();
642
- if (extensionPaths.length > 0) {
643
- const extList = extensionPaths.map((p) => theme.fg("dim", ` ${p}`)).join("\n");
644
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded extensions:\n") + extList, 0, 0));
645
- this.chatContainer.addChild(new Spacer(1));
646
- }
809
+ const extensionRunner = this.session.extensionRunner;
810
+ if (!extensionRunner) {
811
+ this.showLoadedResources({ extensionPaths: [], force: false });
812
+ return;
647
813
  }
648
- // Emit session_start event
649
- await extensionRunner.emit({
650
- type: "session_start",
651
- });
814
+ this.setupExtensionShortcuts(extensionRunner);
815
+ this.showLoadedResources({ extensionPaths: extensionRunner.getExtensionPaths(), force: false });
652
816
  }
653
817
  /**
654
818
  * Get a registered tool definition by name (for custom rendering).
@@ -754,6 +918,40 @@ export class InteractiveMode {
754
918
  targetMap.set(key, component);
755
919
  this.renderWidgets();
756
920
  }
921
+ clearExtensionWidgets() {
922
+ for (const widget of this.extensionWidgetsAbove.values()) {
923
+ widget.dispose?.();
924
+ }
925
+ for (const widget of this.extensionWidgetsBelow.values()) {
926
+ widget.dispose?.();
927
+ }
928
+ this.extensionWidgetsAbove.clear();
929
+ this.extensionWidgetsBelow.clear();
930
+ this.renderWidgets();
931
+ }
932
+ resetExtensionUI() {
933
+ if (this.extensionSelector) {
934
+ this.hideExtensionSelector();
935
+ }
936
+ if (this.extensionInput) {
937
+ this.hideExtensionInput();
938
+ }
939
+ if (this.extensionEditor) {
940
+ this.hideExtensionEditor();
941
+ }
942
+ this.ui.hideOverlay();
943
+ this.setExtensionFooter(undefined);
944
+ this.setExtensionHeader(undefined);
945
+ this.clearExtensionWidgets();
946
+ this.footerDataProvider.clearExtensionStatuses();
947
+ this.footer.invalidate();
948
+ this.setCustomEditorComponent(undefined);
949
+ this.defaultEditor.onExtensionShortcut = undefined;
950
+ this.updateTerminalTitle();
951
+ if (this.loadingAnimation) {
952
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
953
+ }
954
+ }
757
955
  // Maximum total widget lines to prevent viewport overflow
758
956
  static MAX_WIDGET_LINES = 10;
759
957
  /**
@@ -858,6 +1056,10 @@ export class InteractiveMode {
858
1056
  this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
859
1057
  }
860
1058
  }
1059
+ else {
1060
+ // Queue message for when loadingAnimation is created (handles agent_start race)
1061
+ this.pendingWorkingMessage = message;
1062
+ }
861
1063
  },
862
1064
  setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
863
1065
  setFooter: (factory) => this.setExtensionFooter(factory),
@@ -1022,6 +1224,9 @@ export class InteractiveMode {
1022
1224
  if (newEditor.borderColor !== undefined) {
1023
1225
  newEditor.borderColor = this.defaultEditor.borderColor;
1024
1226
  }
1227
+ if (newEditor.setPaddingX !== undefined) {
1228
+ newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1229
+ }
1025
1230
  // Set autocomplete if supported
1026
1231
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1027
1232
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1033,7 +1238,7 @@ export class InteractiveMode {
1033
1238
  customEditor.onEscape = this.defaultEditor.onEscape;
1034
1239
  customEditor.onCtrlD = this.defaultEditor.onCtrlD;
1035
1240
  customEditor.onPasteImage = this.defaultEditor.onPasteImage;
1036
- customEditor.onExtensionShortcut = this.defaultEditor.onExtensionShortcut;
1241
+ customEditor.onExtensionShortcut = (data) => this.defaultEditor.onExtensionShortcut?.(data);
1037
1242
  // Copy action handlers (clear, suspend, model switching, etc.)
1038
1243
  for (const [action, handler] of this.defaultEditor.actionHandlers) {
1039
1244
  customEditor.actionHandlers.set(action, handler);
@@ -1323,6 +1528,11 @@ export class InteractiveMode {
1323
1528
  await this.handleCompactCommand(customInstructions);
1324
1529
  return;
1325
1530
  }
1531
+ if (text === "/reload") {
1532
+ this.editor.setText("");
1533
+ await this.handleReloadCommand();
1534
+ return;
1535
+ }
1326
1536
  if (text === "/debug") {
1327
1537
  this.handleDebugCommand();
1328
1538
  this.editor.setText("");
@@ -1419,6 +1629,13 @@ export class InteractiveMode {
1419
1629
  this.statusContainer.clear();
1420
1630
  this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
1421
1631
  this.statusContainer.addChild(this.loadingAnimation);
1632
+ // Apply any pending working message queued before loader existed
1633
+ if (this.pendingWorkingMessage !== undefined) {
1634
+ if (this.pendingWorkingMessage) {
1635
+ this.loadingAnimation.setMessage(this.pendingWorkingMessage);
1636
+ }
1637
+ this.pendingWorkingMessage = undefined;
1638
+ }
1422
1639
  this.ui.requestRender();
1423
1640
  break;
1424
1641
  case "message_start":
@@ -1703,8 +1920,23 @@ export class InteractiveMode {
1703
1920
  case "user": {
1704
1921
  const textContent = this.getUserMessageText(message);
1705
1922
  if (textContent) {
1706
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
1707
- this.chatContainer.addChild(userComponent);
1923
+ const skillBlock = parseSkillBlock(textContent);
1924
+ if (skillBlock) {
1925
+ // Render skill block (collapsible)
1926
+ this.chatContainer.addChild(new Spacer(1));
1927
+ const component = new SkillInvocationMessageComponent(skillBlock, this.getMarkdownThemeWithSettings());
1928
+ component.setExpanded(this.toolOutputExpanded);
1929
+ this.chatContainer.addChild(component);
1930
+ // Render user message separately if present
1931
+ if (skillBlock.userMessage) {
1932
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings());
1933
+ this.chatContainer.addChild(userComponent);
1934
+ }
1935
+ }
1936
+ else {
1937
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
1938
+ this.chatContainer.addChild(userComponent);
1939
+ }
1708
1940
  if (options?.populateHistory) {
1709
1941
  this.editor.addToHistory?.(textContent);
1710
1942
  }
@@ -1784,7 +2016,6 @@ export class InteractiveMode {
1784
2016
  this.ui.requestRender();
1785
2017
  }
1786
2018
  renderInitialMessages() {
1787
- this.hasRenderedInitialMessages = true;
1788
2019
  // Get aligned messages and entries from session context
1789
2020
  const context = this.sessionManager.buildSessionContext();
1790
2021
  this.renderSessionContext(context, {
@@ -1871,7 +2102,7 @@ export class InteractiveMode {
1871
2102
  process.kill(0, "SIGTSTP");
1872
2103
  }
1873
2104
  async handleFollowUp() {
1874
- const text = this.editor.getText().trim();
2105
+ const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
1875
2106
  if (!text)
1876
2107
  return;
1877
2108
  // Queue input during compaction (extension commands execute immediately)
@@ -2034,22 +2265,51 @@ export class InteractiveMode {
2034
2265
  ? `Download from: ${theme.fg("accent", "https://github.com/badlogic/pi-mono/releases/latest")}`
2035
2266
  : `Run: ${theme.fg("accent", `${isBunRuntime ? "bun" : "npm"} install -g @mariozechner/pi-coding-agent`)}`;
2036
2267
  const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
2268
+ const changelogUrl = theme.fg("accent", "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md");
2269
+ const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
2037
2270
  this.chatContainer.addChild(new Spacer(1));
2038
2271
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2039
- this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0));
2272
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
2040
2273
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2041
2274
  this.ui.requestRender();
2042
2275
  }
2276
+ /**
2277
+ * Get all queued messages (read-only).
2278
+ * Combines session queue and compaction queue.
2279
+ */
2280
+ getAllQueuedMessages() {
2281
+ return {
2282
+ steering: [
2283
+ ...this.session.getSteeringMessages(),
2284
+ ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
2285
+ ],
2286
+ followUp: [
2287
+ ...this.session.getFollowUpMessages(),
2288
+ ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
2289
+ ],
2290
+ };
2291
+ }
2292
+ /**
2293
+ * Clear all queued messages and return their contents.
2294
+ * Clears both session queue and compaction queue.
2295
+ */
2296
+ clearAllQueues() {
2297
+ const { steering, followUp } = this.session.clearQueue();
2298
+ const compactionSteering = this.compactionQueuedMessages
2299
+ .filter((msg) => msg.mode === "steer")
2300
+ .map((msg) => msg.text);
2301
+ const compactionFollowUp = this.compactionQueuedMessages
2302
+ .filter((msg) => msg.mode === "followUp")
2303
+ .map((msg) => msg.text);
2304
+ this.compactionQueuedMessages = [];
2305
+ return {
2306
+ steering: [...steering, ...compactionSteering],
2307
+ followUp: [...followUp, ...compactionFollowUp],
2308
+ };
2309
+ }
2043
2310
  updatePendingMessagesDisplay() {
2044
2311
  this.pendingMessagesContainer.clear();
2045
- const steeringMessages = [
2046
- ...this.session.getSteeringMessages(),
2047
- ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
2048
- ];
2049
- const followUpMessages = [
2050
- ...this.session.getFollowUpMessages(),
2051
- ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
2052
- ];
2312
+ const { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();
2053
2313
  if (steeringMessages.length > 0 || followUpMessages.length > 0) {
2054
2314
  this.pendingMessagesContainer.addChild(new Spacer(1));
2055
2315
  for (const message of steeringMessages) {
@@ -2066,7 +2326,7 @@ export class InteractiveMode {
2066
2326
  }
2067
2327
  }
2068
2328
  restoreQueuedMessagesToEditor(options) {
2069
- const { steering, followUp } = this.session.clearQueue();
2329
+ const { steering, followUp } = this.clearAllQueues();
2070
2330
  const allQueued = [...steering, ...followUp];
2071
2331
  if (allQueued.length === 0) {
2072
2332
  this.updatePendingMessagesDisplay();
@@ -2294,6 +2554,9 @@ export class InteractiveMode {
2294
2554
  onEditorPaddingXChange: (padding) => {
2295
2555
  this.settingsManager.setEditorPaddingX(padding);
2296
2556
  this.defaultEditor.setPaddingX(padding);
2557
+ if (this.editor !== this.defaultEditor && this.editor.setPaddingX !== undefined) {
2558
+ this.editor.setPaddingX(padding);
2559
+ }
2297
2560
  },
2298
2561
  onCancel: () => {
2299
2562
  done();
@@ -2359,6 +2622,12 @@ export class InteractiveMode {
2359
2622
  return [];
2360
2623
  }
2361
2624
  }
2625
+ /** Update the footer's available provider count from current model candidates */
2626
+ async updateAvailableProviderCount() {
2627
+ const models = await this.getModelCandidates();
2628
+ const uniqueProviders = new Set(models.map((m) => m.provider));
2629
+ this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
2630
+ }
2362
2631
  showModelSelector(initialSearchInput) {
2363
2632
  this.showSelector((done) => {
2364
2633
  const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
@@ -2632,8 +2901,17 @@ export class InteractiveMode {
2632
2901
  this.ui.requestRender();
2633
2902
  }, () => {
2634
2903
  void this.shutdown();
2635
- }, () => this.ui.requestRender(), this.sessionManager.getSessionFile());
2636
- return { component: selector, focus: selector.getSessionList() };
2904
+ }, () => this.ui.requestRender(), {
2905
+ renameSession: async (sessionFilePath, nextName) => {
2906
+ const next = (nextName ?? "").trim();
2907
+ if (!next)
2908
+ return;
2909
+ const mgr = SessionManager.open(sessionFilePath);
2910
+ mgr.appendSessionInfo(next);
2911
+ },
2912
+ showRenameHint: true,
2913
+ }, this.sessionManager.getSessionFile());
2914
+ return { component: selector, focus: selector };
2637
2915
  });
2638
2916
  }
2639
2917
  async handleResumeSession(sessionPath) {
@@ -2678,6 +2956,7 @@ export class InteractiveMode {
2678
2956
  try {
2679
2957
  this.session.modelRegistry.authStorage.logout(providerId);
2680
2958
  this.session.modelRegistry.refresh();
2959
+ await this.updateAvailableProviderCount();
2681
2960
  this.showStatus(`Logged out of ${providerName}`);
2682
2961
  }
2683
2962
  catch (error) {
@@ -2695,7 +2974,7 @@ export class InteractiveMode {
2695
2974
  const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
2696
2975
  const providerName = providerInfo?.name || providerId;
2697
2976
  // Providers that use callback servers (can paste redirect URL)
2698
- const usesCallbackServer = providerId === "openai-codex" || providerId === "google-gemini-cli" || providerId === "google-antigravity";
2977
+ const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
2699
2978
  // Create login dialog component
2700
2979
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
2701
2980
  // Completion handled below
@@ -2758,6 +3037,7 @@ export class InteractiveMode {
2758
3037
  // Success
2759
3038
  restoreEditor();
2760
3039
  this.session.modelRegistry.refresh();
3040
+ await this.updateAvailableProviderCount();
2761
3041
  this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
2762
3042
  }
2763
3043
  catch (error) {
@@ -2771,6 +3051,53 @@ export class InteractiveMode {
2771
3051
  // =========================================================================
2772
3052
  // Command handlers
2773
3053
  // =========================================================================
3054
+ async handleReloadCommand() {
3055
+ if (this.session.isStreaming) {
3056
+ this.showWarning("Wait for the current response to finish before reloading.");
3057
+ return;
3058
+ }
3059
+ if (this.session.isCompacting) {
3060
+ this.showWarning("Wait for compaction to finish before reloading.");
3061
+ return;
3062
+ }
3063
+ this.resetExtensionUI();
3064
+ const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", {
3065
+ cancellable: false,
3066
+ });
3067
+ const previousEditor = this.editor;
3068
+ this.editorContainer.clear();
3069
+ this.editorContainer.addChild(loader);
3070
+ this.ui.setFocus(loader);
3071
+ this.ui.requestRender();
3072
+ const dismissLoader = (editor) => {
3073
+ loader.dispose();
3074
+ this.editorContainer.clear();
3075
+ this.editorContainer.addChild(editor);
3076
+ this.ui.setFocus(editor);
3077
+ this.ui.requestRender();
3078
+ };
3079
+ try {
3080
+ await this.session.reload();
3081
+ setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3082
+ this.rebuildAutocomplete();
3083
+ const runner = this.session.extensionRunner;
3084
+ if (runner) {
3085
+ this.setupExtensionShortcuts(runner);
3086
+ }
3087
+ this.rebuildChatFromMessages();
3088
+ dismissLoader(this.editor);
3089
+ this.showLoadedResources({ extensionPaths: runner?.getExtensionPaths() ?? [], force: true });
3090
+ const modelsJsonError = this.session.modelRegistry.getError();
3091
+ if (modelsJsonError) {
3092
+ this.showError(`models.json error: ${modelsJsonError}`);
3093
+ }
3094
+ this.showStatus("Reloaded extensions, skills, prompts, themes");
3095
+ }
3096
+ catch (error) {
3097
+ dismissLoader(previousEditor);
3098
+ this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
3099
+ }
3100
+ }
2774
3101
  async handleExportCommand(text) {
2775
3102
  const parts = text.split(/\s+/);
2776
3103
  const outputPath = parts.length > 1 ? parts[1] : undefined;
@@ -2983,6 +3310,8 @@ export class InteractiveMode {
2983
3310
  const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
2984
3311
  const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
2985
3312
  const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
3313
+ const pageUp = this.getEditorKeyDisplay("pageUp");
3314
+ const pageDown = this.getEditorKeyDisplay("pageDown");
2986
3315
  // Editing keybindings
2987
3316
  const submit = this.getEditorKeyDisplay("submit");
2988
3317
  const newLine = this.getEditorKeyDisplay("newLine");
@@ -3001,6 +3330,7 @@ export class InteractiveMode {
3001
3330
  const suspend = this.getAppKeyDisplay("suspend");
3002
3331
  const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
3003
3332
  const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
3333
+ const selectModel = this.getAppKeyDisplay("selectModel");
3004
3334
  const expandTools = this.getAppKeyDisplay("expandTools");
3005
3335
  const toggleThinking = this.getAppKeyDisplay("toggleThinking");
3006
3336
  const externalEditor = this.getAppKeyDisplay("externalEditor");
@@ -3014,6 +3344,7 @@ export class InteractiveMode {
3014
3344
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
3015
3345
  | \`${cursorLineStart}\` | Start of line |
3016
3346
  | \`${cursorLineEnd}\` | End of line |
3347
+ | \`${pageUp}\` / \`${pageDown}\` | Scroll by page |
3017
3348
 
3018
3349
  **Editing**
3019
3350
  | Key | Action |
@@ -3038,6 +3369,7 @@ export class InteractiveMode {
3038
3369
  | \`${suspend}\` | Suspend to background |
3039
3370
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
3040
3371
  | \`${cycleModelForward}\` | Cycle models |
3372
+ | \`${selectModel}\` | Open model selector |
3041
3373
  | \`${expandTools}\` | Toggle tool output expansion |
3042
3374
  | \`${toggleThinking}\` | Toggle thinking block visibility |
3043
3375
  | \`${externalEditor}\` | Edit message in external editor |
@@ -3060,7 +3392,8 @@ export class InteractiveMode {
3060
3392
  `;
3061
3393
  for (const [key, shortcut] of shortcuts) {
3062
3394
  const description = shortcut.description ?? shortcut.extensionPath;
3063
- hotkeys += `| \`${key}\` | ${description} |\n`;
3395
+ const keyDisplay = key.replace(/\b\w/g, (c) => c.toUpperCase());
3396
+ hotkeys += `| \`${keyDisplay}\` | ${description} |\n`;
3064
3397
  }
3065
3398
  }
3066
3399
  }
@@ -3094,11 +3427,12 @@ export class InteractiveMode {
3094
3427
  }
3095
3428
  handleDebugCommand() {
3096
3429
  const width = this.ui.terminal.columns;
3430
+ const height = this.ui.terminal.rows;
3097
3431
  const allLines = this.ui.render(width);
3098
3432
  const debugLogPath = getDebugLogPath();
3099
3433
  const debugData = [
3100
3434
  `Debug output at ${new Date().toISOString()}`,
3101
- `Terminal width: ${width}`,
3435
+ `Terminal: ${width}x${height}`,
3102
3436
  `Total lines: ${allLines.length}`,
3103
3437
  "",
3104
3438
  "=== All rendered lines with visible widths ===",