@mariozechner/pi-coding-agent 0.49.3 → 0.50.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 (207) hide show
  1. package/CHANGELOG.md +99 -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 +53 -34
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +262 -67
  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 +129 -0
  57. package/dist/core/package-manager.d.ts.map +1 -0
  58. package/dist/core/package-manager.js +1148 -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 +535 -200
  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 +25 -89
  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 +32 -92
  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 +180 -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 +163 -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,360 @@ 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
  }
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;
646
+ }
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() {
496
744
  const extensionRunner = this.session.extensionRunner;
497
745
  if (!extensionRunner) {
498
- return; // No extensions loaded
746
+ this.showLoadedResources({ extensionPaths: [], force: false });
747
+ return;
499
748
  }
500
749
  // Create extension UI context
501
750
  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();
751
+ await this.session.bindExtensions({
752
+ uiContext,
753
+ commandContextActions: {
754
+ waitForIdle: () => this.session.agent.waitForIdle(),
755
+ newSession: async (options) => {
756
+ if (this.loadingAnimation) {
757
+ this.loadingAnimation.stop();
758
+ this.loadingAnimation = undefined;
514
759
  }
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
- }
760
+ this.statusContainer.clear();
761
+ const success = await this.session.newSession({ parentSession: options?.parentSession });
762
+ if (!success) {
763
+ return { cancelled: true };
568
764
  }
569
- catch (error) {
570
- const err = error instanceof Error ? error : new Error(String(error));
571
- options?.onError?.(err);
765
+ if (options?.setup) {
766
+ await options.setup(this.sessionManager);
572
767
  }
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 };
768
+ this.chatContainer.clear();
769
+ this.pendingMessagesContainer.clear();
770
+ this.compactionQueuedMessages = [];
771
+ this.streamingComponent = undefined;
772
+ this.streamingMessage = undefined;
773
+ this.pendingTools.clear();
774
+ this.chatContainer.addChild(new Spacer(1));
775
+ this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
776
+ this.ui.requestRender();
777
+ return { cancelled: false };
778
+ },
779
+ fork: async (entryId) => {
780
+ const result = await this.session.fork(entryId);
781
+ if (result.cancelled) {
782
+ return { cancelled: true };
783
+ }
784
+ this.chatContainer.clear();
785
+ this.renderInitialMessages();
786
+ this.editor.setText(result.selectedText);
787
+ this.showStatus("Forked to new session");
788
+ return { cancelled: false };
789
+ },
790
+ navigateTree: async (targetId, options) => {
791
+ const result = await this.session.navigateTree(targetId, {
792
+ summarize: options?.summarize,
793
+ customInstructions: options?.customInstructions,
794
+ replaceInstructions: options?.replaceInstructions,
795
+ label: options?.label,
796
+ });
797
+ if (result.cancelled) {
798
+ return { cancelled: true };
799
+ }
800
+ this.chatContainer.clear();
801
+ this.renderInitialMessages();
802
+ if (result.editorText) {
803
+ this.editor.setText(result.editorText);
804
+ }
805
+ this.showStatus("Navigated to selected point");
806
+ return { cancelled: false };
807
+ },
602
808
  },
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 };
809
+ shutdownHandler: () => {
810
+ this.shutdownRequested = true;
613
811
  },
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 };
812
+ onError: (error) => {
813
+ this.showExtensionError(error.extensionPath, error.error, error.stack);
631
814
  },
632
- }, uiContext);
633
- // Subscribe to extension errors
634
- extensionRunner.onError((error) => {
635
- this.showExtensionError(error.extensionPath, error.error, error.stack);
636
815
  });
637
- // Set up extension-registered shortcuts
638
816
  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
- }
647
- }
648
- // Emit session_start event
649
- await extensionRunner.emit({
650
- type: "session_start",
651
- });
817
+ this.showLoadedResources({ extensionPaths: extensionRunner.getExtensionPaths(), force: false });
652
818
  }
