@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/scripts/format-prompts.ts +1 -1
  6. package/src/autoresearch/helpers.ts +17 -0
  7. package/src/autoresearch/tools/log-experiment.ts +9 -17
  8. package/src/autoresearch/tools/run-experiment.ts +2 -17
  9. package/src/capability/skill.ts +7 -0
  10. package/src/cli/args.ts +2 -2
  11. package/src/cli/list-models.ts +1 -1
  12. package/src/cli/shell-cli.ts +3 -13
  13. package/src/cli/update-cli.ts +1 -1
  14. package/src/cli.ts +11 -29
  15. package/src/commands/acp.ts +24 -0
  16. package/src/commands/launch.ts +6 -4
  17. package/src/commit/agentic/prompts/system.md +1 -1
  18. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  19. package/src/commit/analysis/conventional.ts +8 -66
  20. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  21. package/src/commit/pipeline.ts +2 -2
  22. package/src/commit/shared-llm.ts +89 -0
  23. package/src/config/config-file.ts +210 -0
  24. package/src/config/model-equivalence.ts +8 -11
  25. package/src/config/model-registry.ts +13 -2
  26. package/src/config/model-resolver.ts +31 -4
  27. package/src/config/settings-schema.ts +102 -1
  28. package/src/config/settings.ts +1 -1
  29. package/src/config.ts +3 -219
  30. package/src/edit/index.ts +22 -1
  31. package/src/edit/modes/patch.ts +10 -0
  32. package/src/edit/modes/replace.ts +3 -0
  33. package/src/edit/renderer.ts +17 -1
  34. package/src/eval/js/context-manager.ts +1 -1
  35. package/src/eval/js/executor.ts +3 -0
  36. package/src/eval/js/shared/rewrite-imports.ts +122 -50
  37. package/src/eval/js/shared/runtime.ts +31 -4
  38. package/src/eval/js/tool-bridge.ts +43 -21
  39. package/src/eval/py/executor.ts +5 -0
  40. package/src/exa/factory.ts +2 -2
  41. package/src/exa/mcp-client.ts +74 -1
  42. package/src/exec/bash-executor.ts +5 -1
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +0 -11
  45. package/src/extensibility/extensions/runner.ts +55 -2
  46. package/src/extensibility/extensions/types.ts +98 -221
  47. package/src/extensibility/hooks/types.ts +89 -314
  48. package/src/extensibility/shared-events.ts +343 -0
  49. package/src/extensibility/skills.ts +42 -1
  50. package/src/goals/index.ts +3 -0
  51. package/src/goals/runtime.ts +500 -0
  52. package/src/goals/state.ts +37 -0
  53. package/src/goals/tools/goal-tool.ts +237 -0
  54. package/src/hashline/anchors.ts +2 -2
  55. package/src/hindsight/mental-models.ts +1 -1
  56. package/src/internal-urls/agent-protocol.ts +1 -20
  57. package/src/internal-urls/artifact-protocol.ts +1 -19
  58. package/src/internal-urls/docs-index.generated.ts +9 -10
  59. package/src/internal-urls/index.ts +1 -0
  60. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  61. package/src/internal-urls/registry-helpers.ts +25 -0
  62. package/src/internal-urls/router.ts +6 -3
  63. package/src/internal-urls/types.ts +22 -1
  64. package/src/main.ts +24 -11
  65. package/src/mcp/oauth-flow.ts +20 -0
  66. package/src/modes/acp/acp-agent.ts +412 -71
  67. package/src/modes/acp/acp-client-bridge.ts +152 -0
  68. package/src/modes/acp/acp-event-mapper.ts +180 -15
  69. package/src/modes/acp/terminal-auth.ts +37 -0
  70. package/src/modes/components/assistant-message.ts +14 -8
  71. package/src/modes/components/bash-execution.ts +24 -63
  72. package/src/modes/components/custom-message.ts +14 -40
  73. package/src/modes/components/eval-execution.ts +27 -57
  74. package/src/modes/components/execution-shared.ts +102 -0
  75. package/src/modes/components/hook-message.ts +17 -49
  76. package/src/modes/components/mcp-add-wizard.ts +26 -5
  77. package/src/modes/components/message-frame.ts +88 -0
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/read-tool-group.ts +29 -1
  80. package/src/modes/components/session-observer-overlay.ts +6 -2
  81. package/src/modes/components/session-selector.ts +1 -1
  82. package/src/modes/components/status-line/segments.ts +55 -4
  83. package/src/modes/components/status-line/types.ts +4 -0
  84. package/src/modes/components/status-line.ts +28 -10
  85. package/src/modes/components/tool-execution.ts +7 -8
  86. package/src/modes/controllers/command-controller-shared.ts +108 -0
  87. package/src/modes/controllers/command-controller.ts +27 -10
  88. package/src/modes/controllers/event-controller.ts +60 -18
  89. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  90. package/src/modes/controllers/input-controller.ts +85 -39
  91. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  92. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  93. package/src/modes/interactive-mode.ts +675 -39
  94. package/src/modes/print-mode.ts +16 -86
  95. package/src/modes/rpc/rpc-mode.ts +30 -88
  96. package/src/modes/runtime-init.ts +115 -0
  97. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  98. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  99. package/src/modes/theme/theme.ts +18 -6
  100. package/src/modes/types.ts +20 -5
  101. package/src/modes/utils/context-usage.ts +13 -13
  102. package/src/modes/utils/ui-helpers.ts +25 -6
  103. package/src/plan-mode/approved-plan.ts +35 -1
  104. package/src/prompts/agents/designer.md +5 -5
  105. package/src/prompts/agents/explore.md +7 -7
  106. package/src/prompts/agents/init.md +9 -9
  107. package/src/prompts/agents/librarian.md +14 -14
  108. package/src/prompts/agents/plan.md +4 -4
  109. package/src/prompts/agents/reviewer.md +5 -5
  110. package/src/prompts/agents/task.md +10 -10
  111. package/src/prompts/commands/orchestrate.md +2 -2
  112. package/src/prompts/compaction/branch-summary.md +3 -3
  113. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  114. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  115. package/src/prompts/compaction/compaction-summary.md +5 -5
  116. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  117. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  118. package/src/prompts/goals/goal-budget-limit.md +16 -0
  119. package/src/prompts/goals/goal-continuation.md +28 -0
  120. package/src/prompts/goals/goal-mode-active.md +23 -0
  121. package/src/prompts/memories/consolidation.md +2 -2
  122. package/src/prompts/memories/read-path.md +1 -1
  123. package/src/prompts/memories/stage_one_input.md +1 -1
  124. package/src/prompts/memories/stage_one_system.md +5 -5
  125. package/src/prompts/review-request.md +4 -4
  126. package/src/prompts/system/agent-creation-architect.md +17 -17
  127. package/src/prompts/system/agent-creation-user.md +2 -2
  128. package/src/prompts/system/commit-message-system.md +2 -2
  129. package/src/prompts/system/custom-system-prompt.md +2 -2
  130. package/src/prompts/system/eager-todo.md +6 -6
  131. package/src/prompts/system/handoff-document.md +1 -1
  132. package/src/prompts/system/plan-mode-active.md +25 -24
  133. package/src/prompts/system/plan-mode-approved.md +4 -4
  134. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  135. package/src/prompts/system/plan-mode-reference.md +2 -2
  136. package/src/prompts/system/plan-mode-subagent.md +8 -8
  137. package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
  138. package/src/prompts/system/project-prompt.md +4 -4
  139. package/src/prompts/system/subagent-system-prompt.md +7 -7
  140. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  141. package/src/prompts/system/system-prompt.md +72 -71
  142. package/src/prompts/system/ttsr-interrupt.md +1 -1
  143. package/src/prompts/tools/apply-patch.md +1 -1
  144. package/src/prompts/tools/ast-edit.md +3 -3
  145. package/src/prompts/tools/ast-grep.md +3 -3
  146. package/src/prompts/tools/bash.md +6 -0
  147. package/src/prompts/tools/browser.md +3 -3
  148. package/src/prompts/tools/checkpoint.md +3 -3
  149. package/src/prompts/tools/find.md +3 -3
  150. package/src/prompts/tools/github.md +2 -5
  151. package/src/prompts/tools/goal.md +13 -0
  152. package/src/prompts/tools/hashline.md +104 -116
  153. package/src/prompts/tools/image-gen.md +3 -3
  154. package/src/prompts/tools/irc.md +1 -1
  155. package/src/prompts/tools/lsp.md +2 -2
  156. package/src/prompts/tools/patch.md +6 -6
  157. package/src/prompts/tools/read.md +8 -7
  158. package/src/prompts/tools/replace.md +5 -5
  159. package/src/prompts/tools/resolve.md +6 -5
  160. package/src/prompts/tools/retain.md +1 -1
  161. package/src/prompts/tools/rewind.md +2 -2
  162. package/src/prompts/tools/search.md +2 -2
  163. package/src/prompts/tools/ssh.md +2 -2
  164. package/src/prompts/tools/task.md +12 -6
  165. package/src/prompts/tools/web-search.md +2 -2
  166. package/src/prompts/tools/write.md +3 -3
  167. package/src/sdk.ts +81 -17
  168. package/src/session/agent-session.ts +656 -125
  169. package/src/session/blob-store.ts +36 -3
  170. package/src/session/client-bridge.ts +81 -0
  171. package/src/session/compaction/errors.ts +31 -0
  172. package/src/session/compaction/index.ts +1 -0
  173. package/src/session/messages.ts +67 -2
  174. package/src/session/session-manager.ts +131 -12
  175. package/src/session/session-storage.ts +33 -15
  176. package/src/session/streaming-output.ts +309 -13
  177. package/src/slash-commands/acp-builtins.ts +46 -0
  178. package/src/slash-commands/builtin-registry.ts +717 -116
  179. package/src/slash-commands/helpers/context-report.ts +39 -0
  180. package/src/slash-commands/helpers/format.ts +23 -0
  181. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  182. package/src/slash-commands/helpers/mcp.ts +532 -0
  183. package/src/slash-commands/helpers/parse.ts +85 -0
  184. package/src/slash-commands/helpers/ssh.ts +193 -0
  185. package/src/slash-commands/helpers/todo.ts +279 -0
  186. package/src/slash-commands/helpers/usage-report.ts +91 -0
  187. package/src/slash-commands/types.ts +126 -0
  188. package/src/ssh/ssh-executor.ts +5 -0
  189. package/src/system-prompt.ts +4 -2
  190. package/src/task/executor.ts +27 -10
  191. package/src/task/index.ts +20 -1
  192. package/src/task/render.ts +27 -18
  193. package/src/task/types.ts +4 -0
  194. package/src/tools/ast-edit.ts +21 -120
  195. package/src/tools/ast-grep.ts +21 -119
  196. package/src/tools/bash-interactive.ts +9 -1
  197. package/src/tools/bash.ts +203 -6
  198. package/src/tools/browser/attach.ts +3 -3
  199. package/src/tools/browser/launch.ts +81 -18
  200. package/src/tools/browser/registry.ts +1 -5
  201. package/src/tools/browser/tab-supervisor.ts +51 -14
  202. package/src/tools/conflict-detect.ts +21 -10
  203. package/src/tools/eval.ts +3 -1
  204. package/src/tools/fetch.ts +15 -4
  205. package/src/tools/find.ts +39 -39
  206. package/src/tools/gh-renderer.ts +0 -12
  207. package/src/tools/gh.ts +689 -182
  208. package/src/tools/github-cache.ts +548 -0
  209. package/src/tools/index.ts +25 -11
  210. package/src/tools/inspect-image.ts +3 -10
  211. package/src/tools/output-meta.ts +176 -37
  212. package/src/tools/path-utils.ts +125 -2
  213. package/src/tools/read.ts +605 -239
  214. package/src/tools/render-utils.ts +92 -0
  215. package/src/tools/renderers.ts +2 -0
  216. package/src/tools/resolve.ts +72 -44
  217. package/src/tools/search.ts +120 -186
  218. package/src/tools/write.ts +67 -10
  219. package/src/tui/code-cell.ts +70 -2
  220. package/src/utils/file-mentions.ts +1 -1
  221. package/src/utils/image-loading.ts +7 -3
  222. package/src/utils/image-resize.ts +32 -43
  223. package/src/vim/parser.ts +0 -17
  224. package/src/vim/render.ts +1 -1
  225. package/src/vim/types.ts +1 -1
  226. package/src/web/search/providers/gemini.ts +35 -95
  227. package/src/prompts/tools/exit-plan-mode.md +0 -6
  228. package/src/tools/exit-plan-mode.ts +0 -97
  229. package/src/utils/fuzzy.ts +0 -108
  230. package/src/utils/image-convert.ts +0 -27
