@mariozechner/pi-coding-agent 0.49.2 → 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 (237) hide show
  1. package/CHANGELOG.md +126 -1
  2. package/README.md +310 -1229
  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 -22
  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/config.d.ts +2 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +6 -0
  17. package/dist/config.js.map +1 -1
  18. package/dist/core/agent-session.d.ts +53 -34
  19. package/dist/core/agent-session.d.ts.map +1 -1
  20. package/dist/core/agent-session.js +262 -67
  21. package/dist/core/agent-session.js.map +1 -1
  22. package/dist/core/auth-storage.d.ts +8 -18
  23. package/dist/core/auth-storage.d.ts.map +1 -1
  24. package/dist/core/auth-storage.js +39 -55
  25. package/dist/core/auth-storage.js.map +1 -1
  26. package/dist/core/bash-executor.d.ts.map +1 -1
  27. package/dist/core/bash-executor.js +2 -1
  28. package/dist/core/bash-executor.js.map +1 -1
  29. package/dist/core/diagnostics.d.ts +15 -0
  30. package/dist/core/diagnostics.d.ts.map +1 -0
  31. package/dist/core/diagnostics.js +2 -0
  32. package/dist/core/diagnostics.js.map +1 -0
  33. package/dist/core/export-html/template.css +9 -0
  34. package/dist/core/export-html/template.js +6 -4
  35. package/dist/core/extensions/index.d.ts +1 -1
  36. package/dist/core/extensions/index.d.ts.map +1 -1
  37. package/dist/core/extensions/index.js.map +1 -1
  38. package/dist/core/extensions/loader.d.ts +1 -1
  39. package/dist/core/extensions/loader.d.ts.map +1 -1
  40. package/dist/core/extensions/loader.js +10 -1
  41. package/dist/core/extensions/loader.js.map +1 -1
  42. package/dist/core/extensions/runner.d.ts +9 -3
  43. package/dist/core/extensions/runner.d.ts.map +1 -1
  44. package/dist/core/extensions/runner.js +39 -12
  45. package/dist/core/extensions/runner.js.map +1 -1
  46. package/dist/core/extensions/types.d.ts +112 -1
  47. package/dist/core/extensions/types.d.ts.map +1 -1
  48. package/dist/core/extensions/types.js.map +1 -1
  49. package/dist/core/footer-data-provider.d.ts +9 -2
  50. package/dist/core/footer-data-provider.d.ts.map +1 -1
  51. package/dist/core/footer-data-provider.js +13 -0
  52. package/dist/core/footer-data-provider.js.map +1 -1
  53. package/dist/core/model-registry.d.ts +42 -2
  54. package/dist/core/model-registry.d.ts.map +1 -1
  55. package/dist/core/model-registry.js +154 -44
  56. package/dist/core/model-registry.js.map +1 -1
  57. package/dist/core/model-resolver.d.ts.map +1 -1
  58. package/dist/core/model-resolver.js +3 -2
  59. package/dist/core/model-resolver.js.map +1 -1
  60. package/dist/core/package-manager.d.ts +129 -0
  61. package/dist/core/package-manager.d.ts.map +1 -0
  62. package/dist/core/package-manager.js +1148 -0
  63. package/dist/core/package-manager.js.map +1 -0
  64. package/dist/core/prompt-templates.d.ts +6 -0
  65. package/dist/core/prompt-templates.d.ts.map +1 -1
  66. package/dist/core/prompt-templates.js +114 -54
  67. package/dist/core/prompt-templates.js.map +1 -1
  68. package/dist/core/resource-loader.d.ts +160 -0
  69. package/dist/core/resource-loader.d.ts.map +1 -0
  70. package/dist/core/resource-loader.js +604 -0
  71. package/dist/core/resource-loader.js.map +1 -0
  72. package/dist/core/sdk.d.ts +14 -105
  73. package/dist/core/sdk.d.ts.map +1 -1
  74. package/dist/core/sdk.js +52 -304
  75. package/dist/core/sdk.js.map +1 -1
  76. package/dist/core/session-manager.d.ts.map +1 -1
  77. package/dist/core/session-manager.js +45 -1
  78. package/dist/core/session-manager.js.map +1 -1
  79. package/dist/core/settings-manager.d.ts +39 -16
  80. package/dist/core/settings-manager.d.ts.map +1 -1
  81. package/dist/core/settings-manager.js +107 -25
  82. package/dist/core/settings-manager.js.map +1 -1
  83. package/dist/core/skills.d.ts +18 -10
  84. package/dist/core/skills.d.ts.map +1 -1
  85. package/dist/core/skills.js +126 -93
  86. package/dist/core/skills.js.map +1 -1
  87. package/dist/core/system-prompt.d.ts +3 -27
  88. package/dist/core/system-prompt.d.ts.map +1 -1
  89. package/dist/core/system-prompt.js +16 -103
  90. package/dist/core/system-prompt.js.map +1 -1
  91. package/dist/core/tools/bash.d.ts.map +1 -1
  92. package/dist/core/tools/bash.js +2 -1
  93. package/dist/core/tools/bash.js.map +1 -1
  94. package/dist/core/tools/read.d.ts.map +1 -1
  95. package/dist/core/tools/read.js +4 -4
  96. package/dist/core/tools/read.js.map +1 -1
  97. package/dist/index.d.ts +12 -7
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +8 -6
  100. package/dist/index.js.map +1 -1
  101. package/dist/main.d.ts.map +1 -1
  102. package/dist/main.js +209 -97
  103. package/dist/main.js.map +1 -1
  104. package/dist/modes/interactive/components/assistant-message.d.ts +3 -2
  105. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  106. package/dist/modes/interactive/components/assistant-message.js +5 -3
  107. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  108. package/dist/modes/interactive/components/bordered-loader.d.ts +5 -1
  109. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/bordered-loader.js +29 -9
  111. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  112. package/dist/modes/interactive/components/branch-summary-message.d.ts +3 -2
  113. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  114. package/dist/modes/interactive/components/branch-summary-message.js +4 -2
  115. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  116. package/dist/modes/interactive/components/compaction-summary-message.d.ts +3 -2
  117. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  118. package/dist/modes/interactive/components/compaction-summary-message.js +4 -2
  119. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  120. package/dist/modes/interactive/components/config-selector.d.ts +71 -0
  121. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -0
  122. package/dist/modes/interactive/components/config-selector.js +468 -0
  123. package/dist/modes/interactive/components/config-selector.js.map +1 -0
  124. package/dist/modes/interactive/components/custom-message.d.ts +3 -2
  125. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/custom-message.js +4 -2
  127. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  128. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/footer.js +9 -0
  130. package/dist/modes/interactive/components/footer.js.map +1 -1
  131. package/dist/modes/interactive/components/index.d.ts +1 -0
  132. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  133. package/dist/modes/interactive/components/index.js +1 -0
  134. package/dist/modes/interactive/components/index.js.map +1 -1
  135. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  136. package/dist/modes/interactive/components/oauth-selector.js +3 -4
  137. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  138. package/dist/modes/interactive/components/session-selector.d.ts +18 -1
  139. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  140. package/dist/modes/interactive/components/session-selector.js +195 -87
  141. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  142. package/dist/modes/interactive/components/skill-invocation-message.d.ts +17 -0
  143. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -0
  144. package/dist/modes/interactive/components/skill-invocation-message.js +47 -0
  145. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -0
  146. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  147. package/dist/modes/interactive/components/tool-execution.js +12 -5
  148. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  149. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/tree-selector.js +2 -2
  151. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  152. package/dist/modes/interactive/components/user-message.d.ts +2 -2
  153. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/user-message.js +2 -2
  155. package/dist/modes/interactive/components/user-message.js.map +1 -1
  156. package/dist/modes/interactive/interactive-mode.d.ts +47 -2
  157. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  158. package/dist/modes/interactive/interactive-mode.js +566 -211
  159. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  160. package/dist/modes/interactive/theme/dark.json +1 -1
  161. package/dist/modes/interactive/theme/light.json +1 -1
  162. package/dist/modes/interactive/theme/theme-schema.json +8 -1
  163. package/dist/modes/interactive/theme/theme.d.ts +8 -1
  164. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  165. package/dist/modes/interactive/theme/theme.js +79 -28
  166. package/dist/modes/interactive/theme/theme.js.map +1 -1
  167. package/dist/modes/print-mode.d.ts.map +1 -1
  168. package/dist/modes/print-mode.js +25 -89
  169. package/dist/modes/print-mode.js.map +1 -1
  170. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  171. package/dist/modes/rpc/rpc-mode.js +32 -92
  172. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  173. package/dist/utils/git.d.ts +2 -0
  174. package/dist/utils/git.d.ts.map +1 -0
  175. package/dist/utils/git.js +6 -0
  176. package/dist/utils/git.js.map +1 -0
  177. package/dist/utils/shell.d.ts +1 -0
  178. package/dist/utils/shell.d.ts.map +1 -1
  179. package/dist/utils/shell.js +16 -2
  180. package/dist/utils/shell.js.map +1 -1
  181. package/dist/utils/sleep.d.ts +5 -0
  182. package/dist/utils/sleep.d.ts.map +1 -0
  183. package/dist/utils/sleep.js +17 -0
  184. package/dist/utils/sleep.js.map +1 -0
  185. package/docs/compaction.md +23 -21
  186. package/docs/custom-provider.md +538 -0
  187. package/docs/development.md +69 -0
  188. package/docs/extensions.md +180 -118
  189. package/docs/images/doom-extension.png +0 -0
  190. package/docs/images/interactive-mode.png +0 -0
  191. package/docs/images/tree-view.png +0 -0
  192. package/docs/json.md +79 -0
  193. package/docs/keybindings.md +162 -0
  194. package/docs/models.md +193 -0
  195. package/docs/packages.md +163 -0
  196. package/docs/prompt-templates.md +67 -0
  197. package/docs/providers.md +147 -0
  198. package/docs/sdk.md +111 -178
  199. package/docs/session.md +167 -16
  200. package/docs/settings.md +216 -0
  201. package/docs/shell-aliases.md +13 -0
  202. package/docs/skills.md +111 -202
  203. package/docs/terminal-setup.md +65 -0
  204. package/docs/themes.md +295 -0
  205. package/docs/tui.md +36 -5
  206. package/docs/windows.md +17 -0
  207. package/examples/README.md +1 -0
  208. package/examples/extensions/README.md +24 -2
  209. package/examples/extensions/antigravity-image-gen.ts +413 -0
  210. package/examples/extensions/bookmark.ts +50 -0
  211. package/examples/extensions/custom-provider-anthropic/index.ts +604 -0
  212. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  213. package/examples/extensions/custom-provider-anthropic/package.json +19 -0
  214. package/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
  215. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  216. package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
  217. package/examples/extensions/doom-overlay/doom/build.sh +1 -1
  218. package/examples/extensions/event-bus.ts +43 -0
  219. package/examples/extensions/inline-bash.ts +94 -0
  220. package/examples/extensions/message-renderer.ts +59 -0
  221. package/examples/extensions/session-name.ts +27 -0
  222. package/examples/extensions/space-invaders.ts +560 -0
  223. package/examples/extensions/with-deps/package-lock.json +2 -2
  224. package/examples/extensions/with-deps/package.json +1 -1
  225. package/examples/sdk/02-custom-model.ts +3 -3
  226. package/examples/sdk/03-custom-prompt.ts +20 -9
  227. package/examples/sdk/04-skills.ts +26 -27
  228. package/examples/sdk/06-extensions.ts +15 -6
  229. package/examples/sdk/07-context-files.ts +22 -18
  230. package/examples/sdk/08-prompt-templates.ts +19 -14
  231. package/examples/sdk/09-api-keys-and-oauth.ts +5 -12
  232. package/examples/sdk/10-settings.ts +3 -3
  233. package/examples/sdk/12-full-control.ts +16 -7
  234. package/examples/sdk/README.md +24 -30
  235. package/package.json +4 -4
  236. package/docs/theme.md +0 -617
  237. package/examples/extensions/chalk-logger.ts +0 -26
