@oh-my-pi/pi-coding-agent 16.0.4 → 16.0.6

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 (270) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli.js +2027 -1396
  3. package/dist/types/advisor/advise-tool.d.ts +31 -19
  4. package/dist/types/autoresearch/tools/init-experiment.d.ts +13 -17
  5. package/dist/types/autoresearch/tools/log-experiment.d.ts +17 -19
  6. package/dist/types/autoresearch/tools/run-experiment.d.ts +3 -4
  7. package/dist/types/autoresearch/tools/update-notes.d.ts +4 -5
  8. package/dist/types/cli/args.d.ts +1 -0
  9. package/dist/types/cli/bench-cli.d.ts +6 -0
  10. package/dist/types/cli/ttsr-cli.d.ts +39 -0
  11. package/dist/types/commands/launch.d.ts +3 -0
  12. package/dist/types/commands/ttsr.d.ts +57 -0
  13. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  15. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  16. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  17. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  18. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  19. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  20. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  21. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  22. package/dist/types/commit/changelog/generate.d.ts +12 -13
  23. package/dist/types/commit/shared-llm.d.ts +10 -37
  24. package/dist/types/config/config-file.d.ts +4 -4
  25. package/dist/types/config/keybindings.d.ts +5 -0
  26. package/dist/types/config/models-config-schema.d.ts +625 -990
  27. package/dist/types/config/models-config.d.ts +229 -217
  28. package/dist/types/config/settings-schema.d.ts +144 -25
  29. package/dist/types/edit/hashline/params.d.ts +7 -11
  30. package/dist/types/edit/index.d.ts +2 -1
  31. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  32. package/dist/types/edit/modes/patch.d.ts +15 -24
  33. package/dist/types/edit/modes/replace.d.ts +16 -17
  34. package/dist/types/eval/js/index.d.ts +1 -0
  35. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  36. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  37. package/dist/types/extensibility/extensions/runner.d.ts +5 -2
  38. package/dist/types/extensibility/extensions/types.d.ts +14 -10
  39. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  40. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  41. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  42. package/dist/types/extensibility/shared-events.d.ts +22 -1
  43. package/dist/types/extensibility/typebox.d.ts +80 -58
  44. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  45. package/dist/types/index.d.ts +2 -0
  46. package/dist/types/lsp/index.d.ts +11 -26
  47. package/dist/types/lsp/types.d.ts +12 -28
  48. package/dist/types/main.d.ts +1 -0
  49. package/dist/types/mcp/client.d.ts +8 -0
  50. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  51. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  52. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  53. package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -1
  54. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  55. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  58. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  59. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  60. package/dist/types/modes/theme/theme.d.ts +1 -1
  61. package/dist/types/modes/types.d.ts +3 -0
  62. package/dist/types/modes/utils/context-usage.d.ts +12 -0
  63. package/dist/types/sdk.d.ts +8 -1
  64. package/dist/types/session/agent-session.d.ts +24 -0
  65. package/dist/types/session/session-persistence.d.ts +4 -0
  66. package/dist/types/startup-splash.d.ts +12 -0
  67. package/dist/types/task/types.d.ts +47 -48
  68. package/dist/types/tools/ask.d.ts +26 -27
  69. package/dist/types/tools/ast-edit.d.ts +17 -17
  70. package/dist/types/tools/ast-grep.d.ts +12 -13
  71. package/dist/types/tools/bash.d.ts +20 -17
  72. package/dist/types/tools/browser.d.ts +46 -71
  73. package/dist/types/tools/checkpoint.d.ts +14 -15
  74. package/dist/types/tools/debug.d.ts +82 -145
  75. package/dist/types/tools/eval.d.ts +30 -40
  76. package/dist/types/tools/find.d.ts +17 -18
  77. package/dist/types/tools/gh.d.ts +49 -78
  78. package/dist/types/tools/image-gen.d.ts +20 -36
  79. package/dist/types/tools/inspect-image.d.ts +10 -11
  80. package/dist/types/tools/irc.d.ts +22 -33
  81. package/dist/types/tools/job.d.ts +11 -12
  82. package/dist/types/tools/learn.d.ts +21 -28
  83. package/dist/types/tools/manage-skill.d.ts +13 -22
  84. package/dist/types/tools/memory-edit.d.ts +15 -24
  85. package/dist/types/tools/memory-recall.d.ts +7 -8
  86. package/dist/types/tools/memory-reflect.d.ts +9 -10
  87. package/dist/types/tools/memory-retain.d.ts +13 -14
  88. package/dist/types/tools/read.d.ts +8 -8
  89. package/dist/types/tools/resolve.d.ts +11 -18
  90. package/dist/types/tools/review.d.ts +9 -15
  91. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  92. package/dist/types/tools/search.d.ts +16 -17
  93. package/dist/types/tools/ssh.d.ts +14 -15
  94. package/dist/types/tools/todo.d.ts +27 -43
  95. package/dist/types/tools/tts.d.ts +8 -9
  96. package/dist/types/tools/write.d.ts +9 -10
  97. package/dist/types/tui/code-cell.d.ts +2 -0
  98. package/dist/types/tui/index.d.ts +1 -0
  99. package/dist/types/tui/width-aware-text.d.ts +23 -0
  100. package/dist/types/utils/image-vision-fallback.d.ts +28 -0
  101. package/dist/types/utils/markit.d.ts +10 -1
  102. package/dist/types/web/search/index.d.ts +17 -28
  103. package/dist/types/web/search/providers/base.d.ts +1 -0
  104. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  105. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  106. package/dist/types/web/search/types.d.ts +32 -26
  107. package/package.json +14 -13
  108. package/scripts/omp +1 -1
  109. package/src/advisor/__tests__/advisor.test.ts +103 -1
  110. package/src/advisor/advise-tool.ts +47 -11
  111. package/src/autoresearch/tools/init-experiment.ts +13 -16
  112. package/src/autoresearch/tools/log-experiment.ts +15 -18
  113. package/src/autoresearch/tools/run-experiment.ts +3 -3
  114. package/src/autoresearch/tools/update-notes.ts +4 -4
  115. package/src/cli/args.ts +1 -0
  116. package/src/cli/bench-cli.ts +30 -7
  117. package/src/cli/flag-tables.ts +8 -0
  118. package/src/cli/ttsr-cli.ts +995 -0
  119. package/src/cli-commands.ts +1 -0
  120. package/src/cli.ts +7 -1
  121. package/src/collab/host.ts +2 -2
  122. package/src/commands/launch.ts +3 -0
  123. package/src/commands/ttsr.ts +125 -0
  124. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  125. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  126. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  127. package/src/commit/agentic/tools/git-overview.ts +4 -4
  128. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  129. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  130. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  131. package/src/commit/agentic/tools/schemas.ts +8 -20
  132. package/src/commit/agentic/tools/split-commit.ts +19 -23
  133. package/src/commit/analysis/summary.ts +7 -5
  134. package/src/commit/changelog/generate.ts +15 -11
  135. package/src/commit/shared-llm.ts +17 -24
  136. package/src/config/config-file.ts +13 -15
  137. package/src/config/keybindings.ts +6 -0
  138. package/src/config/models-config-schema.ts +206 -179
  139. package/src/config/settings-schema.ts +118 -2
  140. package/src/discovery/builtin-rules/index.ts +2 -0
  141. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  142. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  143. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  144. package/src/edit/hashline/params.ts +12 -11
  145. package/src/edit/index.ts +5 -4
  146. package/src/edit/modes/apply-patch.ts +4 -4
  147. package/src/edit/modes/patch.ts +15 -18
  148. package/src/edit/modes/replace.ts +13 -17
  149. package/src/edit/renderer.ts +0 -1
  150. package/src/eval/agent-bridge.ts +11 -13
  151. package/src/eval/completion-bridge.ts +25 -17
  152. package/src/eval/js/context-manager.ts +17 -2
  153. package/src/eval/js/index.ts +1 -1
  154. package/src/eval/py/executor.ts +2 -2
  155. package/src/eval/py/runner.py +44 -0
  156. package/src/extensibility/custom-commands/loader.ts +5 -3
  157. package/src/extensibility/custom-commands/types.ts +6 -3
  158. package/src/extensibility/custom-tools/loader.ts +4 -2
  159. package/src/extensibility/custom-tools/types.ts +8 -5
  160. package/src/extensibility/extensions/loader.ts +4 -2
  161. package/src/extensibility/extensions/runner.ts +20 -2
  162. package/src/extensibility/extensions/types.ts +22 -8
  163. package/src/extensibility/hooks/loader.ts +5 -2
  164. package/src/extensibility/hooks/types.ts +7 -4
  165. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  166. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  167. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  168. package/src/extensibility/shared-events.ts +24 -0
  169. package/src/extensibility/tool-proxy.ts +4 -1
  170. package/src/extensibility/typebox.ts +778 -251
  171. package/src/goals/guided-setup.ts +12 -3
  172. package/src/goals/tools/goal-tool.ts +6 -6
  173. package/src/index.ts +2 -0
  174. package/src/internal-urls/docs-index.generated.ts +15 -13
  175. package/src/lsp/types.ts +13 -27
  176. package/src/main.ts +29 -21
  177. package/src/mcp/client.ts +38 -13
  178. package/src/mcp/render.ts +102 -89
  179. package/src/modes/components/agent-hub.ts +11 -4
  180. package/src/modes/components/branch-summary-message.ts +1 -0
  181. package/src/modes/components/btw-panel.ts +5 -1
  182. package/src/modes/components/collab-prompt-message.ts +9 -7
  183. package/src/modes/components/compaction-summary-message.ts +1 -0
  184. package/src/modes/components/custom-editor.ts +18 -0
  185. package/src/modes/components/custom-message.ts +1 -0
  186. package/src/modes/components/footer.ts +6 -5
  187. package/src/modes/components/hook-message.ts +1 -0
  188. package/src/modes/components/read-tool-group.ts +9 -3
  189. package/src/modes/components/skill-message.ts +1 -0
  190. package/src/modes/components/status-line/component.ts +139 -15
  191. package/src/modes/components/status-line/context-thresholds.ts +0 -1
  192. package/src/modes/components/todo-reminder.ts +1 -0
  193. package/src/modes/components/tool-execution.ts +17 -10
  194. package/src/modes/components/ttsr-notification.ts +1 -0
  195. package/src/modes/components/user-message.ts +6 -6
  196. package/src/modes/controllers/btw-controller.ts +69 -1
  197. package/src/modes/controllers/event-controller.ts +2 -7
  198. package/src/modes/controllers/input-controller.ts +29 -0
  199. package/src/modes/controllers/selector-controller.ts +10 -3
  200. package/src/modes/interactive-mode.ts +42 -10
  201. package/src/modes/rpc/rpc-types.ts +1 -1
  202. package/src/modes/setup-wizard/index.ts +1 -0
  203. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  204. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  205. package/src/modes/theme/theme.ts +133 -143
  206. package/src/modes/types.ts +3 -0
  207. package/src/modes/utils/context-usage.ts +37 -20
  208. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  209. package/src/prompts/system/system-prompt.md +1 -0
  210. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  211. package/src/prompts/tools/image-attachment-describe.md +10 -0
  212. package/src/sdk.ts +35 -22
  213. package/src/session/agent-session.ts +715 -255
  214. package/src/session/session-history-format.ts +11 -2
  215. package/src/session/session-loader.ts +19 -32
  216. package/src/session/session-persistence.ts +27 -11
  217. package/src/session/snapcompact-inline.ts +1 -1
  218. package/src/slash-commands/builtin-registry.ts +4 -11
  219. package/src/ssh/connection-manager.ts +3 -2
  220. package/src/startup-splash.ts +19 -0
  221. package/src/task/executor.ts +12 -7
  222. package/src/task/types.ts +44 -41
  223. package/src/tool-discovery/tool-index.ts +17 -4
  224. package/src/tools/ask.ts +14 -14
  225. package/src/tools/ast-edit.ts +17 -14
  226. package/src/tools/ast-grep.ts +10 -9
  227. package/src/tools/bash.ts +15 -10
  228. package/src/tools/browser/launch.ts +13 -0
  229. package/src/tools/browser.ts +26 -32
  230. package/src/tools/checkpoint.ts +7 -7
  231. package/src/tools/debug.ts +72 -69
  232. package/src/tools/eval.ts +18 -19
  233. package/src/tools/find.ts +20 -13
  234. package/src/tools/gh.ts +29 -49
  235. package/src/tools/image-gen.ts +94 -57
  236. package/src/tools/inspect-image.ts +8 -9
  237. package/src/tools/irc.ts +12 -12
  238. package/src/tools/job.ts +6 -6
  239. package/src/tools/learn.ts +11 -14
  240. package/src/tools/manage-skill.ts +19 -23
  241. package/src/tools/memory-edit.ts +8 -8
  242. package/src/tools/memory-recall.ts +4 -4
  243. package/src/tools/memory-reflect.ts +5 -5
  244. package/src/tools/memory-retain.ts +9 -11
  245. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  246. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  247. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  248. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  249. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  250. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  251. package/src/tools/read.ts +197 -19
  252. package/src/tools/report-tool-issue.ts +6 -6
  253. package/src/tools/resolve.ts +6 -6
  254. package/src/tools/review.ts +10 -12
  255. package/src/tools/search-tool-bm25.ts +5 -5
  256. package/src/tools/search.ts +20 -29
  257. package/src/tools/ssh.ts +8 -8
  258. package/src/tools/todo.ts +16 -19
  259. package/src/tools/tts.ts +16 -15
  260. package/src/tools/write.ts +5 -5
  261. package/src/tui/code-cell.ts +44 -3
  262. package/src/tui/index.ts +1 -0
  263. package/src/tui/width-aware-text.ts +58 -0
  264. package/src/utils/image-vision-fallback.ts +197 -0
  265. package/src/utils/markit.ts +17 -2
  266. package/src/web/search/index.ts +21 -9
  267. package/src/web/search/providers/base.ts +1 -0
  268. package/src/web/search/providers/gemini.ts +56 -18
  269. package/src/web/search/providers/perplexity.ts +373 -126
  270. package/src/web/search/types.ts +28 -48