@@ -1,9 +1,9 @@
1
- import type { TextContent } from "@oh-my-pi/pi-ai";
2
1
  import type { Component } from "@oh-my-pi/pi-tui";
3
- import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
2
+ import { Box, Container, Spacer } from "@oh-my-pi/pi-tui";
4
3
  import type { MessageRenderer } from "../../extensibility/extensions/types";
5
- import { getMarkdownTheme, theme } from "../../modes/theme/theme";
4
+ import { theme } from "../../modes/theme/theme";
6
5
  import type { CustomMessage } from "../../session/messages";
6
+ import { renderFramedMessage } from "./message-frame";
7
7
 
8
8
  /**
9
9
  * Component that renders a custom message entry from extensions.
@@ -41,51 +41,25 @@ export class CustomMessageComponent extends Container {
41
41
  }
42
42
 
43
43
  #rebuild(): void {
44
- // Remove previous content component
45
44
  if (this.#customComponent) {
46
45
  this.removeChild(this.#customComponent);
47
46
  this.#customComponent = undefined;
48
47
  }
49
48
  this.removeChild(this.#box);
50
49
 
51
- // Try custom renderer first - it handles its own styling
52
- if (this.customRenderer) {
53
- try {
54
- const component = this.customRenderer(this.message, { expanded: this.#expanded }, theme);
55
- if (component) {
56
- this.#customComponent = component;
57
- this.addChild(component);
58
- return;
59
- }
60
- } catch {
61
- // Fall through to default rendering
62
- }
63
- }
64
-
65
- // Default rendering uses our box
66
- this.addChild(this.#box);
67
- this.#box.clear();
50
+ const custom = renderFramedMessage({
51
+ message: this.message,
52
+ box: this.#box,
53
+ expanded: this.#expanded,
54
+ customRenderer: this.customRenderer,
55
+ // Extension messages render full content; no collapse-on-fold behaviour.
56
+ });
68
57
 
69
- // Default rendering: label + content
70
- const label = theme.fg("customMessageLabel", theme.bold(`[${this.message.customType}]`));
71
- this.#box.addChild(new Text(label, 0, 0));
72
- this.#box.addChild(new Spacer(1));
73
-
74
- // Extract text content
75
- let text: string;
76
- if (typeof this.message.content === "string") {
77
- text = this.message.content;
58
+ if (custom) {
59
+ this.#customComponent = custom;
60
+ this.addChild(custom);
78
61
  } else {
79
- text = this.message.content
80
- .filter((c): c is TextContent => c.type === "text")
81
- .map(c => c.text)
82
- .join("\n");
62
+ this.addChild(this.#box);
83
63
  }
84
-
85
- this.#box.addChild(
86
- new Markdown(text, 0, 0, getMarkdownTheme(), {
87
- color: (value: string) => theme.fg("customMessageText", value),
88
- }),
89
- );
90
64
  }
91
65
  }
@@ -4,11 +4,17 @@
4
4
  */
