@oh-my-pi/pi-coding-agent 3.20.1 → 3.21.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 (94) hide show
  1. package/CHANGELOG.md +69 -9
  2. package/docs/custom-tools.md +3 -3
  3. package/docs/extensions.md +226 -220
  4. package/docs/hooks.md +2 -2
  5. package/docs/sdk.md +3 -3
  6. package/examples/custom-tools/README.md +2 -2
  7. package/examples/custom-tools/subagent/index.ts +1 -1
  8. package/examples/extensions/README.md +76 -74
  9. package/examples/extensions/todo.ts +2 -5
  10. package/examples/hooks/custom-compaction.ts +1 -1
  11. package/examples/hooks/handoff.ts +1 -1
  12. package/examples/hooks/qna.ts +1 -1
  13. package/examples/sdk/02-custom-model.ts +1 -1
  14. package/examples/sdk/12-full-control.ts +1 -1
  15. package/examples/sdk/README.md +1 -1
  16. package/package.json +5 -5
  17. package/src/cli/file-processor.ts +1 -1
  18. package/src/cli/list-models.ts +1 -1
  19. package/src/core/agent-session.ts +13 -2
  20. package/src/core/auth-storage.ts +1 -1
  21. package/src/core/compaction/branch-summarization.ts +2 -2
  22. package/src/core/compaction/compaction.ts +2 -2
  23. package/src/core/compaction/utils.ts +1 -1
  24. package/src/core/custom-tools/types.ts +1 -1
  25. package/src/core/extensions/runner.ts +1 -1
  26. package/src/core/extensions/types.ts +1 -1
  27. package/src/core/extensions/wrapper.ts +1 -1
  28. package/src/core/hooks/runner.ts +2 -2
  29. package/src/core/hooks/types.ts +1 -1
  30. package/src/core/messages.ts +1 -1
  31. package/src/core/model-registry.ts +1 -1
  32. package/src/core/model-resolver.ts +1 -1
  33. package/src/core/sdk.ts +33 -4
  34. package/src/core/session-manager.ts +11 -22
  35. package/src/core/settings-manager.ts +66 -1
  36. package/src/core/slash-commands.ts +12 -5
  37. package/src/core/system-prompt.ts +27 -3
  38. package/src/core/title-generator.ts +2 -2
  39. package/src/core/tools/ask.ts +88 -1
  40. package/src/core/tools/bash-interceptor.ts +7 -0
  41. package/src/core/tools/bash.ts +106 -0
  42. package/src/core/tools/edit-diff.ts +73 -24
  43. package/src/core/tools/edit.ts +214 -20
  44. package/src/core/tools/find.ts +155 -0
  45. package/src/core/tools/gemini-image.ts +279 -56
  46. package/src/core/tools/git.ts +4 -0
  47. package/src/core/tools/grep.ts +191 -0
  48. package/src/core/tools/index.ts +3 -6
  49. package/src/core/tools/ls.ts +134 -1
  50. package/src/core/tools/lsp/render.ts +34 -14
  51. package/src/core/tools/notebook.ts +110 -0
  52. package/src/core/tools/output.ts +179 -7
  53. package/src/core/tools/read.ts +122 -9
  54. package/src/core/tools/render-utils.ts +241 -0
  55. package/src/core/tools/renderers.ts +40 -828
  56. package/src/core/tools/review.ts +26 -7
  57. package/src/core/tools/rulebook.ts +3 -1
  58. package/src/core/tools/task/index.ts +18 -3
  59. package/src/core/tools/task/render.ts +5 -0
  60. package/src/core/tools/task/types.ts +1 -1
  61. package/src/core/tools/truncate.ts +27 -1
  62. package/src/core/tools/web-fetch.ts +23 -15
  63. package/src/core/tools/web-search/index.ts +130 -45
  64. package/src/core/tools/web-search/providers/anthropic.ts +7 -2
  65. package/src/core/tools/web-search/providers/exa.ts +2 -1
  66. package/src/core/tools/web-search/providers/perplexity.ts +6 -1
  67. package/src/core/tools/web-search/render.ts +5 -0
  68. package/src/core/tools/web-search/types.ts +13 -0
  69. package/src/core/tools/write.ts +90 -0
  70. package/src/core/voice.ts +1 -1
  71. package/src/main.ts +1 -1
  72. package/src/modes/interactive/components/assistant-message.ts +1 -1
  73. package/src/modes/interactive/components/custom-message.ts +1 -1
  74. package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
  75. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  76. package/src/modes/interactive/components/footer.ts +1 -1
  77. package/src/modes/interactive/components/hook-message.ts +1 -1
  78. package/src/modes/interactive/components/model-selector.ts +1 -1
  79. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  80. package/src/modes/interactive/components/settings-defs.ts +49 -0
  81. package/src/modes/interactive/components/status-line.ts +1 -1
  82. package/src/modes/interactive/components/tool-execution.ts +93 -538
  83. package/src/modes/interactive/interactive-mode.ts +19 -7
  84. package/src/modes/print-mode.ts +1 -1
  85. package/src/modes/rpc/rpc-client.ts +1 -1
  86. package/src/modes/rpc/rpc-types.ts +1 -1
  87. package/src/prompts/system-prompt.md +4 -0
  88. package/src/prompts/tools/gemini-image.md +5 -1
  89. package/src/prompts/tools/output.md +4 -0
  90. package/src/prompts/tools/web-fetch.md +1 -0
  91. package/src/prompts/tools/web-search.md +2 -0
  92. package/src/utils/image-convert.ts +8 -2
  93. package/src/utils/image-magick.ts +247 -0
  94. package/src/utils/image-resize.ts +53 -13
