@oh-my-pi/pi-coding-agent 8.2.2 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/package.json +6 -6
  3. package/src/config/model-registry.ts +8 -0
  4. package/src/discovery/helpers.ts +12 -2
  5. package/src/extensibility/extensions/runner.ts +1 -0
  6. package/src/extensibility/extensions/types.ts +3 -0
  7. package/src/extensibility/extensions/wrapper.ts +4 -0
  8. package/src/extensibility/hooks/tool-wrapper.ts +4 -0
  9. package/src/ipy/executor.ts +4 -0
  10. package/src/lsp/index.ts +6 -4
  11. package/src/lsp/render.ts +115 -19
  12. package/src/lsp/types.ts +1 -0
  13. package/src/modes/components/tool-execution.ts +22 -6
  14. package/src/modes/controllers/event-controller.ts +1 -0
  15. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  16. package/src/modes/controllers/input-controller.ts +2 -2
  17. package/src/modes/interactive-mode.ts +30 -0
  18. package/src/modes/rpc/rpc-mode.ts +4 -0
  19. package/src/modes/theme/theme.ts +10 -107
  20. package/src/modes/types.ts +2 -0
  21. package/src/patch/applicator.ts +209 -41
  22. package/src/patch/fuzzy.ts +148 -28
  23. package/src/patch/index.ts +25 -2
  24. package/src/patch/parser.ts +5 -0
  25. package/src/patch/types.ts +25 -0
  26. package/src/sdk.ts +6 -0
  27. package/src/session/agent-session.ts +1 -0
  28. package/src/session/session-manager.ts +3 -0
  29. package/src/task/agents.ts +1 -1
  30. package/src/task/executor.ts +49 -17
  31. package/src/task/index.ts +16 -3
  32. package/src/task/types.ts +3 -3
  33. package/src/task/worker-protocol.ts +8 -0
  34. package/src/task/worker.ts +11 -0
  35. package/src/tools/bash.ts +1 -0
  36. package/src/tools/fetch.ts +1 -0
  37. package/src/tools/index.ts +19 -1
  38. package/src/tools/write.ts +82 -83
  39. package/src/tui/output-block.ts +26 -13
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [8.4.0] - 2026-01-25
6
+
7
+ ### Added
8
+ - Added extension API to set working/loading messages during streaming
9
+ - Added task worker propagation of context files, skills, and prompt templates
10
+ - Added subagent option to skip Python preflight checks when Python tooling is unused
11
+ - Model field now accepts string arrays for fallback model prioritization
12
+
13
+ ### Changed
14
+ - Merged patch application warnings into edit tool diagnostics output
15
+ - Cached Python prelude docs for subagent workers to avoid repeated warmups
16
+ - Simplified image placeholders inserted on paste to match Claude-style markers
17
+
18
+ ### Fixed
19
+ - Rewrote empty or corrupted session files to restore valid headers
20
+ - Improved patch applicator ambiguity errors with match previews and overlap detection
21
+ - Fixed Task tool agent model resolution to honor comma-separated model lists
22
+ ## [8.3.0] - 2026-01-25
23
+
24
+ ### Changed
25
+ - Added request parameter tracking to LSP tool rendering for better diagnostics visibility
26
+ - Added async diff computation and Kitty protocol support to tool execution rendering
27
+ - Refactored patch applicator with improved fuzzy matching (7-pass sequence matching with Levenshtein distance) and indentation adjustment
28
+ - Added inline rendering flag to bash and fetch tool renderers
29
+ - Extracted constants for preview formatting to improve code maintainability
30
+ - Exposed mergeCallAndResult and inline rendering options from tools to their wrappers
31
+ - Added timeout validation and normalization for tool timeout parameters
32
+
33
+ ### Fixed
34
+ - Fixed output block border rendering (bottom-right corner was missing)
35
+ - Added background control parameter to output block rendering
5
36
  ## [8.2.2] - 2026-01-24
6
37
 