5
5
 
6
6
  import { sanitizeText } from "@oh-my-pi/pi-natives";
7
- import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
8
- import { getSymbolTheme, highlightCode, theme } from "../../modes/theme/theme";
9
- import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
10
- import { DynamicBorder } from "./dynamic-border";
11
- import { truncateToVisualLines } from "./visual-truncate";
7
+ import { Container, type Loader, Text, type TUI } from "@oh-my-pi/pi-tui";
8
+ import { highlightCode, theme } from "../../modes/theme/theme";
9
+ import type { TruncationMeta } from "../../tools/output-meta";
10
+ import {
11
+ buildExecutionFrame,
12
+ buildStatusFooter,
13
+ createCollapsedPreview,
14
+ type ExecutionColorKey,
15
+ type ExecutionStatus,
16
+ resolveExecutionStatus,
17
+ } from "./execution-shared";
12
18
 
13
19
  const PREVIEW_LINES = 20;
14
20
  const MAX_DISPLAY_LINE_CHARS = 4000;
@@ -17,7 +23,7 @@ export type EvalExecutionLanguage = "python" | "js";
17
23
 
18
24
  export class EvalExecutionComponent extends Container {
19
25
  #outputLines: string[] = [];
20
- #status: "running" | "complete" | "cancelled" | "error" = "running";
26
+ #status: ExecutionStatus = "running";
21
27
  #exitCode: number | undefined = undefined;
22
28
  #loader: Loader;
23
29
  #truncation?: TruncationMeta;
@@ -28,7 +34,7 @@ export class EvalExecutionComponent extends Container {
28
34
  return this.language === "js" ? "javascript" : "python";
29
35
  }
30
36
 
31
- #formatHeader(colorKey: "dim" | "pythonMode"): Text {
37
+ #formatHeader(colorKey: ExecutionColorKey): Text {
32
38
  const prompt = theme.fg(colorKey, theme.bold(">>>"));
33
39
  const continuation = theme.fg(colorKey, " ");
34
40
  const codeLines = highlightCode(this.code, this.#highlightLang());
@@ -46,26 +52,13 @@ export class EvalExecutionComponent extends Container {
46
52
  ) {
47
53
  super();
48
54
 
49
- const colorKey = this.excludeFromContext ? "dim" : "pythonMode";
50
- const borderColor = (str: string) => theme.fg(colorKey, str);
55
+ const colorKey: ExecutionColorKey = this.excludeFromContext ? "dim" : "pythonMode";
56
+ const { contentContainer, loader } = buildExecutionFrame(this, ui, colorKey);
57
+ this.#contentContainer = contentContainer;
58
+ this.#loader = loader;
51
59
 
52
- this.addChild(new Spacer(1));
53
- this.addChild(new DynamicBorder(borderColor));
54
-
55
- this.#contentContainer = new Container();
56
- this.addChild(this.#contentContainer);
57
60
  this.#contentContainer.addChild(this.#formatHeader(colorKey));
58
-
59
- this.#loader = new Loader(
60
- ui,
61
- spinner => theme.fg(colorKey, spinner),
62
- text => theme.fg("muted", text),
63
- `Running… (esc to cancel)`,
64
- getSymbolTheme().spinnerFrames,
65
- );
66
61
  this.#contentContainer.addChild(this.#loader);
67
-
68
- this.addChild(new DynamicBorder(borderColor));
69
62
  }
70
63
 
71
64
  setExpanded(expanded: boolean): void {
@@ -99,11 +92,7 @@ export class EvalExecutionComponent extends Container {
99
92
  options?: { output?: string; truncation?: TruncationMeta },
100
93
  ): void {
101
94
  this.#exitCode = exitCode;
102
- this.#status = cancelled
103
- ? "cancelled"
104
- : exitCode !== 0 && exitCode !== undefined && exitCode !== null
105
- ? "error"
106
- : "complete";
95
+ this.#status = resolveExecutionStatus(exitCode, cancelled);
107
96
  this.#truncation = options?.truncation;
108
97
  if (options?.output !== undefined) {
109
98
  this.#setOutput(options.output);
@@ -120,7 +109,7 @@ export class EvalExecutionComponent extends Container {
120
109
 
121
110
  this.#contentContainer.clear();
122
111
 
123
- const colorKey = this.excludeFromContext ? "dim" : "pythonMode";
112
+ const colorKey: ExecutionColorKey = this.excludeFromContext ? "dim" : "pythonMode";
124
113
  this.#contentContainer.addChild(this.#formatHeader(colorKey));
125
114
 
126
115
  if (availableLines.length > 0) {
@@ -129,39 +118,20 @@ export class EvalExecutionComponent extends Container {
129
118
  this.#contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
130
119
  } else {
131
120
  const styledOutput = previewLogicalLines.map(line => theme.fg("muted", line)).join("\n");
132
- const previewText = `\n${styledOutput}`;
133
- this.#contentContainer.addChild({
134
- render: (width: number) => {
135
- const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
136
- return visualLines;
137
- },
138
- invalidate: () => {},
139
- });
121
+ this.#contentContainer.addChild(createCollapsedPreview(`\n${styledOutput}`, PREVIEW_LINES));
140
122
  }
141
123
  }
142
124
 
143
125
  if (this.#status === "running") {
144
126
  this.#contentContainer.addChild(this.#loader);
145
127
  } else {
146
- const statusParts: string[] = [];
147
-
148
- if (hiddenLineCount > 0) {
149
- statusParts.push(theme.fg("dim", `… ${hiddenLineCount} more lines (ctrl+o to expand)`));
150
- }
151
-
152
- if (this.#status === "cancelled") {
153
- statusParts.push(theme.fg("warning", "(cancelled)"));
154
- } else if (this.#status === "error") {
155
- statusParts.push(theme.fg("error", `(exit ${this.#exitCode})`));
156
- }
157
-
158
- if (this.#truncation) {
159
- statusParts.push(theme.fg("warning", formatTruncationMetaNotice(this.#truncation)));
160
- }
161
-
162
- if (statusParts.length > 0) {
163
- this.#contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, 1, 0));
164
- }
128
+ const footer = buildStatusFooter({
129
+ status: this.#status,
130
+ exitCode: this.#exitCode,
131
+ truncation: this.#truncation,
132
+ hiddenLineCount,
133
+ });
134
+ if (footer) this.#contentContainer.addChild(footer);
165
135
  }
166
136
  }
167
137
 
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Shared rendering primitives for bash/eval execution components.
3
+ *
4
+ * Each helper isolates a piece of structure both components share verbatim
5
+ * (frame layout, collapsed preview, post-run status line). Differences in
6
+ * how each component prepares its header, output lines, or sixel masking
7
+ * stay in their respective files.
8
+ */
9
+
10
+ import { type Component, Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
11
+ import { getSymbolTheme, theme } from "../../modes/theme/theme";
12
+ import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
13
+ import { DynamicBorder } from "./dynamic-border";
14
+ import { truncateToVisualLines } from "./visual-truncate";
15
+
16
+ export type ExecutionStatus = "running" | "complete" | "cancelled" | "error";
17
+
18
+ /** Theme color keys valid for an execution frame. */
19
+ export type ExecutionColorKey = "dim" | "bashMode" | "pythonMode";
20
+
21
+ /**
22
+ * Build the spacer + top border + content container + bottom border scaffold
23
+ * that bash and eval execution components share. The caller appends the
24
+ * header (command vs `>>>` prompt) and the returned loader to
25
+ * `contentContainer` so per-mode order is preserved.
26
+ */
27
+ export function buildExecutionFrame(
28
+ parent: Container,
29
+ ui: TUI,
30
+ colorKey: ExecutionColorKey,
31
+ ): { contentContainer: Container; loader: Loader } {
32
+ const borderColor = (str: string) => theme.fg(colorKey, str);
33
+
34
+ parent.addChild(new Spacer(1));
35
+ parent.addChild(new DynamicBorder(borderColor));
36
+
37
+ const contentContainer = new Container();
38
+ parent.addChild(contentContainer);
39
+
40
+ const loader = new Loader(
41
+ ui,
42
+ spinner => theme.fg(colorKey, spinner),
43
+ text => theme.fg("muted", text),
44
+ `Running… (esc to cancel)`,
45
+ getSymbolTheme().spinnerFrames,
46
+ );
47
+
48
+ parent.addChild(new DynamicBorder(borderColor));
49
+ return { contentContainer, loader };
50
+ }
51
+
52
+ /**
53
+ * Wrap a styled preview block in a render-time visual-line truncator.
54
+ * Recomputed per render width so wrapping stays in sync with terminal size.
55
+ */
56
+ export function createCollapsedPreview(previewText: string, previewLines: number): Component {
57
+ return {
58
+ render: (width: number) => truncateToVisualLines(previewText, previewLines, width, 1).visualLines,
59
+ invalidate: () => {},
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Build the post-run status block (hidden-line hint, exit/cancel marker,
65
+ * truncation notice). Returns undefined when there is nothing to display so
66
+ * callers can skip appending a stray Text child.
67
+ */
68
+ export function buildStatusFooter(opts: {
69
+ status: ExecutionStatus;
70
+ exitCode: number | undefined;
71
+ truncation: TruncationMeta | undefined;
72
+ hiddenLineCount: number;
73
+ /** Suppress the "… N more lines" hint (used when sixel passthrough renders the full output). */
74
+ suppressHiddenCount?: boolean;
75
+ }): Text | undefined {
76
+ const parts: string[] = [];
77
+
78
+ if (opts.hiddenLineCount > 0 && !opts.suppressHiddenCount) {
79
+ parts.push(theme.fg("dim", `… ${opts.hiddenLineCount} more lines (ctrl+o to expand)`));
80
+ }
81
+ if (opts.status === "cancelled") {
82
+ parts.push(theme.fg("warning", "(cancelled)"));
83
+ } else if (opts.status === "error") {
84
+ parts.push(theme.fg("error", `(exit ${opts.exitCode})`));
85
+ }
86
+ if (opts.truncation) {
87
+ parts.push(theme.fg("warning", formatTruncationMetaNotice(opts.truncation)));
88
+ }
89
+
90
+ if (parts.length === 0) return undefined;
91
+ return new Text(`\n${parts.join("\n")}`, 1, 0);
92
+ }
93
+
94
+ /**
95
+ * Derive the post-run status from an exit code + cancellation flag using the
96
+ * same precedence both execution components apply.
97
+ */
98
+ export function resolveExecutionStatus(exitCode: number | undefined, cancelled: boolean): ExecutionStatus {
99
+ if (cancelled) return "cancelled";
100
+ if (exitCode !== 0 && exitCode !== undefined && exitCode !== null) return "error";
101
+ return "complete";
102
+ }
@@ -1,9 +1,12 @@
1
- import type { TextContent } from "@oh-my-pi/pi-ai";
2
1
  import type { Component } from "@oh-my-pi/pi-tui";
3
- import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
2
+ import { Box, Container, Spacer } from "@oh-my-pi/pi-tui";
4
3
  import type { HookMessageRenderer } from "../../extensibility/hooks/types";
5
- import { getMarkdownTheme, theme } from "../../modes/theme/theme";
4
+ import { theme } from "../../modes/theme/theme";
6
5
  import type { HookMessage } from "../../session/messages";
6
+ import { renderFramedMessage } from "./message-frame";
7
+
8
+ /** Lines of default markdown body shown before the "…" fold when collapsed. */
9
+ const HOOK_COLLAPSED_LINES = 5;
7
10
 
8
11
  /**
9
12
  * Component that renders a custom message entry from hooks.
@@ -41,60 +44,25 @@ export class HookMessageComponent extends Container {
41
44
  }
42
45
 
43
46
  #rebuild(): void {
44
- // Remove previous content component
45
47
  if (this.#customComponent) {
46
48
  this.removeChild(this.#customComponent);
47
49
  this.#customComponent = undefined;
48
50
  }
49
51
  this.removeChild(this.#box);
50
52
 
51
- // Try custom renderer first - it handles its own styling
52
- if (this.customRenderer) {
53
- try {
54
- const component = this.customRenderer(this.message, { expanded: this.#expanded }, theme);
55
- if (component) {
56
- // Custom renderer provides its own styled component
57
- this.#customComponent = component;
58
- this.addChild(component);
59
- return;
60
- }
61
- } catch {
62
- // Fall through to default rendering
63
- }
64
- }
65
-
66
- // Default rendering uses our box
67
- this.addChild(this.#box);
68
- this.#box.clear();
69
-
70
- // Default rendering: label + content
71
- const label = theme.fg("customMessageLabel", theme.bold(`[${this.message.customType}]`));
72
- this.#box.addChild(new Text(label, 0, 0));
73
- this.#box.addChild(new Spacer(1));
53
+ const custom = renderFramedMessage({
54
+ message: this.message,
55
+ box: this.#box,
56
+ expanded: this.#expanded,
57
+ customRenderer: this.customRenderer,
58
+ collapseAfterLines: HOOK_COLLAPSED_LINES,
59
+ });
74
60
 
75
- // Extract text content
76
- let text: string;
77
- if (typeof this.message.content === "string") {
78
- text = this.message.content;
61
+ if (custom) {
62
+ this.#customComponent = custom;
63
+ this.addChild(custom);
79
64
  } else {
80
- text = this.message.content
81
- .filter((c): c is TextContent => c.type === "text")
82
- .map(c => c.text)
83
- .join("\n");
65
+ this.addChild(this.#box);
84
66
  }
85
-
86
- // Limit lines when collapsed
87
- if (!this.#expanded) {
88
- const lines = text.split("\n");
89
- if (lines.length > 5) {
90
- text = `${lines.slice(0, 5).join("\n")}\n…`;
91
- }
92
- }
93
-
94
- this.#box.addChild(
95
- new Markdown(text, 0, 0, getMarkdownTheme(), {
96
- color: (text: string) => theme.fg("customMessageText", text),
97
- }),
98
- );
99
67
  }
100
68
  }
@@ -47,6 +47,18 @@ type WizardStep =
47
47
  | "scope"
48
48
  | "confirm";
49
49
 
50
+ /**
51
+ * Result of the wizard's OAuth callback. `credentialId` is mandatory;
52
+ * `clientId`/`clientSecret` are populated when the OAuth provider performed
53
+ * dynamic client registration (or when the caller pre-supplied them) so the
54
+ * wizard can fold them into the final `mcp.json` entry for refresh.
55
+ */
56
+ export interface MCPAddWizardOAuthResult {
57
+ credentialId: string;
58
+ clientId?: string;
59
+ clientSecret?: string;
60
+ }
61
+
50
62
  interface WizardState {
51
63
  name: string;
52
64
  transport: TransportType | null;
@@ -104,7 +116,13 @@ export class MCPAddWizard extends Container {
104
116
  #onCompleteCallback: (name: string, config: MCPServerConfig, scope: Scope) => void;
105
117
  #onCancelCallback: () => void;
106
118
  #onOAuthCallback:
107
- | ((authUrl: string, tokenUrl: string, clientId: string, clientSecret: string, scopes: string) => Promise<string>)
119
+ | ((
120
+ authUrl: string,
121
+ tokenUrl: string,
122
+ clientId: string,
123
+ clientSecret: string,
124
+ scopes: string,
125
+ ) => Promise<MCPAddWizardOAuthResult>)
108
126
  | null = null;
109
127
  #onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
110
128
  #onRenderCallback: (() => void) | null = null;
@@ -118,7 +136,7 @@ export class MCPAddWizard extends Container {
118
136
  clientId: string,
119
137
  clientSecret: string,
120
138
  scopes: string,
121
- ) => Promise<string>,
139
+ ) => Promise<MCPAddWizardOAuthResult>,
122
140
  onTestConnection?: (config: MCPServerConfig) => Promise<void>,
123
141
  onRender?: () => void,
124
142
  initialName?: string,
@@ -1120,7 +1138,7 @@ export class MCPAddWizard extends Container {
1120
1138
 
1121
1139
  try {
1122
1140
  // Call OAuth handler
1123
- const credentialId = await this.#onOAuthCallback(
1141
+ const oauthResult = await this.#onOAuthCallback(
1124
1142
  this.#state.oauthAuthUrl,
1125
1143
  this.#state.oauthTokenUrl,
1126
1144
  this.#state.oauthClientId,
@@ -1128,8 +1146,11 @@ export class MCPAddWizard extends Container {
1128
1146
  this.#state.oauthScopes,
1129
1147
  );
1130
1148
 
1131
- // Store credential ID
1132
- this.#state.oauthCredentialId = credentialId;
1149
+ // Store credential ID + any dynamically-registered client credentials,
1150
+ // so the final mcp.json entry persists everything needed for refresh.
1151
+ this.#state.oauthCredentialId = oauthResult.credentialId;
1152
+ if (oauthResult.clientId) this.#state.oauthClientId = oauthResult.clientId;
1153
+ if (oauthResult.clientSecret) this.#state.oauthClientSecret = oauthResult.clientSecret;
1133
1154
 
1134
1155
  // Show success message
1135
1156
  this.#contentContainer.clear();
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Shared rendering for extension/hook custom message frames.
3
+ *
4
+ * Both `CustomMessageComponent` and `HookMessageComponent` wrap a
5
+ * `Spacer(1) + Box` layout, try a user-supplied renderer first, and fall
6
+ * back to a label + markdown body when the renderer returns nothing or
7
+ * throws. The only meaningful difference is that hook messages collapse to
8
+ * the first N lines when not expanded; extension messages render in full.
9
+ */
10
+
11
+ import type { TextContent } from "@oh-my-pi/pi-ai";
12
+ import type { Box, Component } from "@oh-my-pi/pi-tui";
13
+ import { Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
14
+ import { getMarkdownTheme, type Theme, theme } from "../../modes/theme/theme";
15
+
16
+ /** Message shape consumed by the shared frame. */
17
+ export interface FramedMessage {
18
+ customType: string;
19
+ content: string | (TextContent | { type: string })[];
20
+ }
21
+
22
+ /**
23
+ * Callable signature shared by `MessageRenderer` (extensions) and
24
+ * `HookMessageRenderer` (hooks). Both narrow `message` to their own type;
25
+ * this signature is the structural intersection callers can hand off here.
26
+ */
27
+ export type FramedRenderer<M extends FramedMessage> = (
28
+ message: M,
29
+ options: { expanded: boolean },
30
+ theme: Theme,
31
+ ) => Component | undefined;
32
+
33
+ export interface RebuildFrameOptions<M extends FramedMessage> {
34
+ message: M;
35
+ box: Box;
36
+ expanded: boolean;
37
+ /** Collapse the markdown body to this many lines when `expanded` is false. Omit to never collapse. */
38
+ collapseAfterLines?: number;
39
+ customRenderer?: FramedRenderer<M>;
40
+ }
41
+
42
+ /**
43
+ * Attempt the custom renderer; on failure or undefined return, populate
44
+ * `box` with the default `[customType]` label + markdown body and return
45
+ * undefined. When the custom renderer succeeds, return its Component so the
46
+ * caller can mount it and skip the default box.
47
+ */
48
+ export function renderFramedMessage<M extends FramedMessage>(opts: RebuildFrameOptions<M>): Component | undefined {
49
+ if (opts.customRenderer) {
50
+ try {
51
+ const component = opts.customRenderer(opts.message, { expanded: opts.expanded }, theme);
52
+ if (component) return component;
53
+ } catch {
54
+ // Fall through to default rendering
55
+ }
56
+ }
57
+
58
+ opts.box.clear();
59
+
60
+ const label = theme.fg("customMessageLabel", theme.bold(`[${opts.message.customType}]`));
61
+ opts.box.addChild(new Text(label, 0, 0));
62
+ opts.box.addChild(new Spacer(1));
63
+
64
+ let text: string;
65
+ if (typeof opts.message.content === "string") {
66
+ text = opts.message.content;
67
+ } else {
68
+ text = opts.message.content
69
+ .filter((c): c is TextContent => c.type === "text")
70
+ .map(c => c.text)
71
+ .join("\n");
72
+ }
73
+
74
+ if (!opts.expanded && opts.collapseAfterLines !== undefined) {
75
+ const lines = text.split("\n");
76
+ if (lines.length > opts.collapseAfterLines) {
77
+ text = `${lines.slice(0, opts.collapseAfterLines).join("\n")}\n…`;
78
+ }
79
+ }
80
+
81
+ opts.box.addChild(
82
+ new Markdown(text, 0, 0, getMarkdownTheme(), {
83
+ color: (value: string) => theme.fg("customMessageText", value),
84
+ }),
85
+ );
86
+
87
+ return undefined;
88
+ }
@@ -2,6 +2,7 @@ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import { getSupportedEfforts, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
3
3
  import {
4
4
  Container,
5
+ fuzzyFilter,
5
6
  getKeybindings,
6
7
  Input,
7
8
  matchesKey,
@@ -18,7 +19,6 @@ import { resolveModelRoleValue } from "../../config/model-resolver";
18
19
  import type { Settings } from "../../config/settings";
19
20
  import { type ThemeColor, theme } from "../../modes/theme/theme";
20
21
  import { getThinkingLevelMetadata } from "../../thinking";
21
- import { fuzzyFilter } from "../../utils/fuzzy";
22
22
  import { getTabBarTheme } from "../shared";
23
23
  import { DynamicBorder } from "./dynamic-border";
24
24
 
@@ -1,10 +1,38 @@
1
1
  import type { Component } from "@oh-my-pi/pi-tui";
2
2
  import { Container, Text } from "@oh-my-pi/pi-tui";
3
+ import { InternalUrlRouter } from "../../internal-urls";
3
4
  import { getLanguageFromPath, theme } from "../../modes/theme/theme";
5
+ import { splitPathAndSel } from "../../tools/path-utils";
4
6
  import { PREVIEW_LIMITS, shortenPath } from "../../tools/render-utils";
5
7
  import { renderCodeCell } from "../../tui";
6
8
  import type { ToolExecutionHandle } from "./tool-execution";
7
9
 
10
+ /**
11
+ * Read calls whose target is resolved through {@link InternalUrlRouter} are
12
+ * rendered as full tool executions (not collapsed into the read group) so the
13
+ * resolved content is visible. `path` is the canonical arg; `file_path` is the
14
+ * legacy alias still tolerated by the read tool schema.
15
+ */
16
+ function readArgsTarget(args: unknown): string | undefined {
17
+ if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
18
+ const record = args as Record<string, unknown>;
19
+ return typeof record.path === "string"
20
+ ? record.path
21
+ : typeof record.file_path === "string"
22
+ ? record.file_path
23
+ : undefined;
24
+ }
25
+
26
+ export function readArgsHaveTarget(args: unknown): boolean {
27
+ return readArgsTarget(args) !== undefined;
28
+ }
29
+
30
+ export function readArgsTargetInternalUrl(args: unknown): boolean {
31
+ const target = readArgsTarget(args);
32
+ if (!target) return false;
33
+ return InternalUrlRouter.instance().canHandle(target);
34
+ }
35
+
8
36
  type ReadRenderArgs = {
9
37
  path?: string;
10
38
  file_path?: string;
@@ -174,7 +202,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
174
202
  * When expanded: shows full content.
175
203
  */
176
204
  #addContentPreview(entry: ReadEntry): void {
177
- const lang = getLanguageFromPath(entry.path);
205
+ const lang = getLanguageFromPath(splitPathAndSel(entry.path).path);
178
206
  const filePath = shortenPath(entry.path);
179
207
  const correctionSuffix = entry.correctedFrom ? ` (corrected from ${shortenPath(entry.correctedFrom)})` : "";
180
208
  const title = filePath ? `Read ${filePath}${correctionSuffix}` : "Read";
@@ -18,6 +18,7 @@ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
18
18
  import { Container, Markdown, type MarkdownTheme, matchesKey } from "@oh-my-pi/pi-tui";
19
19
  import { formatDuration, formatNumber, logger } from "@oh-my-pi/pi-utils";
20
20
  import type { KeyId } from "../../config/keybindings";
21
+ import { isSilentAbort } from "../../session/messages";
21
22
  import type { SessionMessageEntry } from "../../session/session-manager";
22
23
  import { parseSessionEntries } from "../../session/session-manager";
23
24
  import { PREVIEW_LIMITS, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
@@ -267,7 +268,10 @@ export class SessionObserverOverlayComponent extends Container {
267
268
  if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
268
269
  if (progress.tokens > 0) stats.push(`${formatNumber(progress.tokens)} tokens`);
269
270
  if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
270
- return stats.length > 0 ? theme.fg("dim", stats.join(theme.sep.dot)) : "";
271
+ const parts: string[] = [];
272
+ if (stats.length > 0) parts.push(theme.fg("dim", stats.join(theme.sep.dot)));
273
+ if (progress.cost > 0) parts.push(theme.fg("statusLineCost", `$${progress.cost.toFixed(2)}`));
274
+ return parts.join(theme.sep.dot);
271
275
  }
272
276
 
273
277
  #buildTranscriptLines(messageEntries: SessionMessageEntry[], lines: string[]): void {
@@ -285,7 +289,7 @@ export class SessionObserverOverlayComponent extends Container {
285
289
 
286
290
  if (msg.role === "assistant") {
287
291
  // Handle error messages with empty content
288
- if (msg.content.length === 0 && msg.errorMessage) {
292
+ if (msg.content.length === 0 && msg.errorMessage && !isSilentAbort(msg.errorMessage)) {
289
293
  const startLine = lines.length;
290
294
  const isSelected = entryIndex === this.#selectedEntryIndex;
291
295
  const cursor = isSelected ? theme.fg("accent", "▶") : " ";
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type Component,
3
3
  Container,
4
+ fuzzyFilter,
4
5
  Input,
5
6
  matchesKey,
6
7
  padding,
@@ -14,7 +15,6 @@ import { formatBytes } from "@oh-my-pi/pi-utils";
14
15
  import { theme } from "../../modes/theme/theme";
15
16
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
16
17
  import type { SessionInfo } from "../../session/session-manager";
17
- import { fuzzyFilter } from "../../utils/fuzzy";
18
18
  import { DynamicBorder } from "./dynamic-border";
19
19
  import { HookSelectorComponent } from "./hook-selector";
20
20