@@ -9,13 +9,13 @@ import * as path from "node:path";
9
9
  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
- import { APP_NAME, getAuthPath, getDebugLogPath, isBunBinary, isBunRuntime, VERSION } from "../../config.js";
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"),
@@ -280,7 +283,7 @@ export class InteractiveMode {
280
283
  else {
281
284
  this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
282
285
  this.ui.addChild(new Spacer(1));
283
- this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
286
+ this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
284
287
  this.ui.addChild(new Spacer(1));
285
288
  }
286
289
  this.ui.addChild(new DynamicBorder());
@@ -313,8 +316,7 @@ export class InteractiveMode {
313
316
  this.ui.start();
314
317
  this.isInitialized = true;
315
318
  // Set terminal title
316
- const cwdBasename = path.basename(process.cwd());
317
- this.ui.terminal.setTitle(`pi - ${cwdBasename}`);
319
+ this.updateTerminalTitle();
318
320
  // Initialize extensions with TUI-based UI context
319
321
  await this.initExtensions();
320
322
  // Subscribe to agent events
@@ -329,6 +331,21 @@ export class InteractiveMode {
329
331
  this.footerDataProvider.onBranchChange(() => {
330
332
  this.ui.requestRender();
331
333
  });
334
+ // Initialize available provider count for footer display
335
+ await this.updateAvailableProviderCount();
336
+ }
337
+ /**
338
+ * Update terminal title with session name and cwd.
339
+ */
340
+ updateTerminalTitle() {
341
+ const cwdBasename = path.basename(process.cwd());
342
+ const sessionName = this.sessionManager.getSessionName();
343
+ if (sessionName) {
344
+ this.ui.terminal.setTitle(`π - ${sessionName} - ${cwdBasename}`);
345
+ }
346
+ else {
347
+ this.ui.terminal.setTitle(`π - ${cwdBasename}`);
348
+ }
332
349
  }
333
350
  /**
334
351
  * Run the interactive mode. This is the main entry point.
@@ -435,201 +452,369 @@ export class InteractiveMode {
435
452
  }
436
453
  return undefined;
437
454
  }
455
+ getMarkdownThemeWithSettings() {
456
+ return {
457
+ ...getMarkdownTheme(),
458
+ codeBlockIndent: this.settingsManager.getCodeBlockIndent(),
459
+ };
460
+ }
438
461
  // =========================================================================
439
462
  // Extension System
440
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
+ }
441
473
  /**
442
- * Initialize the extension system with TUI-based UI context.
474
+ * Get a short path relative to the package root for display.
443
475
  */
444
- async initExtensions() {
445
- // Show discovery info unless silenced
446
- if (!this.settingsManager.getQuietStartup()) {
447
- // Show loaded project context files
448
- const contextFiles = loadProjectContextFiles();
449
- if (contextFiles.length > 0) {
450
- const contextList = contextFiles.map((f) => theme.fg("dim", ` ${f.path}`)).join("\n");
451
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded context:\n") + contextList, 0, 0));
452
- 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);
453
537
  }
454
- // Show loaded skills (already discovered by SDK)
455
- const skills = this.session.skills;
456
- if (skills.length > 0) {
457
- const skillList = skills.map((s) => theme.fg("dim", ` ${s.filePath}`)).join("\n");
458
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded skills:\n") + skillList, 0, 0));
459
- this.chatContainer.addChild(new Spacer(1));
538
+ else {
539
+ group.paths.push(p);
460
540
  }
461
- // Show skill warnings if any
462
- const skillWarnings = this.session.skillWarnings;
463
- if (skillWarnings.length > 0) {
464
- const warningList = skillWarnings
465
- .map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`))
466
- .join("\n");
467
- this.chatContainer.addChild(new Text(theme.fg("warning", "Skill warnings:\n") + warningList, 0, 0));
468
- 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)}`));
469
551
  }
470
- // Show loaded prompt templates
471
- const templates = this.session.promptTemplates;
472
- if (templates.length > 0) {
473
- const templateList = templates.map((t) => theme.fg("dim", ` /${t.name} ${t.source}`)).join("\n");
474
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded prompt templates:\n") + templateList, 0, 0));
475
- 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
+ }
476
559
  }
477
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() {
478
744
  const extensionRunner = this.session.extensionRunner;
479
745
  if (!extensionRunner) {
480
- return; // No extensions loaded
746
+ this.showLoadedResources({ extensionPaths: [], force: false });
747
+ return;
481
748
  }
482
749
  // Create extension UI context
483
750
  const uiContext = this.createExtensionUIContext();
484
- extensionRunner.initialize(
485
- // ExtensionActions - for pi.* API
486
- {
487
- sendMessage: (message, options) => {
488
- const wasStreaming = this.session.isStreaming;
489
- this.session
490
- .sendCustomMessage(message, options)
491
- .then(() => {
492
- // Don't rebuild if initial render hasn't happened yet
493
- // (renderInitialMessages will handle it)
494
- if (!wasStreaming && message.display && this.hasRenderedInitialMessages) {
495
- 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;
496
759
  }
497
- })
498
- .catch((err) => {
499
- this.showError(`Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`);
500
- });
501
- },
502
- sendUserMessage: (content, options) => {
503
- this.session.sendUserMessage(content, options).catch((err) => {
504
- this.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
505
- });
506
- },
507
- appendEntry: (customType, data) => {
508
- this.sessionManager.appendCustomEntry(customType, data);
509
- },
510
- setSessionName: (name) => {
511
- this.sessionManager.appendSessionInfo(name);
512
- },
513
- getSessionName: () => {
514
- return this.sessionManager.getSessionName();
515
- },
516
- setLabel: (entryId, label) => {
517
- this.sessionManager.appendLabelChange(entryId, label);
518
- },
519
- getActiveTools: () => this.session.getActiveToolNames(),
520
- getAllTools: () => this.session.getAllTools(),
521
- setActiveTools: (toolNames) => this.session.setActiveToolsByName(toolNames),
522
- setModel: async (model) => {
523
- const key = await this.session.modelRegistry.getApiKey(model);
524
- if (!key)
525
- return false;
526
- await this.session.setModel(model);
527
- return true;
528
- },
529
- getThinkingLevel: () => this.session.thinkingLevel,
530
- setThinkingLevel: (level) => this.session.setThinkingLevel(level),
531
- },
532
- // ExtensionContextActions - for ctx.* in event handlers
533
- {
534
- getModel: () => this.session.model,
535
- isIdle: () => !this.session.isStreaming,
536
- abort: () => this.session.abort(),
537
- hasPendingMessages: () => this.session.pendingMessageCount > 0,
538
- shutdown: () => {
539
- this.shutdownRequested = true;
540
- },
541
- getContextUsage: () => this.session.getContextUsage(),
542
- compact: (options) => {
543
- void (async () => {
544
- try {
545
- const result = await this.executeCompaction(options?.customInstructions, false);
546
- if (result) {
547
- options?.onComplete?.(result);
548
- }
760
+ this.statusContainer.clear();
761
+ const success = await this.session.newSession({ parentSession: options?.parentSession });
762
+ if (!success) {
763
+ return { cancelled: true };
549
764
  }
550
- catch (error) {
551
- const err = error instanceof Error ? error : new Error(String(error));
552
- options?.onError?.(err);
765
+ if (options?.setup) {
766
+ await options.setup(this.sessionManager);
553
767
  }
554
- })();
555
- },
556
- },
557
- // ExtensionCommandContextActions - for ctx.* in command handlers
558
- {
559
- waitForIdle: () => this.session.agent.waitForIdle(),
560
- newSession: async (options) => {
561
- if (this.loadingAnimation) {
562
- this.loadingAnimation.stop();
563
- this.loadingAnimation = undefined;
564
- }
565
- this.statusContainer.clear();
566
- const success = await this.session.newSession({ parentSession: options?.parentSession });
567
- if (!success) {
568
- return { cancelled: true };
569
- }
570
- if (options?.setup) {
571
- await options.setup(this.sessionManager);
572
- }
573
- this.chatContainer.clear();
574
- this.pendingMessagesContainer.clear();
575
- this.compactionQueuedMessages = [];
576
- this.streamingComponent = undefined;
577
- this.streamingMessage = undefined;
578
- this.pendingTools.clear();
579
- this.chatContainer.addChild(new Spacer(1));
580
- this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
581
- this.ui.requestRender();
582
- 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
+ },
583
808
  },
584
- fork: async (entryId) => {
585
- const result = await this.session.fork(entryId);
586
- if (result.cancelled) {
587
- return { cancelled: true };
588
- }
589
- this.chatContainer.clear();
590
- this.renderInitialMessages();
591
- this.editor.setText(result.selectedText);
592
- this.showStatus("Forked to new session");
593
- return { cancelled: false };
809
+ shutdownHandler: () => {
810
+ this.shutdownRequested = true;
594
811
  },
595
- navigateTree: async (targetId, options) => {
596
- const result = await this.session.navigateTree(targetId, {
597
- summarize: options?.summarize,
598
- customInstructions: options?.customInstructions,
599
- replaceInstructions: options?.replaceInstructions,
600
- label: options?.label,
601
- });
602
- if (result.cancelled) {
603
- return { cancelled: true };
604
- }
605
- this.chatContainer.clear();
606
- this.renderInitialMessages();
607
- if (result.editorText) {
608
- this.editor.setText(result.editorText);
609
- }
610
- this.showStatus("Navigated to selected point");
611
- return { cancelled: false };
812
+ onError: (error) => {
813
+ this.showExtensionError(error.extensionPath, error.error, error.stack);
612
814
  },
613
- }, uiContext);
614
- // Subscribe to extension errors
615
- extensionRunner.onError((error) => {
616
- this.showExtensionError(error.extensionPath, error.error, error.stack);
617
815
  });
618
- // Set up extension-registered shortcuts
619
816
  this.setupExtensionShortcuts(extensionRunner);
620
- // Show loaded extensions (unless silenced)
621
- if (!this.settingsManager.getQuietStartup()) {
622
- const extensionPaths = extensionRunner.getExtensionPaths();
623
- if (extensionPaths.length > 0) {
624
- const extList = extensionPaths.map((p) => theme.fg("dim", ` ${p}`)).join("\n");
625
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded extensions:\n") + extList, 0, 0));
626
- this.chatContainer.addChild(new Spacer(1));
627
- }
628
- }
629
- // Emit session_start event
630
- await extensionRunner.emit({
631
- type: "session_start",
632
- });
817
+ this.showLoadedResources({ extensionPaths: extensionRunner.getExtensionPaths(), force: false });
633
818
  }
634
819
  /**
635
820
  * Get a registered tool definition by name (for custom rendering).
@@ -735,6 +920,40 @@ export class InteractiveMode {
735
920
  targetMap.set(key, component);
736
921
  this.renderWidgets();
737
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
+ }
738
957
  // Maximum total widget lines to prevent viewport overflow
739
958
  static MAX_WIDGET_LINES = 10;
740
959
  /**
@@ -839,6 +1058,10 @@ export class InteractiveMode {
839
1058
  this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
840
1059
  }
841
1060
  }
1061
+ else {
1062
+ // Queue message for when loadingAnimation is created (handles agent_start race)
1063
+ this.pendingWorkingMessage = message;
1064
+ }
842
1065
  },
843
1066
  setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
844
1067
  setFooter: (factory) => this.setExtensionFooter(factory),
@@ -1003,6 +1226,9 @@ export class InteractiveMode {
1003
1226
  if (newEditor.borderColor !== undefined) {
1004
1227
  newEditor.borderColor = this.defaultEditor.borderColor;
1005
1228
  }
1229
+ if (newEditor.setPaddingX !== undefined) {
1230
+ newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1231
+ }
1006
1232
  // Set autocomplete if supported
1007
1233
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1008
1234
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1014,7 +1240,7 @@ export class InteractiveMode {
1014
1240
  customEditor.onEscape = this.defaultEditor.onEscape;
1015
1241
  customEditor.onCtrlD = this.defaultEditor.onCtrlD;
1016
1242
  customEditor.onPasteImage = this.defaultEditor.onPasteImage;
1017
- customEditor.onExtensionShortcut = this.defaultEditor.onExtensionShortcut;
1243
+ customEditor.onExtensionShortcut = (data) => this.defaultEditor.onExtensionShortcut?.(data);
1018
1244
  // Copy action handlers (clear, suspend, model switching, etc.)
1019
1245
  for (const [action, handler] of this.defaultEditor.actionHandlers) {
1020
1246
  customEditor.actionHandlers.set(action, handler);
@@ -1304,6 +1530,11 @@ export class InteractiveMode {
1304
1530
  await this.handleCompactCommand(customInstructions);
1305
1531
  return;
1306
1532
  }
1533
+ if (text === "/reload") {
1534
+ this.editor.setText("");
1535
+ await this.handleReloadCommand();
1536
+ return;
1537
+ }
1307
1538
  if (text === "/debug") {
1308
1539
  this.handleDebugCommand();
1309
1540
  this.editor.setText("");
@@ -1400,6 +1631,13 @@ export class InteractiveMode {
1400
1631
  this.statusContainer.clear();
1401
1632
  this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
1402
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
+ }
1403
1641
  this.ui.requestRender();
1404
1642
  break;
1405
1643
  case "message_start":
@@ -1413,7 +1651,7 @@ export class InteractiveMode {
1413
1651
  this.ui.requestRender();
1414
1652
  }
1415
1653
  else if (event.message.role === "assistant") {
1416
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
1654
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
1417
1655
  this.streamingMessage = event.message;
1418
1656
  this.chatContainer.addChild(this.streamingComponent);
1419
1657
  this.streamingComponent.updateContent(this.streamingMessage);
@@ -1663,20 +1901,20 @@ export class InteractiveMode {
1663
1901
  case "custom": {
1664
1902
  if (message.display) {
1665
1903
  const renderer = this.session.extensionRunner?.getMessageRenderer(message.customType);
1666
- this.chatContainer.addChild(new CustomMessageComponent(message, renderer));
1904
+ this.chatContainer.addChild(new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings()));
1667
1905
  }
1668
1906
  break;
1669
1907
  }
1670
1908
  case "compactionSummary": {
1671
1909
  this.chatContainer.addChild(new Spacer(1));
1672
- const component = new CompactionSummaryMessageComponent(message);
1910
+ const component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
1673
1911
  component.setExpanded(this.toolOutputExpanded);
1674
1912
  this.chatContainer.addChild(component);
1675
1913
  break;
1676
1914
  }
1677
1915
  case "branchSummary": {
1678
1916
  this.chatContainer.addChild(new Spacer(1));
1679
- const component = new BranchSummaryMessageComponent(message);
1917
+ const component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
1680
1918
  component.setExpanded(this.toolOutputExpanded);
1681
1919
  this.chatContainer.addChild(component);
1682
1920
  break;
@@ -1684,8 +1922,23 @@ export class InteractiveMode {
1684
1922
  case "user": {
1685
1923
  const textContent = this.getUserMessageText(message);
1686
1924
  if (textContent) {
1687
- const userComponent = new UserMessageComponent(textContent);
1688
- 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
+ }
1689
1942
  if (options?.populateHistory) {
1690
1943
  this.editor.addToHistory?.(textContent);
1691
1944
  }
@@ -1693,7 +1946,7 @@ export class InteractiveMode {
1693
1946
  break;
1694
1947
  }
1695
1948
  case "assistant": {
1696
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
1949
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
1697
1950
  this.chatContainer.addChild(assistantComponent);
1698
1951
  break;
1699
1952
  }
@@ -1765,7 +2018,6 @@ export class InteractiveMode {
1765
2018
  this.ui.requestRender();
1766
2019
  }
1767
2020
  renderInitialMessages() {
1768
- this.hasRenderedInitialMessages = true;
1769
2021
  // Get aligned messages and entries from session context
1770
2022
  const context = this.sessionManager.buildSessionContext();
1771
2023
  this.renderSessionContext(context, {
@@ -1852,7 +2104,7 @@ export class InteractiveMode {
1852
2104
  process.kill(0, "SIGTSTP");
1853
2105
  }
1854
2106
  async handleFollowUp() {
1855
- const text = this.editor.getText().trim();
2107
+ const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
1856
2108
  if (!text)
1857
2109
  return;
1858
2110
  // Queue input during compaction (extension commands execute immediately)
@@ -2015,22 +2267,51 @@ export class InteractiveMode {
2015
2267
  ? `Download from: ${theme.fg("accent", "https://github.com/badlogic/pi-mono/releases/latest")}`
2016
2268
  : `Run: ${theme.fg("accent", `${isBunRuntime ? "bun" : "npm"} install -g @mariozechner/pi-coding-agent`)}`;
2017
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;
2018
2272
  this.chatContainer.addChild(new Spacer(1));
2019
2273
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2020
- 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));
2021
2275
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2022
2276
  this.ui.requestRender();
2023
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
+ }
2024
2312
  updatePendingMessagesDisplay() {
2025
2313
  this.pendingMessagesContainer.clear();
2026
- const steeringMessages = [
2027
- ...this.session.getSteeringMessages(),
2028
- ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
2029
- ];
2030
- const followUpMessages = [
2031
- ...this.session.getFollowUpMessages(),
2032
- ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
2033
- ];
2314
+ const { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();
2034
2315
  if (steeringMessages.length > 0 || followUpMessages.length > 0) {
2035
2316
  this.pendingMessagesContainer.addChild(new Spacer(1));
2036
2317
  for (const message of steeringMessages) {
@@ -2047,7 +2328,7 @@ export class InteractiveMode {
2047
2328
  }
2048
2329
  }
2049
2330
  restoreQueuedMessagesToEditor(options) {
2050
- const { steering, followUp } = this.session.clearQueue();
2331
+ const { steering, followUp } = this.clearAllQueues();
2051
2332
  const allQueued = [...steering, ...followUp];
2052
2333
  if (allQueued.length === 0) {
2053
2334
  this.updatePendingMessagesDisplay();
@@ -2275,6 +2556,9 @@ export class InteractiveMode {
2275
2556
  onEditorPaddingXChange: (padding) => {
2276
2557
  this.settingsManager.setEditorPaddingX(padding);
2277
2558
  this.defaultEditor.setPaddingX(padding);
2559
+ if (this.editor !== this.defaultEditor && this.editor.setPaddingX !== undefined) {
2560
+ this.editor.setPaddingX(padding);
2561
+ }
2278
2562
  },
2279
2563
  onCancel: () => {
2280
2564
  done();
@@ -2340,6 +2624,12 @@ export class InteractiveMode {
2340
2624
  return [];
2341
2625
  }
2342
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
+ }
2343
2633
  showModelSelector(initialSearchInput) {
2344
2634
  this.showSelector((done) => {
2345
2635
  const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
@@ -2613,8 +2903,17 @@ export class InteractiveMode {
2613
2903
  this.ui.requestRender();
2614
2904
  }, () => {
2615
2905
  void this.shutdown();
2616
- }, () => this.ui.requestRender(), this.sessionManager.getSessionFile());
2617
- 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 };
2618
2917
  });
2619
2918
  }
2620
2919
  async handleResumeSession(sessionPath) {
@@ -2659,6 +2958,7 @@ export class InteractiveMode {
2659
2958
  try {
2660
2959
  this.session.modelRegistry.authStorage.logout(providerId);
2661
2960
  this.session.modelRegistry.refresh();
2961
+ await this.updateAvailableProviderCount();
2662
2962
  this.showStatus(`Logged out of ${providerName}`);
2663
2963
  }
2664
2964
  catch (error) {
@@ -2676,7 +2976,7 @@ export class InteractiveMode {
2676
2976
  const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
2677
2977
  const providerName = providerInfo?.name || providerId;
2678
2978
  // Providers that use callback servers (can paste redirect URL)
2679
- const usesCallbackServer = providerId === "openai-codex" || providerId === "google-gemini-cli" || providerId === "google-antigravity";
2979
+ const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
2680
2980
  // Create login dialog component
2681
2981
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
2682
2982
  // Completion handled below
@@ -2739,6 +3039,7 @@ export class InteractiveMode {
2739
3039
  // Success
2740
3040
  restoreEditor();
2741
3041
  this.session.modelRegistry.refresh();
3042
+ await this.updateAvailableProviderCount();
2742
3043
  this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
2743
3044
  }
2744
3045
  catch (error) {
@@ -2752,6 +3053,53 @@ export class InteractiveMode {
2752
3053
  // =========================================================================
2753
3054
  // Command handlers
2754
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
+ }
2755
3103
  async handleExportCommand(text) {
2756
3104
  const parts = text.split(/\s+/);
2757
3105
  const outputPath = parts.length > 1 ? parts[1] : undefined;
@@ -2840,7 +3188,7 @@ export class InteractiveMode {
2840
3188
  return;
2841
3189
  }
2842
3190
  // Create the preview URL
2843
- const previewUrl = `https://buildwithpi.ai/session/#${gistId}`;
3191
+ const previewUrl = getShareViewerUrl(gistId);
2844
3192
  this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
2845
3193
  }
2846
3194
  catch (error) {
@@ -2879,6 +3227,7 @@ export class InteractiveMode {
2879
3227
  return;
2880
3228
  }
2881
3229
  this.sessionManager.appendSessionInfo(name);
3230
+ this.updateTerminalTitle();
2882
3231
  this.chatContainer.addChild(new Spacer(1));
2883
3232
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
2884
3233
  this.ui.requestRender();
@@ -2929,7 +3278,7 @@ export class InteractiveMode {
2929
3278
  this.chatContainer.addChild(new DynamicBorder());
2930
3279
  this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
2931
3280
  this.chatContainer.addChild(new Spacer(1));
2932
- this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
3281
+ this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));
2933
3282
  this.chatContainer.addChild(new DynamicBorder());
2934
3283
  this.ui.requestRender();
2935
3284
  }
@@ -2963,6 +3312,8 @@ export class InteractiveMode {
2963
3312
  const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
2964
3313
  const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
2965
3314
  const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
3315
+ const pageUp = this.getEditorKeyDisplay("pageUp");
3316
+ const pageDown = this.getEditorKeyDisplay("pageDown");
2966
3317
  // Editing keybindings
2967
3318
  const submit = this.getEditorKeyDisplay("submit");
2968
3319
  const newLine = this.getEditorKeyDisplay("newLine");
@@ -2981,6 +3332,7 @@ export class InteractiveMode {
2981
3332
  const suspend = this.getAppKeyDisplay("suspend");
2982
3333
  const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
2983
3334
  const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
3335
+ const selectModel = this.getAppKeyDisplay("selectModel");
2984
3336
  const expandTools = this.getAppKeyDisplay("expandTools");
2985
3337
  const toggleThinking = this.getAppKeyDisplay("toggleThinking");
2986
3338
  const externalEditor = this.getAppKeyDisplay("externalEditor");
@@ -2994,6 +3346,7 @@ export class InteractiveMode {
2994
3346
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
2995
3347
  | \`${cursorLineStart}\` | Start of line |
2996
3348
  | \`${cursorLineEnd}\` | End of line |
3349
+ | \`${pageUp}\` / \`${pageDown}\` | Scroll by page |
2997
3350
 
2998
3351
  **Editing**
2999
3352
  | Key | Action |
@@ -3018,6 +3371,7 @@ export class InteractiveMode {
3018
3371
  | \`${suspend}\` | Suspend to background |
3019
3372
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
3020
3373
  | \`${cycleModelForward}\` | Cycle models |
3374
+ | \`${selectModel}\` | Open model selector |
3021
3375
  | \`${expandTools}\` | Toggle tool output expansion |
3022
3376
  | \`${toggleThinking}\` | Toggle thinking block visibility |
3023
3377
  | \`${externalEditor}\` | Edit message in external editor |
@@ -3048,7 +3402,7 @@ export class InteractiveMode {
3048
3402
  this.chatContainer.addChild(new DynamicBorder());
3049
3403
  this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
3050
3404
  this.chatContainer.addChild(new Spacer(1));
3051
- this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, getMarkdownTheme()));
3405
+ this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
3052
3406
  this.chatContainer.addChild(new DynamicBorder());
3053
3407
  this.ui.requestRender();
3054
3408
  }
@@ -3074,11 +3428,12 @@ export class InteractiveMode {
3074
3428
  }
3075
3429
  handleDebugCommand() {
3076
3430
  const width = this.ui.terminal.columns;
3431
+ const height = this.ui.terminal.rows;
3077
3432
  const allLines = this.ui.render(width);
3078
3433
  const debugLogPath = getDebugLogPath();
3079
3434
  const debugData = [
3080
3435
  `Debug output at ${new Date().toISOString()}`,
3081
- `Terminal width: ${width}`,
3436
+ `Terminal: ${width}x${height}`,
3082
3437
  `Total lines: ${allLines.length}`,
3083
3438
  "",
3084
3439
  "=== All rendered lines with visible widths ===",