package/src/lsp/types.ts CHANGED
@@ -1,38 +1,24 @@
1
1
  import type { ptree } from "@oh-my-pi/pi-utils";
2
- import { z } from "zod/v4";
2
+ import { type } from "arktype";
3
3
 
4
4
  // =============================================================================
5
5
  // Tool Schema
6
6
  // =============================================================================
7
7
 
8
- export const lspSchema = z.object({
9
- action: z.enum([
10
- "diagnostics",
11
- "definition",
12
- "references",
13
- "hover",
14
- "symbols",
15
- "rename",
16
- "rename_file",
17
- "code_actions",
18
- "type_definition",
19
- "implementation",
20
- "status",
21
- "reload",
22
- "capabilities",
23
- "request",
24
- ]),
25
- file: z.string().describe("file path or source path for rename_file").optional(),
26
- line: z.number().describe("line number (1-indexed)").optional(),
27
- symbol: z.string().describe("symbol substring on the line").optional(),
28
- query: z.string().describe("search query or code-action selector").optional(),
29
- new_name: z.string().describe("new symbol name or destination path").optional(),
30
- apply: z.boolean().describe("apply edits").optional(),
31
- timeout: z.number().describe("request timeout in seconds").optional(),
32
- payload: z.string().describe("json-encoded request params").optional(),
8
+ export const lspSchema = type({
9
+ action:
10
+ "'diagnostics' | 'definition' | 'references' | 'hover' | 'symbols' | 'rename' | 'rename_file' | 'code_actions' | 'type_definition' | 'implementation' | 'status' | 'reload' | 'capabilities' | 'request'",
11
+ file: "string?",
12
+ line: "number?",
13
+ symbol: "string?",
14
+ query: "string?",
15
+ new_name: "string?",
16
+ apply: "boolean?",
17
+ timeout: "number?",
18
+ payload: "string?",
33
19
  });
34
20
 
35
- export type LspParams = z.infer<typeof lspSchema>;
21
+ export type LspParams = typeof lspSchema.infer;
36
22
 
37
23
  export interface LspToolDetails {
38
24
  serverName?: string;
package/src/main.ts CHANGED
@@ -68,6 +68,7 @@ import type { AuthStorage } from "./session/auth-storage";
68
68
  import { resolveResumableSession, type SessionInfo } from "./session/session-listing";
69
69
  import { SessionManager } from "./session/session-manager";
70
70
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
71
+ import { shouldShowStartupSplash } from "./startup-splash";
71
72
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
72
73
  import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
73
74
  import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
@@ -90,17 +91,8 @@ type RunRpcMode = (
90
91
  eventBus?: EventBus,
91
92
  ) => Promise<never>;
92
93
 
93
- function maybeShowStartupSplash(options: {
94
- isInteractive: boolean;
95
- resuming: boolean;
96
- quiet: boolean;
97
- version: string;
98
- }): void {
99
- if (!options.isInteractive) return;
100
- if (options.resuming || options.quiet) return;
101
- if ($env.PI_TIMING) return;
102
- if (!process.stdin.isTTY || !process.stdout.isTTY) return;
103
- //process.stdout.write(`${chalk.dim(`omp ${options.version}`)}\n${chalk.dim("Initializing session…")}\n`);
94
+ export function writeStartupNotice(parsedArgs: Pick<Args, "mode">, text: string): void {
95
+ (parsedArgs.mode === "json" ? process.stderr : process.stdout).write(text);
104
96
  }
105
97
 
106
98
  async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
@@ -387,6 +379,7 @@ async function runInteractiveMode(
387
379
  mcpManager: MCPManager | undefined,
388
380
  resuming: boolean,
389
381
  forceSetupWizard: boolean,
382
+ showStartupSplash: boolean,
390
383
  eventBus?: EventBus,
391
384
  initialMessage?: string,
392
385
  initialImages?: ImageContent[],
@@ -407,10 +400,13 @@ async function runInteractiveMode(
407
400
  // Cold-launch gate: the full setup wizard (every scene + the overlay and
408
401
  // their TUI/OAuth/search/theme deps) is heavy, yet the common case only needs
409
402
  // to know whether the stored setup version is current. Lazy-load the wizard
410
- // barrel only when setup is stale or forced; otherwise skip it entirely.
403
+ // barrel only when setup is stale, forced, or the explicit startup splash
404
+ // setting needs the shared setup splash renderer.
411
405
  const storedSetupVersion = settings.get("setupVersion");
412
406
  const setupWizard =
413
- forceSetupWizard || storedSetupVersion < CURRENT_SETUP_VERSION ? await import("./modes/setup-wizard") : undefined;
407
+ forceSetupWizard || storedSetupVersion < CURRENT_SETUP_VERSION || showStartupSplash
408
+ ? await import("./modes/setup-wizard")
409
+ : undefined;
414
410
  const setupScenes = setupWizard
415
411
  ? await setupWizard.selectSetupScenes(storedSetupVersion, setupWizard.ALL_SCENES, mode, {
416
412
  resuming,
@@ -419,12 +415,17 @@ async function runInteractiveMode(
419
415
  force: forceSetupWizard,
420
416
  })
421
417
  : [];
418
+ const playStartupSplash = showStartupSplash && setupScenes.length === 0;
422
419
 
423
420
  await mode.init({
424
- suppressWelcomeIntro: resuming || setupScenes.length > 0,
421
+ suppressWelcomeIntro: resuming || setupScenes.length > 0 || playStartupSplash,
425
422
  clearInitialTerminalHistory: true,
426
423
  });
427
424
 
425
+ if (setupWizard && playStartupSplash) {
426
+ await setupWizard.runStartupSplash(mode);
427
+ }
428
+
428
429
  if (setupWizard && setupScenes.length > 0) {
429
430
  await setupWizard.runSetupWizard(mode, setupScenes);
430
431
  }
@@ -758,6 +759,9 @@ async function buildSessionOptions(
758
759
  cwd: parsed.cwd ?? getProjectDir(),
759
760
  autoApprove: parsed.autoApprove ?? false,
760
761
  };
762
+ if (parsed.maxTime !== undefined) {
763
+ options.deadline = Date.now() + parsed.maxTime * 1000;
764
+ }
761
765
 
762
766
  // Auto-discover SYSTEM.md if no CLI system prompt provided
763
767
  const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
@@ -944,7 +948,7 @@ export async function runRootCommand(
944
948
  const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
945
949
 
946
950
  if (parsedArgs.version) {
947
- process.stdout.write(`${VERSION}\n`);
951
+ writeStartupNotice(parsedArgs, `${VERSION}\n`);
948
952
  process.exit(0);
949
953
  }
950
954
 
@@ -959,7 +963,7 @@ export async function runRootCommand(
959
963
  process.stderr.write(`${chalk.red(`Error: ${message}`)}\n`);
960
964
  process.exit(1);
961
965
  }
962
- process.stdout.write(`Exported to: ${result}\n`);
966
+ writeStartupNotice(parsedArgs, `Exported to: ${result}\n`);
963
967
  process.exit(0);
964
968
  }
965
969
 
@@ -1095,7 +1099,7 @@ export async function runRootCommand(
1095
1099
  // message rather than letting the decline bubble up as an uncaught exception
1096
1100
  // (see issue #1668).
1097
1101
  if (typeof parsedArgs.resume === "string" && !sessionManager) {
1098
- process.stdout.write(`${chalk.dim("Resume cancelled: session is in another project.")}\n`);
1102
+ writeStartupNotice(parsedArgs, `${chalk.dim("Resume cancelled: session is in another project.")}\n`);
1099
1103
  return;
1100
1104
  }
1101
1105
 
@@ -1109,7 +1113,7 @@ export async function runRootCommand(
1109
1113
  // picker can still open in all-projects scope instead of dead-ending.
1110
1114
  preloadedAllSessions = await logger.time("SessionManager.listAll", SessionManager.listAll);
1111
1115
  if (preloadedAllSessions.length === 0) {
1112
- process.stdout.write(`${chalk.dim("No sessions found")}\n`);
1116
+ writeStartupNotice(parsedArgs, `${chalk.dim("No sessions found")}\n`);
1113
1117
  return;
1114
1118
  }
1115
1119
  startInAllScope = true;
@@ -1121,7 +1125,7 @@ export async function runRootCommand(
1121
1125
  });
1122
1126
  resumeStartupWatchdog();
1123
1127
  if (!selected) {
1124
- process.stdout.write(`${chalk.dim("No session selected")}\n`);
1128
+ writeStartupNotice(parsedArgs, `${chalk.dim("No session selected")}\n`);
1125
1129
  return;
1126
1130
  }
1127
1131
  // Resuming a session from another project: switch the process into that
@@ -1254,11 +1258,14 @@ export async function runRootCommand(
1254
1258
  stdinContent: pipedInput,
1255
1259
  });
1256
1260
 
1257
- maybeShowStartupSplash({
1261
+ const showStartupSplash = shouldShowStartupSplash({
1262
+ configured: settingsInstance.get("startup.showSplash"),
1258
1263
  isInteractive,
1259
1264
  resuming: Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1260
1265
  quiet: settingsInstance.get("startup.quiet"),
1261
- version: VERSION,
1266
+ timing: Boolean($env.PI_TIMING),
1267
+ stdinIsTTY: process.stdin.isTTY,
1268
+ stdoutIsTTY: process.stdout.isTTY,
1262
1269
  });
1263
1270
 
1264
1271
  const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await createSession({
@@ -1350,6 +1357,7 @@ export async function runRootCommand(
1350
1357
  mcpManager,
1351
1358
  Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1352
1359
  deps.forceSetupWizard === true,
1360
+ showStartupSplash,
1353
1361
  eventBus,
1354
1362
  initialMessage,
1355
1363
  initialImages,
package/src/mcp/client.ts CHANGED
@@ -303,8 +303,22 @@ export async function listResources(
303
303
  return allResources;
304
304
  }
305
305
 
306
+ /** True when an error is a JSON-RPC "method not found" (-32601) response. */
307
+ function isMethodNotFoundError(error: unknown): boolean {
308
+ const message = error instanceof Error ? error.message : String(error);
309
+ return message.includes("-32601") || /method not found/i.test(message);
310
+ }
311
+
306
312
  /**
307
313
  * List resource templates from a connected server.
314
+ *
315
+ * A server MAY advertise the `resources` capability without implementing the
316
+ * optional `resources/templates/list` method (it is optional in the MCP spec).
317
+ * Such servers reject the request with JSON-RPC -32601 ("Method not found").
318
+ * Treat that as "no templates" and return `[]` rather than throwing — otherwise
319
+ * a caller that loads resources and templates together (see `MCPManager`'s
320
+ * `Promise.all([listResources, listResourceTemplates])`) would discard the
321
+ * server's concrete resources too. Any other error still propagates.
308
322
  */
309
323
  export async function listResourceTemplates(
310
324
  connection: MCPServerConnection,
@@ -321,20 +335,31 @@ export async function listResourceTemplates(
321
335
  const allTemplates: MCPResourceTemplate[] = [];
322
336
  let cursor: string | undefined;
323
337
 
324
- do {
325
- const params: Record<string, unknown> = {};
326
- if (cursor) {
327
- params.cursor = cursor;
338
+ try {
339
+ do {
340
+ const params: Record<string, unknown> = {};
341
+ if (cursor) {
342
+ params.cursor = cursor;
343
+ }
344
+
345
+ const result = await connection.transport.request<MCPResourceTemplatesListResult>(
346
+ "resources/templates/list",
347
+ params,
348
+ options,
349
+ );
350
+ allTemplates.push(...result.resourceTemplates);
351
+ cursor = result.nextCursor;
352
+ } while (cursor);
353
+ } catch (error) {
354
+ // A server that doesn't implement the optional templates method answers
355
+ // -32601; cache an empty list so we neither retry nor let the failure
356
+ // bubble up and discard the server's concrete resources.
357
+ if (isMethodNotFoundError(error)) {
358
+ connection.resourceTemplates = [];
359
+ return [];
328
360
  }
329
-
330
- const result = await connection.transport.request<MCPResourceTemplatesListResult>(
331
- "resources/templates/list",
332
- params,
333
- options,
334
- );
335
- allTemplates.push(...result.resourceTemplates);
336
- cursor = result.nextCursor;
337
- } while (cursor);
361
+ throw error;
362
+ }
338
363
 
339
364
  connection.resourceTemplates = allTemplates;
340
365
  return allTemplates;
package/src/mcp/render.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  * showing args and output in JSON tree format similar to task tool.
6
6
  */
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
- import { Text } from "@oh-my-pi/pi-tui";
9
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
9
  import type { Theme } from "../modes/theme/theme";
11
10
  import {
@@ -20,25 +19,33 @@ import {
20
19
  } from "../tools/json-tree";
21
20
  import { formatStyledTruncationWarning, stripOutputNotice } from "../tools/output-meta";
22
21
  import { formatExpandHint, truncateToWidth } from "../tools/render-utils";
23
- import { renderStatusLine } from "../tui";
22
+ import { renderStatusLine, WidthAwareText } from "../tui";
24
23
  import type { MCPToolDetails } from "./tool-bridge";
25
24
 
26
25
  /**
27
26
  * Render MCP tool call.
28
27
  */
29
28
  export function renderMCPCall(args: Record<string, unknown>, theme: Theme, label: string): Component {
30
- const lines: string[] = [];
31
- lines.push(renderStatusLine({ icon: "pending", title: label }, theme));
29
+ return new WidthAwareText(
30
+ contentWidth => {
31
+ const lines: string[] = [];
32
+ lines.push(renderStatusLine({ icon: "pending", title: label }, theme));
32
33
 
33
- if (args && typeof args === "object" && Object.keys(args).length > 0) {
34
- // Show args inline preview
35
- const preview = formatArgsInline(args, 70);
36
- if (preview) {
37
- lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
38
- }
39
- }
34
+ if (args && typeof args === "object" && Object.keys(args).length > 0) {
35
+ // Inline preview budgeted against the render width, leaving room for
36
+ // the ` └─ ` connector prefix instead of a fixed cap.
37
+ const inlineBudget = Math.max(20, contentWidth - Bun.stringWidth(theme.tree.last) - 2);
38
+ const preview = formatArgsInline(args, inlineBudget);
39
+ if (preview) {
40
+ lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
41
+ }
42
+ }
40
43
 
41
- return new Text(lines.join("\n"), 0, 0);
44
+ return lines.join("\n");
45
+ },
46
+ 0,
47
+ 0,
48
+ );
42
49
  }
43
50
 
44
51
  /**
@@ -51,92 +58,98 @@ export function renderMCPResult(
51
58
  args?: Record<string, unknown>,
52
59
  ): Component {
53
60
  const { expanded } = options;
54
- const lines: string[] = [];
55
- const isError = result.isError ?? result.details?.isError ?? false;
56
- const title = result.details ? `${result.details.serverName}/${result.details.mcpToolName}` : "MCP";
57
- const success = !isError;
58
- lines.push(
59
- renderStatusLine(
60
- success ? { iconOverride: theme.styledSymbol("tool.mcp", "accent"), title } : { icon: "error", title },
61
- theme,
62
- ),
63
- );
64
-
65
- // Args section (when expanded)
66
- if (expanded && args && typeof args === "object" && Object.keys(args).length > 0) {
67
- lines.push(`${theme.fg("dim", "Args")}`);
68
- const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED;
69
- const maxLines = JSON_TREE_MAX_LINES_EXPANDED;
70
- const tree = renderJsonTreeLines(args, theme, maxDepth, maxLines, JSON_TREE_SCALAR_LEN_EXPANDED);
71
- for (const line of tree.lines) {
72
- lines.push(line);
73
- }
74
- if (tree.truncated) {
75
- lines.push(theme.fg("dim", "…"));
76
- }
77
- lines.push(""); // Blank line before output
78
- }
61
+ return new WidthAwareText(
62
+ contentWidth => {
63
+ const lines: string[] = [];
64
+ const isError = result.isError ?? result.details?.isError ?? false;
65
+ const title = result.details ? `${result.details.serverName}/${result.details.mcpToolName}` : "MCP";
66
+ const success = !isError;
67
+ lines.push(
68
+ renderStatusLine(
69
+ success ? { iconOverride: theme.styledSymbol("tool.mcp", "accent"), title } : { icon: "error", title },
70
+ theme,
71
+ ),
72
+ );
79
73
 
80
- // Output section
81
- const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
82
- // Strip the LLM-facing spill notice before parsing/rendering: a spilled
83
- // result appends `[Showing… artifact://N]` to the body, which would break
84
- // JSON detection and bury the recovery link. Surface it as a styled warning
85
- // instead, mirroring the built-in read/bash/ssh/browser renderers.
86
- const trimmedOutput = stripOutputNotice(textContent, result.details?.meta).trimEnd();
87
- const truncationWarning = result.details?.meta?.truncation
88
- ? formatStyledTruncationWarning(result.details.meta, theme)
89
- : null;
90
-
91
- if (!trimmedOutput) {
92
- lines.push(theme.fg("dim", "(no output)"));
93
- return new Text(lines.join("\n"), 0, 0);
94
- }
95
-
96
- // Try to parse as JSON for structured display
97
- if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
98
- try {
99
- const parsed = JSON.parse(trimmedOutput);
100
- const maxDepth = expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
101
- const maxLines = expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
102
- const maxScalarLen = expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
103
- const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
104
-
105
- if (tree.lines.length > 0) {
74
+ // Args section (when expanded)
75
+ if (expanded && args && typeof args === "object" && Object.keys(args).length > 0) {
76
+ lines.push(`${theme.fg("dim", "Args")}`);
77
+ const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED;
78
+ const maxLines = JSON_TREE_MAX_LINES_EXPANDED;
79
+ const tree = renderJsonTreeLines(args, theme, maxDepth, maxLines, JSON_TREE_SCALAR_LEN_EXPANDED);
106
80
  for (const line of tree.lines) {
107
81
  lines.push(line);
108
82
  }
109
- // Always show expand hint when collapsed (expanded view shows longer values and deeper nesting)
110
- if (!expanded) {
111
- lines.push(formatExpandHint(theme, expanded, true));
112
- } else if (tree.truncated) {
83
+ if (tree.truncated) {
113
84
  lines.push(theme.fg("dim", "…"));
114
85
  }
115
- if (truncationWarning) lines.push(truncationWarning);
116
- return new Text(lines.join("\n"), 0, 0);
86
+ lines.push(""); // Blank line before output
87
+ }
88
+
89
+ // Output section
90
+ const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
91
+ // Strip the LLM-facing spill notice before parsing/rendering: a spilled
92
+ // result appends `[Showing… artifact://N]` to the body, which would break
93
+ // JSON detection and bury the recovery link. Surface it as a styled warning
94
+ // instead, mirroring the built-in read/bash/ssh/browser renderers.
95
+ const trimmedOutput = stripOutputNotice(textContent, result.details?.meta).trimEnd();
96
+ const truncationWarning = result.details?.meta?.truncation
97
+ ? formatStyledTruncationWarning(result.details.meta, theme)
98
+ : null;
99
+
100
+ if (!trimmedOutput) {
101
+ lines.push(theme.fg("dim", "(no output)"));
102
+ return lines.join("\n");
103
+ }
104
+
105
+ // Try to parse as JSON for structured display
106
+ if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
107
+ try {
108
+ const parsed = JSON.parse(trimmedOutput);
109
+ const maxDepth = expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
110
+ const maxLines = expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
111
+ const maxScalarLen = expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
112
+ const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
113
+
114
+ if (tree.lines.length > 0) {
115
+ for (const line of tree.lines) {
116
+ lines.push(line);
117
+ }
118
+ // Always show expand hint when collapsed (expanded view shows longer values and deeper nesting)
119
+ if (!expanded) {
120
+ lines.push(formatExpandHint(theme, expanded, true));
121
+ } else if (tree.truncated) {
122
+ lines.push(theme.fg("dim", "…"));
123
+ }
124
+ if (truncationWarning) lines.push(truncationWarning);
125
+ return lines.join("\n");
126
+ }
127
+ } catch {
128
+ // Fall through to raw output
129
+ }
117
130
  }
118
- } catch {
119
- // Fall through to raw output
120
- }
121
- }
122
131
 
123
- // Raw text output
124
- const outputLines = trimmedOutput.split("\n");
125
- const maxOutputLines = expanded ? 12 : 4;
126
- const displayLines = outputLines.slice(0, maxOutputLines);
132
+ // Raw text output
133
+ const outputLines = trimmedOutput.split("\n");
134
+ const maxOutputLines = expanded ? 12 : 4;
135
+ const displayLines = outputLines.slice(0, maxOutputLines);
127
136
 
128
- for (const line of displayLines) {
129
- lines.push(theme.fg("toolOutput", truncateToWidth(line, 80)));
130
- }
137
+ for (const line of displayLines) {
138
+ lines.push(theme.fg("toolOutput", truncateToWidth(line, contentWidth)));
139
+ }
131
140
 
132
- if (outputLines.length > maxOutputLines) {
133
- const remaining = outputLines.length - maxOutputLines;
134
- lines.push(`${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, expanded, true)}`);
135
- } else if (!expanded) {
136
- // Show expand hint when collapsed even if all lines shown (lines may be truncated)
137
- lines.push(formatExpandHint(theme, expanded, true));
138
- }
141
+ if (outputLines.length > maxOutputLines) {
142
+ const remaining = outputLines.length - maxOutputLines;
143
+ lines.push(`${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, expanded, true)}`);
144
+ } else if (!expanded) {
145
+ // Show expand hint when collapsed even if all lines shown (lines may be truncated)
146
+ lines.push(formatExpandHint(theme, expanded, true));
147
+ }
139
148
 
140
- if (truncationWarning) lines.push(truncationWarning);
141
- return new Text(lines.join("\n"), 0, 0);
149
+ if (truncationWarning) lines.push(truncationWarning);
150
+ return lines.join("\n");
151
+ },
152
+ 0,
153
+ 0,
154
+ );
142
155
  }
@@ -17,7 +17,7 @@ import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
  import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
19
19
  import type { Usage } from "@oh-my-pi/pi-ai";
20
- import { Container, Editor, matchesKey, ScrollView, Text, type TUI } from "@oh-my-pi/pi-tui";
20
+ import { Container, Editor, Ellipsis, matchesKey, ScrollView, Text, type TUI } from "@oh-my-pi/pi-tui";
21
21
  import { formatAge, formatBytes, formatDuration, formatNumber, getProjectDir, logger } from "@oh-my-pi/pi-utils";
22
22
  import type { AdvisorMessageDetails } from "../../advisor";
23
23
  import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
@@ -81,7 +81,12 @@ function contentWidth(): number {
81
81
 
82
82
  /** Sanitize a line for TUI display: replace tabs, then truncate to viewport width. */
83
83
  function sanitizeLine(text: string, maxWidth?: number): string {
84
- return truncateToWidth(replaceTabs(text), maxWidth ?? contentWidth());
84
+ const singleLine = replaceTabs(text).replace(/[\r\n]+/g, " ");
85
+ return truncateToWidth(singleLine, maxWidth ?? contentWidth());
86
+ }
87
+
88
+ function clampHubLine(line: string, width: number): string {
89
+ return truncateToWidth(line.replace(/[\r\n]+/g, " "), Math.max(1, width - 2), Ellipsis.Omit);
85
90
  }
86
91
 
87
92
  const STATUS_ORDER: Record<AgentStatus, number> = { running: 0, idle: 1, parked: 2, aborted: 3 };
@@ -298,7 +303,8 @@ export class AgentHubOverlayComponent extends Container {
298
303
  }
299
304
 
300
305
  override render(width: number): readonly string[] {
301
- return this.#view === "table" ? this.#renderTable(width) : this.#renderChat(width);
306
+ const lines = this.#view === "table" ? this.#renderTable(width) : this.#renderChat(width);
307
+ return lines.map(line => clampHubLine(line, width));
302
308
  }
303
309
 
304
310
  handleInput(keyData: string): void {
@@ -490,7 +496,8 @@ export class AgentHubOverlayComponent extends Container {
490
496
  parts.push(theme.fg("warning", `⧉ ${unread}`));
491
497
  }
492
498
  parts.push(theme.fg("dim", formatAge(Math.max(1, Math.round((Date.now() - ref.lastActivity) / 1000)))));
493
- return truncateToWidth(` ${cursor} ${parts.join(theme.sep.dot)}`, Math.max(10, width - 1));
499
+ const rawLine = ` ${cursor} ${parts.join(theme.sep.dot)}`;
500
+ return truncateToWidth(rawLine.replace(/[\r\n]+/g, " "), Math.max(1, width - 1));
494
501
  }
495
502
 
496
503
  #handleTableInput(keyData: string): void {
@@ -11,6 +11,7 @@ export class BranchSummaryMessageComponent extends Box {
11
11
 
12
12
  constructor(private readonly message: BranchSummaryMessage) {
13
13
  super(1, 1, t => theme.bg("customMessageBg", t));
14
+ this.setIgnoreTight(true);
14
15
  this.#updateDisplay();
15
16
  }
16
17
 
@@ -58,6 +58,10 @@ export class BtwPanelComponent extends Container {
58
58
  this.#rebuild();
59
59
  }
60
60
 
61
+ isBranchable(): boolean {
62
+ return this.#state === "complete" && this.#answer.trim().length > 0;
63
+ }
64
+
61
65
  close(): void {
62
66
  this.#closed = true;
63
67
  }
@@ -85,7 +89,7 @@ export class BtwPanelComponent extends Container {
85
89
  case "running":
86
90
  return theme.fg("muted", "Esc cancel /btw");
87
91
  case "complete":
88
- return theme.fg("muted", "Esc dismiss");
92
+ return theme.fg("muted", this.isBranchable() ? "b branch · Esc dismiss" : "Esc dismiss");
89
93
  case "aborted":
90
94
  return theme.fg("warning", `${theme.status.warning} Cancelled · Esc dismiss`);
91
95
  case "error":
@@ -12,7 +12,9 @@ export class CollabPromptMessageComponent extends Container {
12
12
  constructor(message: CustomMessage<CollabPromptDetails>) {
13
13
  super();
14
14
  const from = message.details?.from?.trim() || "guest";
15
- this.addChild(new Text(theme.fg("accent", `\x1b[1m«${from}»\x1b[22m ›`), 1, 0));
15
+ const authorText = new Text(theme.fg("accent", `\x1b[1m«${from}»\x1b[22m ›`), 1, 0);
16
+ authorText.setIgnoreTight(true);
17
+ this.addChild(authorText);
16
18
  const text =
17
19
  typeof message.content === "string"
18
20
  ? message.content
@@ -20,11 +22,11 @@ export class CollabPromptMessageComponent extends Container {
20
22
  .filter((content): content is TextContent => content.type === "text")
21
23
  .map(content => content.text)
22
24
  .join("");
23
- this.addChild(
24
- new Markdown(text, 1, 1, getMarkdownTheme(), {
25
- bgColor: (value: string) => theme.bg("userMessageBg", value),
26
- color: (value: string) => theme.fg("userMessageText", value),
27
- }),
28
- );
25
+ const md = new Markdown(text, 1, 1, getMarkdownTheme(), {
26
+ bgColor: (value: string) => theme.bg("userMessageBg", value),
27
+ color: (value: string) => theme.fg("userMessageText", value),
28
+ });
29
+ md.setIgnoreTight(true);
30
+ this.addChild(md);
29
31
  }
30
32
  }
@@ -62,6 +62,7 @@ class SummaryDividerComponent implements Component {
62
62
  #detailBox(): Box {
63
63
  if (this.#detail) return this.#detail;
64
64
  const box = new Box(1, 1, t => theme.bg("customMessageBg", t));
65
+ box.setIgnoreTight(true);
65
66
  box.addChild(
66
67
  new Markdown(this.options.detailMarkdown(), 0, 0, getMarkdownTheme(), {
67
68
  color: (text: string) => theme.fg("customMessageText", text),
@@ -22,6 +22,7 @@ type ConfigurableEditorAction = Extract<
22
22
  | "app.editor.external"
23
23
  | "app.history.search"
24
24
  | "app.message.dequeue"
25
+ | "app.retry"
25
26
  | "app.clipboard.pasteImage"
26
27
  | "app.clipboard.pasteTextRaw"
27
28
  | "app.clipboard.copyPrompt"
@@ -43,6 +44,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
43
44
  "app.editor.external": ["ctrl+g"],
44
45
  "app.history.search": ["ctrl+r"],
45
46
  "app.message.dequeue": ["alt+up"],
47
+ "app.retry": ["alt+r"],
46
48
  "app.clipboard.pasteImage": ["ctrl+v"],
47
49
  "app.clipboard.pasteTextRaw": ["ctrl+shift+v", "alt+shift+v"],
48
50
  "app.clipboard.copyPrompt": ["alt+shift+c"],
@@ -268,6 +270,8 @@ export class CustomEditor extends Editor {
268
270
  onPasteTextRaw?: () => void;
269
271
  /** Called when the configured dequeue shortcut is pressed. */
270
272
  onDequeue?: () => void;
273
+ /** Called when the configured retry shortcut is pressed. */
274
+ onRetry?: () => void;
271
275
  /** Called when Caps Lock is pressed. */
272
276
  onCapsLock?: () => void;
273
277
  /** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
@@ -588,6 +592,20 @@ export class CustomEditor extends Editor {
588
592
  return;
589
593
  }
590
594
 
595
+ // Intercept configured retry shortcut. Later user/custom handlers keep
596
+ // precedence so adding the default Alt+R binding does not steal existing
597
+ // shortcuts such as app.plan.toggle or extension commands; copy-prompt is
598
+ // checked above for the same reason.
599
+ if (this.#matchesAction(canonical, "app.retry") && this.onRetry) {
600
+ const customHandler = this.#customMatchKeys.get(canonical);
601
+ if (customHandler) {
602
+ customHandler();
603
+ return;
604
+ }
605
+ this.onRetry();
606
+ return;
607
+ }
608
+
591
609
  // Check custom key handlers (extensions)
592
610
  const handler = this.#customMatchKeys.get(canonical);
593
611
  if (handler) {
@@ -22,6 +22,7 @@ export class CustomMessageComponent extends Container {
22
22
 
23
23
  // Create box with custom background (used for default rendering)
24
24
  this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
25
+ this.#box.setIgnoreTight(true);
25
26
 
26
27
  this.#rebuild();
27
28
  }