@oh-my-pi/pi-coding-agent 16.0.5 → 16.0.7

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 (223) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +1945 -1386
  3. package/dist/types/advisor/advise-tool.d.ts +22 -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/ttsr-cli.d.ts +39 -0
  9. package/dist/types/commands/ttsr.d.ts +57 -0
  10. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  11. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  12. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  13. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  15. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  16. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  17. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  18. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  19. package/dist/types/commit/changelog/generate.d.ts +12 -13
  20. package/dist/types/commit/shared-llm.d.ts +10 -37
  21. package/dist/types/config/config-file.d.ts +4 -4
  22. package/dist/types/config/keybindings.d.ts +5 -0
  23. package/dist/types/config/models-config-schema.d.ts +625 -990
  24. package/dist/types/config/models-config.d.ts +229 -217
  25. package/dist/types/config/settings-schema.d.ts +53 -23
  26. package/dist/types/edit/hashline/params.d.ts +7 -11
  27. package/dist/types/edit/index.d.ts +2 -1
  28. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  29. package/dist/types/edit/modes/patch.d.ts +15 -24
  30. package/dist/types/edit/modes/replace.d.ts +16 -17
  31. package/dist/types/eval/js/index.d.ts +1 -0
  32. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  33. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  34. package/dist/types/extensibility/extensions/types.d.ts +6 -3
  35. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  38. package/dist/types/extensibility/typebox.d.ts +80 -58
  39. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  40. package/dist/types/index.d.ts +2 -0
  41. package/dist/types/lsp/index.d.ts +11 -26
  42. package/dist/types/lsp/types.d.ts +12 -28
  43. package/dist/types/mcp/client.d.ts +8 -0
  44. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  46. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  47. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +3 -0
  49. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  50. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  51. package/dist/types/modes/theme/theme.d.ts +1 -1
  52. package/dist/types/modes/types.d.ts +3 -0
  53. package/dist/types/sdk.d.ts +5 -0
  54. package/dist/types/session/agent-session.d.ts +4 -0
  55. package/dist/types/startup-splash.d.ts +12 -0
  56. package/dist/types/task/types.d.ts +47 -48
  57. package/dist/types/tools/ask.d.ts +26 -27
  58. package/dist/types/tools/ast-edit.d.ts +17 -17
  59. package/dist/types/tools/ast-grep.d.ts +12 -13
  60. package/dist/types/tools/bash.d.ts +20 -17
  61. package/dist/types/tools/browser.d.ts +46 -71
  62. package/dist/types/tools/checkpoint.d.ts +14 -15
  63. package/dist/types/tools/debug.d.ts +82 -145
  64. package/dist/types/tools/eval.d.ts +30 -40
  65. package/dist/types/tools/find.d.ts +17 -18
  66. package/dist/types/tools/gh.d.ts +49 -78
  67. package/dist/types/tools/image-gen.d.ts +20 -36
  68. package/dist/types/tools/inspect-image.d.ts +10 -11
  69. package/dist/types/tools/irc.d.ts +22 -33
  70. package/dist/types/tools/job.d.ts +11 -12
  71. package/dist/types/tools/learn.d.ts +21 -28
  72. package/dist/types/tools/manage-skill.d.ts +13 -22
  73. package/dist/types/tools/memory-edit.d.ts +15 -24
  74. package/dist/types/tools/memory-recall.d.ts +7 -8
  75. package/dist/types/tools/memory-reflect.d.ts +9 -10
  76. package/dist/types/tools/memory-retain.d.ts +13 -14
  77. package/dist/types/tools/read.d.ts +7 -8
  78. package/dist/types/tools/resolve.d.ts +11 -18
  79. package/dist/types/tools/review.d.ts +9 -15
  80. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  81. package/dist/types/tools/search.d.ts +16 -17
  82. package/dist/types/tools/ssh.d.ts +14 -15
  83. package/dist/types/tools/todo.d.ts +27 -43
  84. package/dist/types/tools/tts.d.ts +8 -9
  85. package/dist/types/tools/write.d.ts +9 -10
  86. package/dist/types/tui/index.d.ts +1 -0
  87. package/dist/types/tui/width-aware-text.d.ts +23 -0
  88. package/dist/types/utils/markit.d.ts +10 -1
  89. package/dist/types/web/search/index.d.ts +17 -28
  90. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  91. package/dist/types/web/search/types.d.ts +32 -26
  92. package/package.json +14 -13
  93. package/scripts/omp +1 -1
  94. package/src/advisor/__tests__/advisor.test.ts +44 -1
  95. package/src/advisor/advise-tool.ts +34 -11
  96. package/src/autoresearch/tools/init-experiment.ts +13 -16
  97. package/src/autoresearch/tools/log-experiment.ts +15 -18
  98. package/src/autoresearch/tools/run-experiment.ts +3 -3
  99. package/src/autoresearch/tools/update-notes.ts +4 -4
  100. package/src/cli/ttsr-cli.ts +995 -0
  101. package/src/cli-commands.ts +1 -0
  102. package/src/cli.ts +7 -1
  103. package/src/commands/ttsr.ts +125 -0
  104. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  105. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  106. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  107. package/src/commit/agentic/tools/git-overview.ts +4 -4
  108. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  109. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  110. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  111. package/src/commit/agentic/tools/schemas.ts +8 -20
  112. package/src/commit/agentic/tools/split-commit.ts +19 -23
  113. package/src/commit/analysis/summary.ts +7 -5
  114. package/src/commit/changelog/generate.ts +15 -11
  115. package/src/commit/shared-llm.ts +17 -24
  116. package/src/config/config-file.ts +13 -15
  117. package/src/config/keybindings.ts +6 -0
  118. package/src/config/models-config-schema.ts +206 -179
  119. package/src/config/settings-schema.ts +34 -0
  120. package/src/discovery/builtin-rules/index.ts +2 -0
  121. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  122. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  123. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  124. package/src/edit/hashline/params.ts +12 -11
  125. package/src/edit/index.ts +5 -4
  126. package/src/edit/modes/apply-patch.ts +4 -4
  127. package/src/edit/modes/patch.ts +15 -18
  128. package/src/edit/modes/replace.ts +13 -17
  129. package/src/edit/renderer.ts +0 -1
  130. package/src/eval/agent-bridge.ts +11 -13
  131. package/src/eval/completion-bridge.ts +25 -17
  132. package/src/eval/js/context-manager.ts +17 -2
  133. package/src/eval/js/index.ts +1 -1
  134. package/src/eval/py/executor.ts +2 -2
  135. package/src/extensibility/custom-commands/loader.ts +5 -3
  136. package/src/extensibility/custom-commands/types.ts +6 -3
  137. package/src/extensibility/custom-tools/loader.ts +4 -2
  138. package/src/extensibility/custom-tools/types.ts +8 -5
  139. package/src/extensibility/extensions/loader.ts +4 -2
  140. package/src/extensibility/extensions/types.ts +6 -3
  141. package/src/extensibility/hooks/loader.ts +5 -2
  142. package/src/extensibility/hooks/types.ts +7 -4
  143. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  144. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  145. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  146. package/src/extensibility/tool-proxy.ts +4 -1
  147. package/src/extensibility/typebox.ts +778 -251
  148. package/src/goals/guided-setup.ts +12 -3
  149. package/src/goals/tools/goal-tool.ts +6 -6
  150. package/src/index.ts +2 -0
  151. package/src/internal-urls/docs-index.generated.ts +11 -9
  152. package/src/lsp/types.ts +13 -27
  153. package/src/main.ts +19 -18
  154. package/src/mcp/client.ts +38 -13
  155. package/src/mcp/render.ts +102 -89
  156. package/src/modes/components/agent-hub.ts +11 -4
  157. package/src/modes/components/btw-panel.ts +5 -1
  158. package/src/modes/components/custom-editor.ts +18 -0
  159. package/src/modes/components/status-line/component.ts +8 -1
  160. package/src/modes/components/tool-execution.ts +17 -10
  161. package/src/modes/controllers/btw-controller.ts +69 -1
  162. package/src/modes/controllers/input-controller.ts +29 -0
  163. package/src/modes/interactive-mode.ts +38 -8
  164. package/src/modes/setup-wizard/index.ts +1 -0
  165. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  166. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  167. package/src/modes/theme/theme.ts +133 -143
  168. package/src/modes/types.ts +3 -0
  169. package/src/modes/utils/context-usage.ts +9 -5
  170. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  171. package/src/prompts/system/system-prompt.md +1 -0
  172. package/src/sdk.ts +21 -4
  173. package/src/session/agent-session.ts +173 -33
  174. package/src/session/session-history-format.ts +11 -2
  175. package/src/session/snapcompact-inline.ts +1 -1
  176. package/src/slash-commands/builtin-registry.ts +3 -10
  177. package/src/startup-splash.ts +19 -0
  178. package/src/task/executor.ts +11 -6
  179. package/src/task/types.ts +44 -41
  180. package/src/tool-discovery/tool-index.ts +17 -4
  181. package/src/tools/ask.ts +14 -14
  182. package/src/tools/ast-edit.ts +17 -14
  183. package/src/tools/ast-grep.ts +10 -9
  184. package/src/tools/bash.ts +15 -10
  185. package/src/tools/browser/launch.ts +13 -0
  186. package/src/tools/browser.ts +26 -32
  187. package/src/tools/checkpoint.ts +7 -7
  188. package/src/tools/debug.ts +72 -69
  189. package/src/tools/eval.ts +18 -19
  190. package/src/tools/find.ts +20 -13
  191. package/src/tools/gh.ts +29 -49
  192. package/src/tools/image-gen.ts +27 -32
  193. package/src/tools/inspect-image.ts +8 -9
  194. package/src/tools/irc.ts +12 -12
  195. package/src/tools/job.ts +6 -6
  196. package/src/tools/learn.ts +11 -14
  197. package/src/tools/manage-skill.ts +19 -23
  198. package/src/tools/memory-edit.ts +8 -8
  199. package/src/tools/memory-recall.ts +4 -4
  200. package/src/tools/memory-reflect.ts +5 -5
  201. package/src/tools/memory-retain.ts +9 -11
  202. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  203. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  204. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  205. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  206. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  207. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  208. package/src/tools/read.ts +169 -13
  209. package/src/tools/report-tool-issue.ts +6 -6
  210. package/src/tools/resolve.ts +6 -6
  211. package/src/tools/review.ts +10 -12
  212. package/src/tools/search-tool-bm25.ts +5 -5
  213. package/src/tools/search.ts +20 -29
  214. package/src/tools/ssh.ts +8 -8
  215. package/src/tools/todo.ts +16 -19
  216. package/src/tools/tts.ts +16 -15
  217. package/src/tools/write.ts +5 -5
  218. package/src/tui/index.ts +1 -0
  219. package/src/tui/width-aware-text.ts +58 -0
  220. package/src/utils/markit.ts +17 -2
  221. package/src/web/search/index.ts +9 -9
  222. package/src/web/search/providers/perplexity.ts +373 -126
  223. 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,19 +91,6 @@ 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`);
104
- }
105
-
106
94
  export function writeStartupNotice(parsedArgs: Pick<Args, "mode">, text: string): void {
107
95
  (parsedArgs.mode === "json" ? process.stderr : process.stdout).write(text);
108
96
  }
@@ -391,6 +379,7 @@ async function runInteractiveMode(
391
379
  mcpManager: MCPManager | undefined,
392
380
  resuming: boolean,
393
381
  forceSetupWizard: boolean,
382
+ showStartupSplash: boolean,
394
383
  eventBus?: EventBus,
395
384
  initialMessage?: string,
396
385
  initialImages?: ImageContent[],
@@ -411,10 +400,13 @@ async function runInteractiveMode(
411
400
  // Cold-launch gate: the full setup wizard (every scene + the overlay and
412
401
  // their TUI/OAuth/search/theme deps) is heavy, yet the common case only needs
413
402
  // to know whether the stored setup version is current. Lazy-load the wizard
414
- // 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.
415
405
  const storedSetupVersion = settings.get("setupVersion");
416
406
  const setupWizard =
417
- 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;
418
410
  const setupScenes = setupWizard
419
411
  ? await setupWizard.selectSetupScenes(storedSetupVersion, setupWizard.ALL_SCENES, mode, {
420
412
  resuming,
@@ -423,12 +415,17 @@ async function runInteractiveMode(
423
415
  force: forceSetupWizard,
424
416
  })
425
417
  : [];
418
+ const playStartupSplash = showStartupSplash && setupScenes.length === 0;
426
419
 
427
420
  await mode.init({
428
- suppressWelcomeIntro: resuming || setupScenes.length > 0,
421
+ suppressWelcomeIntro: resuming || setupScenes.length > 0 || playStartupSplash,
429
422
  clearInitialTerminalHistory: true,
430
423
  });
431
424
 
425
+ if (setupWizard && playStartupSplash) {
426
+ await setupWizard.runStartupSplash(mode);
427
+ }
428
+
432
429
  if (setupWizard && setupScenes.length > 0) {
433
430
  await setupWizard.runSetupWizard(mode, setupScenes);
434
431
  }
@@ -1261,11 +1258,14 @@ export async function runRootCommand(
1261
1258
  stdinContent: pipedInput,
1262
1259
  });
1263
1260
 
1264
- maybeShowStartupSplash({
1261
+ const showStartupSplash = shouldShowStartupSplash({
1262
+ configured: settingsInstance.get("startup.showSplash"),
1265
1263
  isInteractive,
1266
1264
  resuming: Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1267
1265
  quiet: settingsInstance.get("startup.quiet"),
1268
- version: VERSION,
1266
+ timing: Boolean($env.PI_TIMING),
1267
+ stdinIsTTY: process.stdin.isTTY,
1268
+ stdoutIsTTY: process.stdout.isTTY,
1269
1269
  });
1270
1270
 
1271
1271
  const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await createSession({
@@ -1357,6 +1357,7 @@ export async function runRootCommand(
1357
1357
  mcpManager,
1358
1358
  Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1359
1359
  deps.forceSetupWizard === true,
1360
+ showStartupSplash,
1360
1361
  eventBus,
1361
1362
  initialMessage,
1362
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 {
@@ -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":
@@ -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) {
@@ -181,6 +181,7 @@ export class StatusLineComponent implements Component {
181
181
  #cachedBranchCwd: string | undefined = undefined;
182
182
  #gitWatcher: fs.FSWatcher | null = null;
183
183
  #onBranchChange: (() => void) | null = null;
184
+ #disposed = false;
184
185
  #autoCompactEnabled: boolean = true;
185
186
  #hookStatuses: Map<string, string> = new Map();
186
187
  #subagentCount: number = 0;
@@ -329,6 +330,7 @@ export class StatusLineComponent implements Component {
329
330
 
330
331
  try {
331
332
  this.#gitWatcher = fs.watch(watchPath, () => {
333
+ if (this.#disposed) return;
332
334
  this.#invalidateGitCaches();
333
335
  if (this.#onBranchChange) {
334
336
  this.#onBranchChange();
@@ -340,6 +342,8 @@ export class StatusLineComponent implements Component {
340
342
  }
341
343
 
342
344
  dispose(): void {
345
+ this.#disposed = true;
346
+ this.#onBranchChange = null;
343
347
  if (this.#gitWatcher) {
344
348
  this.#gitWatcher.close();
345
349
  this.#gitWatcher = null;
@@ -391,6 +395,7 @@ export class StatusLineComponent implements Component {
391
395
  this.#defaultBranch = "main";
392
396
  (async () => {
393
397
  const resolved = await git.branch.default(getProjectDir());
398
+ if (this.#disposed) return;
394
399
  if (resolved) {
395
400
  this.#defaultBranch = resolved;
396
401
  if (this.#onBranchChange) {
@@ -460,6 +465,7 @@ export class StatusLineComponent implements Component {
460
465
  try {
461
466
  // Requires `gh repo set-default` to be configured; fails gracefully if not
462
467
  const result = await $`gh pr view --json number,url`.quiet().nothrow();
468
+ if (this.#disposed) return;
463
469
  if (result.exitCode !== 0) {
464
470
  setCachedPr(null);
465
471
  return;
@@ -471,10 +477,11 @@ export class StatusLineComponent implements Component {
471
477
  setCachedPr(null);
472
478
  }
473
479
  } catch {
480
+ if (this.#disposed) return;
474
481
  setCachedPr(null);
475
482
  } finally {
476
483
  this.#prLookupInFlight = false;
477
- if (this.#onBranchChange) {
484
+ if (!this.#disposed && this.#onBranchChange) {
478
485
  this.#onBranchChange();
479
486
  }
480
487
  }