@@ -1,8 +1,13 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import type { Component } from "@oh-my-pi/pi-tui";
3
+ import { Text } from "@oh-my-pi/pi-tui";
2
4
  import { Type } from "@sinclair/typebox";
5
+ import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme";
3
6
  import writeDescription from "../../prompts/tools/write.md" with { type: "text" };
7
+ import type { RenderResultOptions } from "../custom-tools/types";
4
8
  import { type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "./lsp/index";
5
9
  import { resolveToCwd } from "./path-utils";
10
+ import { formatDiagnostics, replaceTabs, shortenPath } from "./render-utils";
6
11
 
7
12
  const writeSchema = Type.Object({
8
13
  path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
@@ -61,3 +66,88 @@ export function createWriteTool(
61
66
 
62
67
  /** Default write tool using process.cwd() - for backwards compatibility */
63
68
  export const writeTool = createWriteTool(process.cwd());
69
+
70
+ // =============================================================================
71
+ // TUI Renderer
72
+ // =============================================================================
73
+
74
+ interface WriteRenderArgs {
75
+ path?: string;
76
+ file_path?: string;
77
+ content?: string;
78
+ }
79
+
80
+ function countLines(text: string): number {
81
+ if (!text) return 0;
82
+ return text.split("\n").length;
83
+ }
84
+
85
+ function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
86
+ const icon = uiTheme.getLangIcon(language);
87
+ if (lineCount !== null) {
88
+ return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
89
+ }
90
+ return uiTheme.fg("dim", `${icon}`);
91
+ }
92
+
93
+ export const writeToolRenderer = {
94
+ renderCall(args: WriteRenderArgs, uiTheme: Theme): Component {
95
+ const rawPath = args.file_path || args.path || "";
96
+ const filePath = shortenPath(rawPath);
97
+ const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
98
+ const text = `${uiTheme.fg("toolTitle", uiTheme.bold("Write"))} ${pathDisplay}`;
99
+ return new Text(text, 0, 0);
100
+ },
101
+
102
+ renderResult(
103
+ result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails },
104
+ { expanded }: RenderResultOptions,
105
+ uiTheme: Theme,
106
+ args?: WriteRenderArgs,
107
+ ): Component {
108
+ const rawPath = args?.file_path || args?.path || "";
109
+ const fileContent = args?.content || "";
110
+ const lang = getLanguageFromPath(rawPath);
111
+ const contentLines = fileContent
112
+ ? lang
113
+ ? highlightCode(replaceTabs(fileContent), lang)
114
+ : fileContent.split("\n")
115
+ : [];
116
+ const totalLines = contentLines.length;
117
+ const outputLines: string[] = [];
118
+
119
+ outputLines.push(formatMetadataLine(countLines(fileContent), lang ?? "text", uiTheme));
120
+
121
+ if (fileContent) {
122
+ const maxLines = expanded ? contentLines.length : 10;
123
+ const displayLines = contentLines.slice(0, maxLines);
124
+ const remaining = contentLines.length - maxLines;
125
+
126
+ outputLines.push(
127
+ "",
128
+ ...displayLines.map((line: string) =>
129
+ lang ? replaceTabs(line) : uiTheme.fg("toolOutput", replaceTabs(line)),
130
+ ),
131
+ );
132
+ if (remaining > 0) {
133
+ outputLines.push(
134
+ uiTheme.fg(
135
+ "toolOutput",
136
+ `${uiTheme.format.ellipsis} (${remaining} more lines, ${totalLines} total) ${uiTheme.format.bracketLeft}Ctrl+O to expand${uiTheme.format.bracketRight}`,
137
+ ),
138
+ );
139
+ }
140
+ }
141
+
142
+ // Show LSP diagnostics if available
143
+ if (result.details?.diagnostics) {
144
+ outputLines.push(
145
+ formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp) =>
146
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
147
+ ),
148
+ );
149
+ }
150
+
151
+ return new Text(outputLines.join("\n"), 0, 0);
152
+ },
153
+ };
package/src/core/voice.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { unlinkSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { completeSimple, type Model } from "@oh-my-pi/pi-ai";
4
+ import { completeSimple, type Model } from "@mariozechner/pi-ai";
5
5
  import voiceSummaryPrompt from "../prompts/voice-summary.md" with { type: "text" };
6
6
  import { logger } from "./logger";
7
7
  import type { ModelRegistry } from "./model-registry";
package/src/main.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * createAgentSession() options. The SDK does the heavy lifting.
6
6
  */
7
7
 
8
- import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
8
+ import { type ImageContent, supportsXhigh } from "@mariozechner/pi-ai";
9
9
  import chalk from "chalk";
10
10
  import { type Args, parseArgs, printHelp } from "./cli/args";
11
11
  import { processFileArguments } from "./cli/file-processor";
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage } from "@oh-my-pi/pi-ai";
1
+ import type { AssistantMessage } from "@mariozechner/pi-ai";
2
2
  import { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
3
3
  import { getMarkdownTheme, theme } from "../theme/theme";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { TextContent } from "@oh-my-pi/pi-ai";
1
+ import type { TextContent } from "@mariozechner/pi-ai";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
4
4
  import type { MessageRenderer } from "../../../core/extensions/types";
@@ -68,7 +68,7 @@ export class InspectorPanel implements Component {
68
68
 
69
69
  switch (ext.kind) {
70
70
  case "context-file":
71
- content = this.renderFilePreview(ext.path, width);
71
+ content = this.renderFilePreview(ext.raw, width);
72
72
  break;
73
73
  case "tool":
74
74
  content = this.renderToolArgs(ext.raw, width);
@@ -91,37 +91,40 @@ export class InspectorPanel implements Component {
91
91
  return lines;
92
92
  }
93
93
 
94
- private renderFilePreview(path: string, width: number): string[] {
94
+ private renderFilePreview(raw: unknown, width: number): string[] {
95
95
  const lines: string[] = [];
96
96
  lines.push(theme.fg("muted", "Preview:"));
97
97
  lines.push(theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.min(width - 2, 40))));
98
98
 
99
- try {
100
- const content = Bun.file(path).text();
101
- // Note: async call to sync context - will show empty on first render
102
- // This is acceptable for preview which can populate on next render
103
- if (typeof content === "object" && "then" in content) {
104
- content.then((text: string) => {
105
- const fileLines = text.split("\n").slice(0, 20);
106
-
107
- for (const line of fileLines) {
108
- const highlighted = this.highlightMarkdown(line);
109
- lines.push(truncateToWidth(highlighted, width - 2));
110
- }
111
-
112
- if (text.split("\n").length > 20) {
113
- lines.push(theme.fg("dim", "(truncated at line 20)"));
114
- }
115
- });
116
- }
117
- } catch (err) {
118
- lines.push(theme.fg("error", `Failed to read file: ${err instanceof Error ? err.message : String(err)}`));
99
+ const content = this.getContextFileContent(raw);
100
+ if (!content) {
101
+ lines.push(theme.fg("dim", " (no content)"));
102
+ lines.push("");
103
+ return lines;
104
+ }
105
+
106
+ const fileLines = content.split("\n");
107
+ for (const line of fileLines.slice(0, 20)) {
108
+ const highlighted = this.highlightMarkdown(line);
109
+ lines.push(truncateToWidth(highlighted, width - 2));
110
+ }
111
+
112
+ if (fileLines.length > 20) {
113
+ lines.push(theme.fg("dim", "(truncated at line 20)"));
119
114
  }
120
115
 
121
116
  lines.push("");
122
117
  return lines;
123
118
  }
124
119
 
120
+ private getContextFileContent(raw: unknown): string | null {
121
+ if (raw && typeof raw === "object" && "content" in raw) {
122
+ const content = (raw as { content?: unknown }).content;
123
+ return typeof content === "string" ? content : null;
124
+ }
125
+ return null;
126
+ }
127
+
125
128
  private highlightMarkdown(line: string): string {
126
129
  // Basic markdown syntax highlighting
127
130
  let highlighted = line;
@@ -10,6 +10,7 @@ import type { MCPServer } from "../../../../capability/mcp";
10
10
  import type { Prompt } from "../../../../capability/prompt";
11
11
  import type { Rule } from "../../../../capability/rule";
12
12
  import type { Skill } from "../../../../capability/skill";
13
+ import type { SlashCommand } from "../../../../capability/slash-command";
13
14
  import type { CustomTool } from "../../../../capability/tool";
14
15
  import type { SourceMeta } from "../../../../capability/types";
15
16
  import {
@@ -192,6 +193,17 @@ export function loadAllExtensions(cwd?: string, disabledIds?: string[]): Extensi
192
193
  // Capability may not be registered
193
194
  }
194
195
 
196
+ // Load slash commands
197
+ try {
198
+ const commands = loadSync<SlashCommand>("slash-commands", loadOpts);
199
+ addItems(commands.all, "slash-command", {
200
+ getDescription: () => undefined,
201
+ getTrigger: (c) => `/${c.name}`,
202
+ });
203
+ } catch {
204
+ // Capability may not be registered
205
+ }
206
+
195
207
  // Load hooks
196
208
  try {
197
209
  const hooks = loadSync<Hook>("hooks", loadOpts);
@@ -1,5 +1,5 @@
1
1
  import { existsSync, type FSWatcher, readFileSync, watch } from "node:fs";
2
- import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
+ import type { AssistantMessage } from "@mariozechner/pi-ai";
3
3
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
4
4
  import { dirname, join } from "path";
5
5
  import type { AgentSession } from "../../../core/agent-session";
@@ -1,4 +1,4 @@
1
- import type { TextContent } from "@oh-my-pi/pi-ai";
1
+ import type { TextContent } from "@mariozechner/pi-ai";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
4
4
  import type { HookMessageRenderer } from "../../../core/hooks/types";
@@ -1,4 +1,4 @@
1
- import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
1
+ import { type Model, modelsAreEqual } from "@mariozechner/pi-ai";
2
2
  import {
3
3
  Container,
4
4
  Input,
@@ -1,4 +1,4 @@
1
- import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
1
+ import { getOAuthProviders, type OAuthProviderInfo } from "@mariozechner/pi-ai";
2
2
  import { Container, isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
3
3
  import type { AuthStorage } from "../../../core/auth-storage";
4
4
  import { theme } from "../theme/theme";
@@ -11,11 +11,13 @@
11
11
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
12
12
  import { getCapabilities } from "@oh-my-pi/pi-tui";
13
13
  import type {
14
+ ImageProviderOption,
14
15
  NotificationMethod,
15
16
  SettingsManager,
16
17
  StatusLinePreset,
17
18
  StatusLineSeparatorStyle,
18
19
  SymbolPreset,
20
+ WebSearchProviderOption,
19
21
  } from "../../../core/settings-manager";
20
22
  import { getPreset } from "./status-line/presets";
21
23
 
@@ -86,6 +88,15 @@ export const SETTINGS_DEFS: SettingDef[] = [
86
88
  get: (sm) => sm.getCompactionEnabled(),
87
89
  set: (sm, v) => sm.setCompactionEnabled(v), // Also handled in session
88
90
  },
91
+ {
92
+ id: "branchSummaries",
93
+ tab: "config",
94
+ type: "boolean",
95
+ label: "Branch summaries",
96
+ description: "Prompt to summarize when leaving a branch",
97
+ get: (sm) => sm.getBranchSummaryEnabled(),
98
+ set: (sm, v) => sm.setBranchSummaryEnabled(v),
99
+ },
89
100
  {
90
101
  id: "showImages",
91
102
  tab: "config",
@@ -191,6 +202,15 @@ export const SETTINGS_DEFS: SettingDef[] = [
191
202
  get: (sm) => sm.getBashInterceptorEnabled(),
192
203
  set: (sm, v) => sm.setBashInterceptorEnabled(v),
193
204
  },
205
+ {
206
+ id: "gitTool",
207
+ tab: "config",
208
+ type: "boolean",
209
+ label: "Git tool",
210
+ description: "Enable structured Git tool",
211
+ get: (sm) => sm.getGitToolEnabled(),
212
+ set: (sm, v) => sm.setGitToolEnabled(v),
213
+ },
194
214
  {
195
215
  id: "mcpProjectConfig",
196
216
  tab: "config",
@@ -277,6 +297,35 @@ export const SETTINGS_DEFS: SettingDef[] = [
277
297
  { value: "ascii", label: "ASCII", description: "ASCII-only characters (maximum compatibility)" },
278
298
  ],
279
299
  },
300
+ {
301
+ id: "webSearchProvider",
302
+ tab: "config",
303
+ type: "submenu",
304
+ label: "Web search provider",
305
+ description: "Provider for web search tool",
306
+ get: (sm) => sm.getWebSearchProvider(),
307
+ set: (sm, v) => sm.setWebSearchProvider(v as WebSearchProviderOption),
308
+ getOptions: () => [
309
+ { value: "auto", label: "Auto", description: "Priority: Exa > Perplexity > Anthropic" },
310
+ { value: "exa", label: "Exa", description: "Use Exa (requires EXA_API_KEY)" },
311
+ { value: "perplexity", label: "Perplexity", description: "Use Perplexity (requires PERPLEXITY_API_KEY)" },
312
+ { value: "anthropic", label: "Anthropic", description: "Use Anthropic web search" },
313
+ ],
314
+ },
315
+ {
316
+ id: "imageProvider",
317
+ tab: "config",
318
+ type: "submenu",
319
+ label: "Image provider",
320
+ description: "Provider for image generation tool",
321
+ get: (sm) => sm.getImageProvider(),
322
+ set: (sm, v) => sm.setImageProvider(v as ImageProviderOption),
323
+ getOptions: () => [
324
+ { value: "auto", label: "Auto", description: "Priority: OpenRouter > Gemini" },
325
+ { value: "gemini", label: "Gemini", description: "Use Gemini API directly (requires GEMINI_API_KEY)" },
326
+ { value: "openrouter", label: "OpenRouter", description: "Use OpenRouter (requires OPENROUTER_API_KEY)" },
327
+ ],
328
+ },
280
329
 
281
330
  // LSP tab
282
331
  {
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage } from "@oh-my-pi/pi-ai";
1
+ import type { AssistantMessage } from "@mariozechner/pi-ai";
2
2
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
3
3
  import { type FSWatcher, watch } from "fs";
4
4
  import { dirname, join } from "path";