653
819
  /**
654
820
  * Get a registered tool definition by name (for custom rendering).
@@ -754,6 +920,40 @@ export class InteractiveMode {
754
920
  targetMap.set(key, component);
755
921
  this.renderWidgets();
756
922
  }
923
+ clearExtensionWidgets() {
924
+ for (const widget of this.extensionWidgetsAbove.values()) {
925
+ widget.dispose?.();
926
+ }
927
+ for (const widget of this.extensionWidgetsBelow.values()) {
928
+ widget.dispose?.();
929
+ }
930
+ this.extensionWidgetsAbove.clear();
931
+ this.extensionWidgetsBelow.clear();
932
+ this.renderWidgets();
933
+ }
934
+ resetExtensionUI() {
935
+ if (this.extensionSelector) {
936
+ this.hideExtensionSelector();
937
+ }
938
+ if (this.extensionInput) {
939
+ this.hideExtensionInput();
940
+ }
941
+ if (this.extensionEditor) {
942
+ this.hideExtensionEditor();
943
+ }
944
+ this.ui.hideOverlay();
945
+ this.setExtensionFooter(undefined);
946
+ this.setExtensionHeader(undefined);
947
+ this.clearExtensionWidgets();
948
+ this.footerDataProvider.clearExtensionStatuses();
949
+ this.footer.invalidate();
950
+ this.setCustomEditorComponent(undefined);
951
+ this.defaultEditor.onExtensionShortcut = undefined;
952
+ this.updateTerminalTitle();
953
+ if (this.loadingAnimation) {
954
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
955
+ }
956
+ }
757
957
  // Maximum total widget lines to prevent viewport overflow
758
958
  static MAX_WIDGET_LINES = 10;
759
959
  /**
@@ -858,6 +1058,10 @@ export class InteractiveMode {
858
1058
  this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
859
1059
  }
860
1060
  }
1061
+ else {
1062
+ // Queue message for when loadingAnimation is created (handles agent_start race)
1063
+ this.pendingWorkingMessage = message;
1064
+ }
861
1065
  },
862
1066
  setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
863
1067
  setFooter: (factory) => this.setExtensionFooter(factory),
@@ -1022,6 +1226,9 @@ export class InteractiveMode {
1022
1226
  if (newEditor.borderColor !== undefined) {
1023
1227
  newEditor.borderColor = this.defaultEditor.borderColor;
1024
1228
  }
1229
+ if (newEditor.setPaddingX !== undefined) {
1230
+ newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1231
+ }
1025
1232
  // Set autocomplete if supported
1026
1233
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1027
1234
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1033,7 +1240,7 @@ export class InteractiveMode {
1033
1240
  customEditor.onEscape = this.defaultEditor.onEscape;
1034
1241
  customEditor.onCtrlD = this.defaultEditor.onCtrlD;
1035
1242
  customEditor.onPasteImage = this.defaultEditor.onPasteImage;
1036
- customEditor.onExtensionShortcut = this.defaultEditor.onExtensionShortcut;
1243
+ customEditor.onExtensionShortcut = (data) => this.defaultEditor.onExtensionShortcut?.(data);
1037
1244
  // Copy action handlers (clear, suspend, model switching, etc.)
1038
1245
  for (const [action, handler] of this.defaultEditor.actionHandlers) {
1039
1246
  customEditor.actionHandlers.set(action, handler);
@@ -1323,6 +1530,11 @@ export class InteractiveMode {
1323
1530
  await this.handleCompactCommand(customInstructions);
1324
1531
  return;
1325
1532
  }
1533
+ if (text === "/reload") {
1534
+ this.editor.setText("");
1535
+ await this.handleReloadCommand();
1536
+ return;
1537
+ }
1326
1538
  if (text === "/debug") {
1327
1539
  this.handleDebugCommand();
1328
1540
  this.editor.setText("");
@@ -1419,6 +1631,13 @@ export class InteractiveMode {
1419
1631
  this.statusContainer.clear();
1420
1632
  this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
1421
1633
  this.statusContainer.addChild(this.loadingAnimation);
1634
+ // Apply any pending working message queued before loader existed
1635
+ if (this.pendingWorkingMessage !== undefined) {
1636
+ if (this.pendingWorkingMessage) {
1637
+ this.loadingAnimation.setMessage(this.pendingWorkingMessage);
1638
+ }
1639
+ this.pendingWorkingMessage = undefined;
1640
+ }
1422
1641
  this.ui.requestRender();
1423
1642
  break;
1424
1643
  case "message_start":
@@ -1703,8 +1922,23 @@ export class InteractiveMode {
1703
1922
  case "user": {
1704
1923
  const textContent = this.getUserMessageText(message);
1705
1924
  if (textContent) {
1706
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
1707
- this.chatContainer.addChild(userComponent);
1925
+ const skillBlock = parseSkillBlock(textContent);
1926
+ if (skillBlock) {
1927
+ // Render skill block (collapsible)
1928
+ this.chatContainer.addChild(new Spacer(1));
1929
+ const component = new SkillInvocationMessageComponent(skillBlock, this.getMarkdownThemeWithSettings());
1930
+ component.setExpanded(this.toolOutputExpanded);
1931
+ this.chatContainer.addChild(component);
1932
+ // Render user message separately if present
1933
+ if (skillBlock.userMessage) {
1934
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings());
1935
+ this.chatContainer.addChild(userComponent);
1936
+ }
1937
+ }
1938
+ else {
1939
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
1940
+ this.chatContainer.addChild(userComponent);
1941
+ }
1708
1942
  if (options?.populateHistory) {
1709
1943
  this.editor.addToHistory?.(textContent);
1710
1944
  }
@@ -1784,7 +2018,6 @@ export class InteractiveMode {
1784
2018
  this.ui.requestRender();
1785
2019
  }
1786
2020
  renderInitialMessages() {
1787
- this.hasRenderedInitialMessages = true;
1788
2021
  // Get aligned messages and entries from session context
1789
2022
  const context = this.sessionManager.buildSessionContext();
1790
2023
  this.renderSessionContext(context, {
@@ -1871,7 +2104,7 @@ export class InteractiveMode {
1871
2104
  process.kill(0, "SIGTSTP");
1872
2105
  }
1873
2106
  async handleFollowUp() {
1874
- const text = this.editor.getText().trim();
2107
+ const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
1875
2108
  if (!text)
1876
2109
  return;
1877
2110
  // Queue input during compaction (extension commands execute immediately)
@@ -2034,22 +2267,51 @@ export class InteractiveMode {
2034
2267
  ? `Download from: ${theme.fg("accent", "https://github.com/badlogic/pi-mono/releases/latest")}`
2035
2268
  : `Run: ${theme.fg("accent", `${isBunRuntime ? "bun" : "npm"} install -g @mariozechner/pi-coding-agent`)}`;
2036
2269
  const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
2270
+ const changelogUrl = theme.fg("accent", "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md");
2271
+ const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
2037
2272
  this.chatContainer.addChild(new Spacer(1));
2038
2273
  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));
2274
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
2040
2275
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2041
2276
  this.ui.requestRender();
2042
2277
  }
2278
+ /**
2279
+ * Get all queued messages (read-only).
2280
+ * Combines session queue and compaction queue.
2281
+ */
2282
+ getAllQueuedMessages() {
2283
+ return {
2284
+ steering: [
2285
+ ...this.session.getSteeringMessages(),
2286
+ ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
2287
+ ],
2288
+ followUp: [
2289
+ ...this.session.getFollowUpMessages(),
2290
+ ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
2291
+ ],
2292
+ };
2293
+ }
2294
+ /**
2295
+ * Clear all queued messages and return their contents.
2296
+ * Clears both session queue and compaction queue.
2297
+ */
2298
+ clearAllQueues() {
2299
+ const { steering, followUp } = this.session.clearQueue();
2300
+ const compactionSteering = this.compactionQueuedMessages
2301
+ .filter((msg) => msg.mode === "steer")
2302
+ .map((msg) => msg.text);
2303
+ const compactionFollowUp = this.compactionQueuedMessages
2304
+ .filter((msg) => msg.mode === "followUp")
2305
+ .map((msg) => msg.text);
2306
+ this.compactionQueuedMessages = [];
2307
+ return {
2308
+ steering: [...steering, ...compactionSteering],
2309
+ followUp: [...followUp, ...compactionFollowUp],
2310
+ };
2311
+ }
2043
2312
  updatePendingMessagesDisplay() {
2044
2313
  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
- ];
2314
+ const { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();
2053
2315
  if (steeringMessages.length > 0 || followUpMessages.length > 0) {
2054
2316
  this.pendingMessagesContainer.addChild(new Spacer(1));
2055
2317
  for (const message of steeringMessages) {
@@ -2066,7 +2328,7 @@ export class InteractiveMode {
2066
2328
  }
2067
2329
  }
2068
2330
  restoreQueuedMessagesToEditor(options) {
2069
- const { steering, followUp } = this.session.clearQueue();
2331
+ const { steering, followUp } = this.clearAllQueues();
2070
2332
  const allQueued = [...steering, ...followUp];
2071
2333
  if (allQueued.length === 0) {
2072
2334
  this.updatePendingMessagesDisplay();
@@ -2294,6 +2556,9 @@ export class InteractiveMode {
2294
2556
  onEditorPaddingXChange: (padding) => {
2295
2557
  this.settingsManager.setEditorPaddingX(padding);
2296
2558
  this.defaultEditor.setPaddingX(padding);
2559
+ if (this.editor !== this.defaultEditor && this.editor.setPaddingX !== undefined) {
2560
+ this.editor.setPaddingX(padding);
2561
+ }
2297
2562
  },
2298
2563
  onCancel: () => {
2299
2564
  done();
@@ -2359,6 +2624,12 @@ export class InteractiveMode {
2359
2624
  return [];
2360
2625
  }
2361
2626
  }
2627
+ /** Update the footer's available provider count from current model candidates */
2628
+ async updateAvailableProviderCount() {
2629
+ const models = await this.getModelCandidates();
2630
+ const uniqueProviders = new Set(models.map((m) => m.provider));
2631
+ this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
2632
+ }
2362
2633
  showModelSelector(initialSearchInput) {
2363
2634
  this.showSelector((done) => {
2364
2635
  const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
@@ -2632,8 +2903,17 @@ export class InteractiveMode {
2632
2903
  this.ui.requestRender();
2633
2904
  }, () => {
2634
2905
  void this.shutdown();
2635
- }, () => this.ui.requestRender(), this.sessionManager.getSessionFile());
2636
- return { component: selector, focus: selector.getSessionList() };
2906
+ }, () => this.ui.requestRender(), {
2907
+ renameSession: async (sessionFilePath, nextName) => {
2908
+ const next = (nextName ?? "").trim();
2909
+ if (!next)
2910
+ return;
2911
+ const mgr = SessionManager.open(sessionFilePath);
2912
+ mgr.appendSessionInfo(next);
2913
+ },
2914
+ showRenameHint: true,
2915
+ }, this.sessionManager.getSessionFile());
2916
+ return { component: selector, focus: selector };
2637
2917
  });
2638
2918
  }
2639
2919
  async handleResumeSession(sessionPath) {
@@ -2678,6 +2958,7 @@ export class InteractiveMode {
2678
2958
  try {
2679
2959
  this.session.modelRegistry.authStorage.logout(providerId);
2680
2960
  this.session.modelRegistry.refresh();
2961
+ await this.updateAvailableProviderCount();
2681
2962
  this.showStatus(`Logged out of ${providerName}`);
2682
2963
  }
2683
2964
  catch (error) {
@@ -2695,7 +2976,7 @@ export class InteractiveMode {
2695
2976
  const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
2696
2977
  const providerName = providerInfo?.name || providerId;
2697
2978
  // Providers that use callback servers (can paste redirect URL)
2698
- const usesCallbackServer = providerId === "openai-codex" || providerId === "google-gemini-cli" || providerId === "google-antigravity";
2979
+ const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
2699
2980
  // Create login dialog component
2700
2981
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
2701
2982
  // Completion handled below
@@ -2758,6 +3039,7 @@ export class InteractiveMode {
2758
3039
  // Success
2759
3040
  restoreEditor();
2760
3041
  this.session.modelRegistry.refresh();
3042
+ await this.updateAvailableProviderCount();
2761
3043
  this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
2762
3044
  }
2763
3045
  catch (error) {
@@ -2771,6 +3053,53 @@ export class InteractiveMode {
2771
3053
  // =========================================================================
2772
3054
  // Command handlers
2773
3055
  // =========================================================================
3056
+ async handleReloadCommand() {
3057
+ if (this.session.isStreaming) {
3058
+ this.showWarning("Wait for the current response to finish before reloading.");
3059
+ return;
3060
+ }
3061
+ if (this.session.isCompacting) {
3062
+ this.showWarning("Wait for compaction to finish before reloading.");
3063
+ return;
3064
+ }
3065
+ this.resetExtensionUI();
3066
+ const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", {
3067
+ cancellable: false,
3068
+ });
3069
+ const previousEditor = this.editor;
3070
+ this.editorContainer.clear();
3071
+ this.editorContainer.addChild(loader);
3072
+ this.ui.setFocus(loader);
3073
+ this.ui.requestRender();
3074
+ const dismissLoader = (editor) => {
3075
+ loader.dispose();
3076
+ this.editorContainer.clear();
3077
+ this.editorContainer.addChild(editor);
3078
+ this.ui.setFocus(editor);
3079
+ this.ui.requestRender();
3080
+ };
3081
+ try {
3082
+ await this.session.reload();
3083
+ setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3084
+ this.rebuildAutocomplete();
3085
+ const runner = this.session.extensionRunner;
3086
+ if (runner) {
3087
+ this.setupExtensionShortcuts(runner);
3088
+ }
3089
+ this.rebuildChatFromMessages();
3090
+ dismissLoader(this.editor);
3091
+ this.showLoadedResources({ extensionPaths: runner?.getExtensionPaths() ?? [], force: true });
3092
+ const modelsJsonError = this.session.modelRegistry.getError();
3093
+ if (modelsJsonError) {
3094
+ this.showError(`models.json error: ${modelsJsonError}`);
3095
+ }
3096
+ this.showStatus("Reloaded extensions, skills, prompts, themes");
3097
+ }
3098
+ catch (error) {
3099
+ dismissLoader(previousEditor);
3100
+ this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
3101
+ }
3102
+ }
2774
3103
  async handleExportCommand(text) {
2775
3104
  const parts = text.split(/\s+/);
2776
3105
  const outputPath = parts.length > 1 ? parts[1] : undefined;
@@ -2983,6 +3312,8 @@ export class InteractiveMode {
2983
3312
  const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
2984
3313
  const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
2985
3314
  const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
3315
+ const pageUp = this.getEditorKeyDisplay("pageUp");
3316
+ const pageDown = this.getEditorKeyDisplay("pageDown");
2986
3317
  // Editing keybindings
2987
3318
  const submit = this.getEditorKeyDisplay("submit");
2988
3319
  const newLine = this.getEditorKeyDisplay("newLine");
@@ -3001,6 +3332,7 @@ export class InteractiveMode {
3001
3332
  const suspend = this.getAppKeyDisplay("suspend");
3002
3333
  const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
3003
3334
  const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
3335
+ const selectModel = this.getAppKeyDisplay("selectModel");
3004
3336
  const expandTools = this.getAppKeyDisplay("expandTools");
3005
3337
  const toggleThinking = this.getAppKeyDisplay("toggleThinking");
3006
3338
  const externalEditor = this.getAppKeyDisplay("externalEditor");
@@ -3014,6 +3346,7 @@ export class InteractiveMode {
3014
3346
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
3015
3347
  | \`${cursorLineStart}\` | Start of line |
3016
3348
  | \`${cursorLineEnd}\` | End of line |
3349
+ | \`${pageUp}\` / \`${pageDown}\` | Scroll by page |
3017
3350
 
3018
3351
  **Editing**
3019
3352
  | Key | Action |
@@ -3038,6 +3371,7 @@ export class InteractiveMode {
3038
3371
  | \`${suspend}\` | Suspend to background |
3039
3372
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
3040
3373
  | \`${cycleModelForward}\` | Cycle models |
3374
+ | \`${selectModel}\` | Open model selector |
3041
3375
  | \`${expandTools}\` | Toggle tool output expansion |
3042
3376
  | \`${toggleThinking}\` | Toggle thinking block visibility |
3043
3377
  | \`${externalEditor}\` | Edit message in external editor |
@@ -3094,11 +3428,12 @@ export class InteractiveMode {
3094
3428
  }
3095
3429
  handleDebugCommand() {
3096
3430
  const width = this.ui.terminal.columns;
3431
+ const height = this.ui.terminal.rows;
3097
3432
  const allLines = this.ui.render(width);
3098
3433
  const debugLogPath = getDebugLogPath();
3099
3434
  const debugData = [
3100
3435
  `Debug output at ${new Date().toISOString()}`,
3101
- `Terminal width: ${width}`,
3436
+ `Terminal: ${width}x${height}`,
3102
3437
  `Total lines: ${allLines.length}`,
3103
3438
  "",
3104
3439
  "=== All rendered lines with visible widths ===",