7
38
  ### Removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.2.2",
3
+ "version": "8.4.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -75,11 +75,11 @@
75
75
  "test": "bun test"
76
76
  },
77
77
  "dependencies": {
78
- "@oh-my-pi/omp-stats": "8.2.2",
79
- "@oh-my-pi/pi-agent-core": "8.2.2",
80
- "@oh-my-pi/pi-ai": "8.2.2",
81
- "@oh-my-pi/pi-tui": "8.2.2",
82
- "@oh-my-pi/pi-utils": "8.2.2",
78
+ "@oh-my-pi/omp-stats": "8.4.0",
79
+ "@oh-my-pi/pi-agent-core": "8.4.0",
80
+ "@oh-my-pi/pi-ai": "8.4.0",
81
+ "@oh-my-pi/pi-tui": "8.4.0",
82
+ "@oh-my-pi/pi-utils": "8.4.0",
83
83
  "@openai/agents": "^0.4.3",
84
84
  "@sinclair/typebox": "^0.34.46",
85
85
  "ajv": "^8.17.1",
@@ -19,12 +19,18 @@ import type { AuthStorage } from "../session/auth-storage";
19
19
 
20
20
  const Ajv = (AjvModule as any).default || AjvModule;
21
21
 
22
+ const OpenRouterRoutingSchema = Type.Object({
23
+ only: Type.Optional(Type.Array(Type.String())),
24
+ order: Type.Optional(Type.Array(Type.String())),
25
+ });
26
+
22
27
  // Schema for OpenAI compatibility settings
23
28
  const OpenAICompatSchema = Type.Object({
24
29
  supportsStore: Type.Optional(Type.Boolean()),
25
30
  supportsDeveloperRole: Type.Optional(Type.Boolean()),
26
31
  supportsReasoningEffort: Type.Optional(Type.Boolean()),
27
32
  maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
33
+ openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
28
34
  });
29
35
 
30
36
  // Schema for custom model definition
@@ -36,6 +42,7 @@ const ModelDefinitionSchema = Type.Object({
36
42
  Type.Literal("openai-completions"),
37
43
  Type.Literal("openai-responses"),
38
44
  Type.Literal("openai-codex-responses"),
45
+ Type.Literal("azure-openai-responses"),
39
46
  Type.Literal("anthropic-messages"),
40
47
  Type.Literal("google-generative-ai"),
41
48
  Type.Literal("google-vertex"),
@@ -63,6 +70,7 @@ const ProviderConfigSchema = Type.Object({
63
70
  Type.Literal("openai-completions"),
64
71
  Type.Literal("openai-responses"),
65
72
  Type.Literal("openai-codex-responses"),
73
+ Type.Literal("azure-openai-responses"),
66
74
  Type.Literal("anthropic-messages"),
67
75
  Type.Literal("google-generative-ai"),
68
76
  Type.Literal("google-vertex"),
@@ -156,13 +156,23 @@ export function parseArrayOrCSV(value: unknown): string[] | undefined {
156
156
  return undefined;
157
157
  }
158
158
 
159
+ /**
160
+ * Parse model field into a prioritized list.
161
+ */
162
+ export function parseModelList(value: unknown): string[] | undefined {
163
+ const parsed = parseArrayOrCSV(value);
164
+ if (!parsed) return undefined;
165
+ const normalized = parsed.map(entry => entry.trim()).filter(Boolean);
166
+ return normalized.length > 0 ? normalized : undefined;
167
+ }
168
+
159
169
  /** Parsed agent fields from frontmatter (excludes source/filePath/systemPrompt) */
160
170
  export interface ParsedAgentFields {
161
171
  name: string;
162
172
  description: string;
163
173
  tools?: string[];
164
174
  spawns?: string[] | "*";
165
- model?: string;
175
+ model?: string[];
166
176
  output?: unknown;
167
177
  thinkingLevel?: ThinkingLevel;
168
178
  }
@@ -202,7 +212,7 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
202
212
  }
203
213
 
204
214
  const output = frontmatter.output !== undefined ? frontmatter.output : undefined;
205
- const model = typeof frontmatter.model === "string" ? frontmatter.model : undefined;
215
+ const model = parseModelList(frontmatter.model);
206
216
  const thinkingLevel = parseThinkingLevel(frontmatter);
207
217
 
208
218
  return { name, description, tools, spawns, model, output, thinkingLevel };
@@ -85,6 +85,7 @@ const noOpUIContext: ExtensionUIContext = {
85
85
  input: async (_title, _placeholder, _dialogOptions) => undefined,
86
86
  notify: () => {},
87
87
  setStatus: () => {},
88
+ setWorkingMessage: () => {},
88
89
  setWidget: () => {},
89
90
  setFooter: () => {},
90
91
  setHeader: () => {},
@@ -69,6 +69,9 @@ export interface ExtensionUIContext {
69
69
  /** Set status text in the footer/status bar. Pass undefined to clear. */
70
70
  setStatus(key: string, text: string | undefined): void;
71
71
 
72
+ /** Set the working/loading message shown during streaming. Call with no argument to restore default. */
73
+ setWorkingMessage(message?: string): void;
74
+
72
75
  /** Set a widget to display above the editor. Accepts string array or component factory. */
73
76
  setWidget(key: string, content: string[] | undefined): void;
74
77
  setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
@@ -79,6 +79,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
79
79
  parameters: TParameters;
80
80
  renderCall?: AgentTool<TParameters, TDetails>["renderCall"];
81
81
  renderResult?: AgentTool<TParameters, TDetails>["renderResult"];
82
+ mergeCallAndResult?: boolean;
83
+ inline?: boolean;
82
84
 
83
85
  constructor(
84
86
  private tool: AgentTool<TParameters, TDetails>,
@@ -90,6 +92,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
90
92
  this.parameters = tool.parameters;
91
93
  this.renderCall = tool.renderCall;
92
94
  this.renderResult = tool.renderResult;
95
+ this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
96
+ this.inline = (tool as { inline?: boolean }).inline;
93
97
  }
94
98
 
95
99
  async execute(
@@ -23,6 +23,8 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
23
23
  parameters: TParameters;
24
24
  renderCall?: AgentTool<TParameters, TDetails>["renderCall"];
25
25
  renderResult?: AgentTool<TParameters, TDetails>["renderResult"];
26
+ mergeCallAndResult?: boolean;
27
+ inline?: boolean;
26
28
 
27
29
  constructor(
28
30
  private tool: AgentTool<TParameters, TDetails>,
@@ -34,6 +36,8 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
34
36
  this.parameters = tool.parameters;
35
37
  this.renderCall = tool.renderCall;
36
38
  this.renderResult = tool.renderResult;
39
+ this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
40
+ this.inline = (tool as { inline?: boolean }).inline;
37
41
  }
38
42
 
39
43
  async execute(
@@ -187,6 +187,10 @@ export function getPreludeDocs(): PreludeHelper[] {
187
187
  return cachedPreludeDocs ?? [];
188
188
  }
189
189
 
190
+ export function setPreludeDocsCache(docs: PreludeHelper[]): void {
191
+ cachedPreludeDocs = docs;
192
+ }
193
+
190
194
  export function resetPreludeDocsCache(): void {
191
195
  cachedPreludeDocs = null;
192
196
  }
package/src/lsp/index.ts CHANGED
@@ -955,6 +955,8 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
955
955
  public readonly parameters = lspSchema;
956
956
  public readonly renderCall = renderCall;
957
957
  public readonly renderResult = renderResult;
958
+ public readonly mergeCallAndResult = true;
959
+ public readonly inline = true;
958
960
 
959
961
  private readonly session: ToolSession;
960
962
 
@@ -1011,7 +1013,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1011
1013
  const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
1012
1014
  return {
1013
1015
  content: [{ type: "text", text: output }],
1014
- details: { action, success: true },
1016
+ details: { action, success: true, request: params },
1015
1017
  };
1016
1018
  }
1017
1019
 
@@ -1025,7 +1027,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1025
1027
  text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
1026
1028
  },
1027
1029
  ],
1028
- details: { action, success: true },
1030
+ details: { action, success: true, request: params },
1029
1031
  };
1030
1032
  }
1031
1033
 
@@ -1636,13 +1638,13 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1636
1638
 
1637
1639
  return {
1638
1640
  content: [{ type: "text", text: output }],
1639
- details: { serverName, action, success: true },
1641
+ details: { serverName, action, success: true, request: params },
1640
1642
  };
1641
1643
  } catch (err) {
1642
1644
  const errorMessage = err instanceof Error ? err.message : String(err);
1643
1645
  return {
1644
1646
  content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
1645
- details: { serverName, action, success: false },
1647
+ details: { serverName, action, success: false, request: params },
1646
1648
  };
1647
1649
  }
1648
1650
  }
package/src/lsp/render.ts CHANGED
@@ -7,11 +7,18 @@
7
7
  * - Grouped references and symbols
8
8
  * - Collapsible/expandable views
9
9
  */
10
- import type { AgentToolResult, RenderResultOptions } from "@oh-my-pi/pi-agent-core";
10
+ import type { RenderResultOptions } from "@oh-my-pi/pi-agent-core";
11
11
  import { type Component, Text } from "@oh-my-pi/pi-tui";
12
12
  import { highlight, supportsLanguage } from "cli-highlight";
13
13
  import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
14
- import { formatExpandHint, formatMoreItems, TRUNCATE_LENGTHS, truncate } from "../tools/render-utils";
14
+ import {
15
+ formatExpandHint,
16
+ formatMoreItems,
17
+ formatStatusIcon,
18
+ shortenPath,
19
+ TRUNCATE_LENGTHS,
20
+ truncate,
21
+ } from "../tools/render-utils";
15
22
  import { renderOutputBlock, renderStatusLine } from "../tui";
16
23
  import type { LspParams, LspToolDetails } from "./types";
17
24
 
@@ -23,15 +30,74 @@ import type { LspParams, LspToolDetails } from "./types";
23
30
  * Render the LSP tool call in the TUI.
24
31
  * Shows: "lsp <operation> <file/filecount>"
25
32
  */
26
- export function renderCall(args: unknown, theme: Theme): Text {
27
- const p = args as LspParams & { file?: string; files?: string[] };
33
+ export function renderCall(args: LspParams, theme: Theme): Text {
34
+ const actionLabel = (args.action ?? "request").replace(/_/g, " ");
35
+ const queryPreview = args.query ? truncate(args.query, TRUNCATE_LENGTHS.SHORT, theme.format.ellipsis) : undefined;
36
+ const replacementPreview = args.replacement
37
+ ? truncate(args.replacement, TRUNCATE_LENGTHS.SHORT, theme.format.ellipsis)
38
+ : undefined;
39
+
40
+ let target: string | undefined;
41
+ let hasFileTarget = false;
42
+
43
+ if (args.file) {
44
+ target = shortenPath(args.file);
45
+ hasFileTarget = true;
46
+ } else if (args.files?.length === 1) {
47
+ target = shortenPath(args.files[0]);
48
+ hasFileTarget = true;
49
+ } else if (args.files?.length) {
50
+ target = `${args.files.length} files`;
51
+ }
52
+
53
+ if (hasFileTarget && args.line !== undefined) {
54
+ const col = args.column !== undefined ? `:${args.column}` : "";
55
+ target += `:${args.line}${col}`;
56
+ if (args.end_line !== undefined) {
57
+ const endCol = args.end_character !== undefined ? `:${args.end_character}` : "";
58
+ target += `-${args.end_line}${endCol}`;
59
+ }
60
+ } else if (!target && args.line !== undefined) {
61
+ const col = args.column !== undefined ? `:${args.column}` : "";
62
+ target = `line ${args.line}${col}`;
63
+ if (args.end_line !== undefined) {
64
+ const endCol = args.end_character !== undefined ? `:${args.end_character}` : "";
65
+ target += `-${args.end_line}${endCol}`;
66
+ }
67
+ }
68
+
28
69
  const meta: string[] = [];
29
- if (p.file) {
30
- meta.push(p.file);
31
- } else if (p.files?.length) {
32
- meta.push(`${p.files.length} file(s)`);
70
+ if (queryPreview && target) meta.push(`query:${queryPreview}`);
71
+ if (args.new_name) meta.push(`new:${args.new_name}`);
72
+ if (replacementPreview) meta.push(`replace:${replacementPreview}`);
73
+ if (args.kind) meta.push(`kind:${args.kind}`);
74
+ if (args.apply !== undefined) meta.push(`apply:${args.apply ? "true" : "false"}`);
75
+ if (args.action_index !== undefined) meta.push(`action:${args.action_index}`);
76
+ if (args.include_declaration !== undefined) {
77
+ meta.push(`include_decl:${args.include_declaration ? "true" : "false"}`);
78
+ }
79
+ if (args.end_line !== undefined && args.line === undefined) {
80
+ const endCol = args.end_character !== undefined ? `:${args.end_character}` : "";
81
+ meta.push(`end:${args.end_line}${endCol}`);
82
+ }
83
+
84
+ const descriptionParts = [actionLabel];
85
+ if (target) {
86
+ descriptionParts.push(target);
87
+ } else if (queryPreview) {
88
+ descriptionParts.push(queryPreview);
33
89
  }
34
- const text = renderStatusLine({ icon: "pending", title: "LSP", description: p.action || "?", meta }, theme);
90
+
91
+ const text = renderStatusLine(
92
+ {
93
+ icon: "pending",
94
+ title: "LSP",
95
+ description: descriptionParts.join(" "),
96
+ meta,
97
+ },
98
+ theme,
99
+ );
100
+
35
101
  return new Text(text, 0, 0);
36
102
  }
37
103
 
@@ -44,14 +110,15 @@ export function renderCall(args: unknown, theme: Theme): Text {
44
110
  * Detects hover, diagnostics, references, symbols, etc. and formats accordingly.
45
111
  */
46
112
  export function renderResult(
47
- result: AgentToolResult<LspToolDetails>,
113
+ result: { content: Array<{ type: string; text?: string }>; details?: LspToolDetails; isError?: boolean },
48
114
  options: RenderResultOptions,
49
115
  theme: Theme,
50
116
  args?: LspParams & { file?: string; files?: string[] },
51
117
  ): Component {
52
118
  const content = result.content?.[0];
53
119
  if (!content || content.type !== "text" || !("text" in content) || !content.text) {
54
- const header = renderStatusLine({ icon: "warning", title: "LSP", description: "No result" }, theme);
120
+ const icon = formatStatusIcon("warning", theme, options.spinnerFrame);
121
+ const header = `${icon} LSP`;
55
122
  return new Text([header, theme.fg("dim", "No result")].join("\n"), 0, 0);
56
123
  }
57
124
 
@@ -94,22 +161,50 @@ export function renderResult(
94
161
  }
95
162
  }
96
163
 
97
- const meta: string[] = [];
98
- if (args?.action) meta.push(args.action);
99
- if (args?.file) {
100
- meta.push(args.file);
101
- } else if (args?.files?.length) {
102
- meta.push(`${args.files.length} file(s)`);
164
+ const request = args ?? result.details?.request;
165
+ const requestLines: string[] = [];
166
+ if (request?.file) {
167
+ requestLines.push(theme.fg("toolOutput", request.file));
168
+ } else if (request?.files?.length === 1) {
169
+ requestLines.push(theme.fg("toolOutput", request.files[0]));
170
+ } else if (request?.files?.length) {
171
+ requestLines.push(theme.fg("dim", `${request.files.length} file(s)`));
172
+ }
173
+ if (request?.line !== undefined) {
174
+ const col = request.column !== undefined ? `:${request.column}` : "";
175
+ requestLines.push(theme.fg("dim", `line ${request.line}${col}`));
103
176
  }
104
- const header = renderStatusLine({ icon: state, title: "LSP", description: label, meta }, theme);
177
+ if (request?.end_line !== undefined) {
178
+ const endCol = request.end_character !== undefined ? `:${request.end_character}` : "";
179
+ requestLines.push(theme.fg("dim", `end ${request.end_line}${endCol}`));
180
+ }
181
+ if (request?.query) requestLines.push(theme.fg("dim", `query: ${request.query}`));
182
+ if (request?.new_name) requestLines.push(theme.fg("dim", `new name: ${request.new_name}`));
183
+ if (request?.replacement) requestLines.push(theme.fg("dim", `replacement: ${request.replacement}`));
184
+ if (request?.kind) requestLines.push(theme.fg("dim", `kind: ${request.kind}`));
185
+ if (request?.apply !== undefined) requestLines.push(theme.fg("dim", `apply: ${request.apply ? "true" : "false"}`));
186
+ if (request?.action_index !== undefined) requestLines.push(theme.fg("dim", `action: ${request.action_index}`));
187
+ if (request?.include_declaration !== undefined) {
188
+ requestLines.push(theme.fg("dim", `include declaration: ${request.include_declaration ? "true" : "false"}`));
189
+ }
190
+
191
+ const actionLabel = (request?.action ?? result.details?.action ?? label.toLowerCase()).replace(/_/g, " ");
192
+ const status = options.isPartial ? "running" : result.isError ? "error" : "success";
193
+ const icon = formatStatusIcon(status, theme, options.spinnerFrame);
194
+ const header = `${icon} LSP ${actionLabel}`;
195
+
105
196
  return {
106
197
  render: (width: number) =>
107
198
  renderOutputBlock(
108
199
  {
109
200
  header,
110
201
  state,
111
- sections: [{ label: theme.fg("toolTitle", label), lines: bodyLines }],
202
+ sections: [
203
+ ...(requestLines.length > 0 ? [{ lines: requestLines }] : []),
204
+ { label: theme.fg("toolTitle", "Response"), lines: bodyLines },
205
+ ],
112
206
  width,
207
+ applyBg: false,
113
208
  },
114
209
  theme,
115
210
  ),
@@ -612,4 +707,5 @@ export const lspToolRenderer = {
612
707
  renderCall,
613
708
  renderResult,
614
709
  mergeCallAndResult: true,
710
+ inline: true,
615
711
  };
package/src/lsp/types.ts CHANGED
@@ -53,6 +53,7 @@ export interface LspToolDetails {
53
53
  serverName?: string;
54
54
  action: string;
55
55
  success: boolean;
56
+ request?: LspParams;
56
57
  }
57
58
 
58
59
  // =============================================================================
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
2
  import {
3
3
  Box,
4
+ type Component,
4
5
  Container,
5
6
  getCapabilities,
6
7
  getImageDimensions,
@@ -11,6 +12,7 @@ import {
11
12
  type TUI,
12
13
  } from "@oh-my-pi/pi-tui";
13
14
  import { sanitizeText } from "@oh-my-pi/pi-utils";
15
+ import type { Theme } from "../../modes/theme/theme";
14
16
  import { theme } from "../../modes/theme/theme";
15
17
  import { computeEditDiff, computePatchDiff, type EditDiffError, type EditDiffResult } from "../../patch";
16
18
  import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
@@ -376,7 +378,8 @@ export class ToolExecutionComponent extends Container {
376
378
  const tool = this.tool;
377
379
  const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
378
380
  // Custom tools use Box for flexible component rendering
379
- this.contentBox.setBgFn(bgFn);
381
+ const inline = Boolean((tool as { inline?: boolean }).inline);
382
+ this.contentBox.setBgFn(inline ? undefined : bgFn);
380
383
  this.contentBox.clear();
381
384
 
382
385
  // Render call component
@@ -404,10 +407,17 @@ export class ToolExecutionComponent extends Container {
404
407
  // Render result component if we have a result
405
408
  if (this.result && tool.renderResult) {
406
409
  try {
407
- const resultComponent = tool.renderResult(
408
- { content: this.result.content as any, details: this.result.details },
410
+ const renderResult = tool.renderResult as (
411
+ result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
412
+ options: { expanded: boolean; isPartial: boolean; spinnerFrame?: number },
413
+ theme: Theme,
414
+ args?: unknown,
415
+ ) => Component;
416
+ const resultComponent = renderResult(
417
+ { content: this.result.content as any, details: this.result.details, isError: this.result.isError },
409
418
  { expanded: this.expanded, isPartial: this.isPartial, spinnerFrame: this.spinnerFrame },
410
419
  theme,
420
+ this.args,
411
421
  );
412
422
  if (resultComponent) {
413
423
  // Ensure component has invalidate() method for Component interface
@@ -542,10 +552,16 @@ export class ToolExecutionComponent extends Container {
542
552
  }
543
553
 
544
554
  /**
545
- * Build render context for tools that need extra state (bash, edit)
555
+ * Build render context for tools that need extra state (bash, python, edit)
546
556
  */
547
557
  private buildRenderContext(): Record<string, unknown> {
548
558
  const context: Record<string, unknown> = {};
559
+ const normalizeTimeoutSeconds = (value: unknown, maxSeconds: number): number | undefined => {
560
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
561
+ let timeoutSec = value > 1000 ? value / 1000 : value;
562
+ timeoutSec = Math.max(1, Math.min(maxSeconds, timeoutSec));
563
+ return timeoutSec;
564
+ };
549
565
 
550
566
  if (this.toolName === "bash" && this.result) {
551
567
  // Pass raw output and expanded state - renderer handles width-aware truncation
@@ -553,13 +569,13 @@ export class ToolExecutionComponent extends Container {
553
569
  context.output = output;
554
570
  context.expanded = this.expanded;
555
571
  context.previewLines = BASH_DEFAULT_PREVIEW_LINES;
556
- context.timeout = typeof this.args?.timeout === "number" ? this.args.timeout : undefined;
572
+ context.timeout = normalizeTimeoutSeconds(this.args?.timeout, 3600);
557
573
  } else if (this.toolName === "python" && this.result) {
558
574
  const output = this.getTextOutput().trimEnd();
559
575
  context.output = output;
560
576
  context.expanded = this.expanded;
561
577
  context.previewLines = PYTHON_DEFAULT_PREVIEW_LINES;
562
- context.timeout = typeof this.args?.timeout === "number" ? this.args.timeout : undefined;
578
+ context.timeout = normalizeTimeoutSeconds(this.args?.timeout, 600);
563
579
  } else if (this.toolName === "edit") {
564
580
  // Edit needs diff preview and renderDiff function
565
581
  context.editDiffPreview = this.editDiffPreview;
@@ -68,6 +68,7 @@ export class EventController {
68
68
  getSymbolTheme().spinnerFrames,
69
69
  );
70
70
  this.ctx.statusContainer.addChild(this.ctx.loadingAnimation);
71
+ this.ctx.applyPendingWorkingMessage();
71
72
  this.ctx.ui.requestRender();
72
73
  break;
73
74
 
@@ -30,6 +30,7 @@ export class ExtensionUiController {
30
30
  input: (title, placeholder, _dialogOptions) => this.showHookInput(title, placeholder),
31
31
  notify: (message, type) => this.showHookNotify(message, type),
32
32
  setStatus: (key, text) => this.setHookStatus(key, text),
33
+ setWorkingMessage: message => this.ctx.setWorkingMessage(message),
33
34
  setWidget: (key, content) => this.setHookWidget(key, content),
34
35
  setTitle: title => setTerminalTitle(title),
35
36
  custom: (factory, _options) => this.showHookCustom(factory),
@@ -389,6 +390,7 @@ export class ExtensionUiController {
389
390
  input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
390
391
  notify: () => {},
391
392
  setStatus: () => {},
393
+ setWorkingMessage: () => {},
392
394
  setWidget: () => {},
393
395
  setTitle: () => {},
394
396
  custom: async () => undefined as never,
@@ -541,9 +541,9 @@ export class InputController {
541
541
  data: imageData.data,
542
542
  mimeType: imageData.mimeType,
543
543
  });
544
- // Insert styled placeholder at cursor like Claude does
544
+ // Insert placeholder at cursor like Claude does
545
545
  const imageNum = this.ctx.pendingImages.length;
546
- const placeholder = theme.bold(theme.underline(`[Image #${imageNum}]`));
546
+ const placeholder = `[Image #${imageNum}]`;
547
547
  this.ctx.editor.insertText(`${placeholder} `);
548
548
  this.ctx.ui.requestRender();
549
549
  return true;
@@ -102,6 +102,8 @@ export class InteractiveMode implements InteractiveModeContext {
102
102
  public loadingAnimation: Loader | undefined = undefined;
103
103
  public autoCompactionLoader: Loader | undefined = undefined;
104
104
  public retryLoader: Loader | undefined = undefined;
105
+ private pendingWorkingMessage: string | undefined;
106
+ private readonly defaultWorkingMessage = `Working${theme.format.ellipsis} (esc to interrupt)`;
105
107
  public autoCompactionEscapeHandler?: () => void;
106
108
  public retryEscapeHandler?: () => void;
107
109
  public unsubscribe?: () => void;
@@ -160,6 +162,7 @@ export class InteractiveMode implements InteractiveModeContext {
160
162
  this.statusContainer = new Container();
161
163
  this.todoContainer = new Container();
162
164
  this.editor = new CustomEditor(getEditorTheme());
165
+ this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
163
166
  this.editor.onAutocompleteCancel = () => {
164
167
  this.ui.requestRender(true);
165
168
  };
@@ -538,6 +541,33 @@ export class InteractiveMode implements InteractiveModeContext {
538
541
  this.uiHelpers.showWarning(message);
539
542
  }
540
543
 
544
+ setWorkingMessage(message?: string): void {
545
+ if (message === undefined) {
546
+ this.pendingWorkingMessage = undefined;
547
+ if (this.loadingAnimation) {
548
+ this.loadingAnimation.setMessage(this.defaultWorkingMessage);
549
+ }
550
+ return;
551
+ }
552
+
553
+ if (this.loadingAnimation) {
554
+ this.loadingAnimation.setMessage(message);
555
+ return;
556
+ }
557
+
558
+ this.pendingWorkingMessage = message;
559
+ }
560
+
561
+ applyPendingWorkingMessage(): void {
562
+ if (this.pendingWorkingMessage === undefined) {
563
+ return;
564
+ }
565
+
566
+ const message = this.pendingWorkingMessage;
567
+ this.pendingWorkingMessage = undefined;
568
+ this.setWorkingMessage(message);
569
+ }
570
+
541
571
  showNewVersionNotification(newVersion: string): void {
542
572
  this.uiHelpers.showNewVersionNotification(newVersion);
543
573
  }
@@ -187,6 +187,10 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
187
187
  } as RpcExtensionUIRequest);
188
188
  }
189
189
 
190
+ setWorkingMessage(_message?: string): void {
191
+ // Not supported in RPC mode
192
+ }
193
+
190
194
  setWidget(key: string, content: unknown): void {
191
195
  // Only support string arrays in RPC mode - factory functions are ignored
192
196
  if (content === undefined || Array.isArray(content)) {