@oh-my-pi/pi-coding-agent 3.20.1 → 3.24.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 (123) hide show
  1. package/CHANGELOG.md +107 -8
  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 +50 -53
  6. package/examples/custom-tools/README.md +2 -17
  7. package/examples/extensions/README.md +76 -74
  8. package/examples/extensions/todo.ts +2 -5
  9. package/examples/hooks/custom-compaction.ts +2 -4
  10. package/examples/hooks/handoff.ts +1 -1
  11. package/examples/hooks/qna.ts +1 -1
  12. package/examples/sdk/02-custom-model.ts +1 -1
  13. package/examples/sdk/README.md +7 -11
  14. package/package.json +6 -6
  15. package/src/cli/args.ts +9 -6
  16. package/src/cli/file-processor.ts +1 -1
  17. package/src/cli/list-models.ts +1 -1
  18. package/src/core/agent-session.ts +16 -5
  19. package/src/core/auth-storage.ts +1 -1
  20. package/src/core/compaction/branch-summarization.ts +2 -2
  21. package/src/core/compaction/compaction.ts +2 -2
  22. package/src/core/compaction/utils.ts +1 -1
  23. package/src/core/custom-tools/types.ts +1 -1
  24. package/src/core/custom-tools/wrapper.ts +0 -1
  25. package/src/core/extensions/index.ts +1 -6
  26. package/src/core/extensions/runner.ts +1 -1
  27. package/src/core/extensions/types.ts +1 -1
  28. package/src/core/extensions/wrapper.ts +1 -8
  29. package/src/core/file-mentions.ts +5 -8
  30. package/src/core/hooks/runner.ts +2 -2
  31. package/src/core/hooks/types.ts +1 -1
  32. package/src/core/messages.ts +1 -1
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/model-resolver.ts +1 -1
  35. package/src/core/sdk.ts +64 -105
  36. package/src/core/session-manager.ts +18 -22
  37. package/src/core/settings-manager.ts +66 -1
  38. package/src/core/slash-commands.ts +12 -5
  39. package/src/core/system-prompt.ts +49 -36
  40. package/src/core/title-generator.ts +2 -2
  41. package/src/core/tools/ask.ts +98 -4
  42. package/src/core/tools/bash-interceptor.ts +11 -4
  43. package/src/core/tools/bash.ts +121 -5
  44. package/src/core/tools/context.ts +7 -0
  45. package/src/core/tools/edit-diff.ts +73 -24
  46. package/src/core/tools/edit.ts +221 -34
  47. package/src/core/tools/exa/render.ts +4 -16
  48. package/src/core/tools/find.ts +149 -5
  49. package/src/core/tools/gemini-image.ts +279 -56
  50. package/src/core/tools/git.ts +17 -3
  51. package/src/core/tools/grep.ts +185 -5
  52. package/src/core/tools/index.test.ts +180 -0
  53. package/src/core/tools/index.ts +96 -242
  54. package/src/core/tools/ls.ts +133 -5
  55. package/src/core/tools/lsp/index.ts +32 -29
  56. package/src/core/tools/lsp/render.ts +21 -22
  57. package/src/core/tools/notebook.ts +112 -4
  58. package/src/core/tools/output.ts +175 -15
  59. package/src/core/tools/read.ts +127 -25
  60. package/src/core/tools/render-utils.ts +241 -0
  61. package/src/core/tools/renderers.ts +40 -828
  62. package/src/core/tools/review.ts +26 -25
  63. package/src/core/tools/rulebook.ts +11 -3
  64. package/src/core/tools/task/agents.ts +28 -7
  65. package/src/core/tools/task/discovery.ts +0 -6
  66. package/src/core/tools/task/executor.ts +264 -254
  67. package/src/core/tools/task/index.ts +48 -208
  68. package/src/core/tools/task/render.ts +26 -11
  69. package/src/core/tools/task/types.ts +7 -12
  70. package/src/core/tools/task/worker-protocol.ts +17 -0
  71. package/src/core/tools/task/worker.ts +238 -0
  72. package/src/core/tools/truncate.ts +27 -1
  73. package/src/core/tools/web-fetch.ts +25 -49
  74. package/src/core/tools/web-search/index.ts +132 -46
  75. package/src/core/tools/web-search/providers/anthropic.ts +7 -2
  76. package/src/core/tools/web-search/providers/exa.ts +2 -1
  77. package/src/core/tools/web-search/providers/perplexity.ts +6 -1
  78. package/src/core/tools/web-search/render.ts +6 -4
  79. package/src/core/tools/web-search/types.ts +13 -0
  80. package/src/core/tools/write.ts +96 -14
  81. package/src/core/voice.ts +1 -1
  82. package/src/discovery/helpers.test.ts +1 -1
  83. package/src/index.ts +5 -16
  84. package/src/main.ts +5 -5
  85. package/src/modes/interactive/components/assistant-message.ts +1 -1
  86. package/src/modes/interactive/components/custom-message.ts +1 -1
  87. package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
  88. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  89. package/src/modes/interactive/components/footer.ts +1 -1
  90. package/src/modes/interactive/components/hook-message.ts +1 -1
  91. package/src/modes/interactive/components/model-selector.ts +1 -1
  92. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  93. package/src/modes/interactive/components/settings-defs.ts +49 -0
  94. package/src/modes/interactive/components/status-line.ts +1 -1
  95. package/src/modes/interactive/components/tool-execution.ts +93 -538
  96. package/src/modes/interactive/interactive-mode.ts +19 -7
  97. package/src/modes/interactive/theme/theme.ts +4 -4
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +1 -1
  100. package/src/modes/rpc/rpc-types.ts +1 -1
  101. package/src/prompts/system-prompt.md +4 -0
  102. package/src/prompts/task.md +0 -7
  103. package/src/prompts/tools/gemini-image.md +5 -1
  104. package/src/prompts/tools/output.md +6 -2
  105. package/src/prompts/tools/task.md +68 -0
  106. package/src/prompts/tools/web-fetch.md +1 -0
  107. package/src/prompts/tools/web-search.md +2 -0
  108. package/src/utils/image-convert.ts +8 -2
  109. package/src/utils/image-magick.ts +247 -0
  110. package/src/utils/image-resize.ts +53 -13
  111. package/examples/custom-tools/question/index.ts +0 -84
  112. package/examples/custom-tools/subagent/README.md +0 -172
  113. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  114. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  115. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  116. package/examples/custom-tools/subagent/agents.ts +0 -156
  117. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  118. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  119. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  120. package/examples/custom-tools/subagent/index.ts +0 -1002
  121. package/examples/sdk/05-tools.ts +0 -94
  122. package/examples/sdk/12-full-control.ts +0 -95
  123. package/src/prompts/browser.md +0 -71
@@ -13,6 +13,7 @@ import type {
13
13
  WebSearchResponse,
14
14
  WebSearchSource,
15
15
  } from "../types";
16
+ import { WebSearchProviderError } from "../types";
16
17
 
17
18
  const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
18
19
 
@@ -92,7 +93,11 @@ async function callPerplexity(apiKey: string, request: PerplexityRequest): Promi
92
93
 
93
94
  if (!response.ok) {
94
95
  const errorText = await response.text();
95
- throw new Error(`Perplexity API error (${response.status}): ${errorText}`);
96
+ throw new WebSearchProviderError(
97
+ "perplexity",
98
+ `Perplexity API error (${response.status}): ${errorText}`,
99
+ response.status,
100
+ );
96
101
  }
97
102
 
98
103
  return response.json() as Promise<PerplexityResponse>;
@@ -257,10 +257,7 @@ export function renderWebSearchResult(
257
257
  }
258
258
  if (response.requestId) {
259
259
  metaLines.push(
260
- `${theme.fg("muted", "Request:")} ${theme.fg(
261
- "text",
262
- truncate(response.requestId, MAX_REQUEST_ID_LEN, theme.format.ellipsis),
263
- )}`,
260
+ `${theme.fg("muted", "Request:")} ${theme.fg("text", truncate(response.requestId, MAX_REQUEST_ID_LEN, theme.format.ellipsis))}`,
264
261
  );
265
262
  }
266
263
  if (searchQueries.length > 0) {
@@ -325,3 +322,8 @@ export function renderWebSearchCall(
325
322
  const text = `${theme.fg("toolTitle", "Web Search")} ${theme.fg("dim", `(${provider})`)} ${theme.fg("muted", query)}`;
326
323
  return new Text(text, 0, 0);
327
324
  }
325
+
326
+ export const webSearchToolRenderer = {
327
+ renderCall: renderWebSearchCall,
328
+ renderResult: renderWebSearchResult,
329
+ };
@@ -57,6 +57,19 @@ export interface WebSearchResponse {
57
57
  requestId?: string;
58
58
  }
59
59
 
60
+ /** Provider-specific error with optional HTTP status */
61
+ export class WebSearchProviderError extends Error {
62
+ provider: WebSearchProvider;
63
+ status?: number;
64
+
65
+ constructor(provider: WebSearchProvider, message: string, status?: number) {
66
+ super(message);
67
+ this.name = "WebSearchProviderError";
68
+ this.provider = provider;
69
+ this.status = status;
70
+ }
71
+ }
72
+
60
73
  /** Auth configuration for Anthropic */
61
74
  export interface AnthropicAuthConfig {
62
75
  apiKey: string;
@@ -1,29 +1,29 @@
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" };
4
- import { type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "./lsp/index";
7
+ import type { RenderResultOptions } from "../custom-tools/types";
8
+ import type { ToolSession } from "../sdk";
9
+ import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
5
10
  import { resolveToCwd } from "./path-utils";
11
+ import { formatDiagnostics, replaceTabs, shortenPath } from "./render-utils";
6
12
 
7
13
  const writeSchema = Type.Object({
8
14
  path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
9
15
  content: Type.String({ description: "Content to write to the file" }),
10
16
  });
11
17
 
12
- /** Options for creating the write tool */
13
- export interface WriteToolOptions {
14
- writethrough?: WritethroughCallback;
15
- }
16
-
17
18
  /** Details returned by the write tool for TUI rendering */
18
19
  export interface WriteToolDetails {
19
20
  diagnostics?: FileDiagnosticsResult;
20
21
  }
21
22
 
22
- export function createWriteTool(
23
- cwd: string,
24
- options: WriteToolOptions = {},
25
- ): AgentTool<typeof writeSchema, WriteToolDetails> {
26
- const writethrough = options.writethrough ?? writethroughNoop;
23
+ export function createWriteTool(session: ToolSession): AgentTool<typeof writeSchema, WriteToolDetails> {
24
+ const enableFormat = session.settings?.getLspFormatOnWrite() ?? true;
25
+ const enableDiagnostics = session.settings?.getLspDiagnosticsOnWrite() ?? true;
26
+ const writethrough = createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics });
27
27
  return {
28
28
  name: "write",
29
29
  label: "Write",
@@ -34,7 +34,7 @@ export function createWriteTool(
34
34
  { path, content }: { path: string; content: string },
35
35
  signal?: AbortSignal,
36
36
  ) => {
37
- const absolutePath = resolveToCwd(path, cwd);
37
+ const absolutePath = resolveToCwd(path, session.cwd);
38
38
 
39
39
  const diagnostics = await writethrough(absolutePath, content, signal);
40
40
 
@@ -59,5 +59,87 @@ export function createWriteTool(
59
59
  };
60
60
  }
61
61
 
62
- /** Default write tool using process.cwd() - for backwards compatibility */
63
- export const writeTool = createWriteTool(process.cwd());
62
+ // =============================================================================
63
+ // TUI Renderer
64
+ // =============================================================================
65
+
66
+ interface WriteRenderArgs {
67
+ path?: string;
68
+ file_path?: string;
69
+ content?: string;
70
+ }
71
+
72
+ function countLines(text: string): number {
73
+ if (!text) return 0;
74
+ return text.split("\n").length;
75
+ }
76
+
77
+ function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
78
+ const icon = uiTheme.getLangIcon(language);
79
+ if (lineCount !== null) {
80
+ return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
81
+ }
82
+ return uiTheme.fg("dim", `${icon}`);
83
+ }
84
+
85
+ export const writeToolRenderer = {
86
+ renderCall(args: WriteRenderArgs, uiTheme: Theme): Component {
87
+ const rawPath = args.file_path || args.path || "";
88
+ const filePath = shortenPath(rawPath);
89
+ const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
90
+ const text = `${uiTheme.fg("toolTitle", uiTheme.bold("Write"))} ${pathDisplay}`;
91
+ return new Text(text, 0, 0);
92
+ },
93
+
94
+ renderResult(
95
+ result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails },
96
+ { expanded }: RenderResultOptions,
97
+ uiTheme: Theme,
98
+ args?: WriteRenderArgs,
99
+ ): Component {
100
+ const rawPath = args?.file_path || args?.path || "";
101
+ const fileContent = args?.content || "";
102
+ const lang = getLanguageFromPath(rawPath);
103
+ const contentLines = fileContent
104
+ ? lang
105
+ ? highlightCode(replaceTabs(fileContent), lang)
106
+ : fileContent.split("\n")
107
+ : [];
108
+ const totalLines = contentLines.length;
109
+ const outputLines: string[] = [];
110
+
111
+ outputLines.push(formatMetadataLine(countLines(fileContent), lang ?? "text", uiTheme));
112
+
113
+ if (fileContent) {
114
+ const maxLines = expanded ? contentLines.length : 10;
115
+ const displayLines = contentLines.slice(0, maxLines);
116
+ const remaining = contentLines.length - maxLines;
117
+
118
+ outputLines.push(
119
+ "",
120
+ ...displayLines.map((line: string) =>
121
+ lang ? replaceTabs(line) : uiTheme.fg("toolOutput", replaceTabs(line)),
122
+ ),
123
+ );
124
+ if (remaining > 0) {
125
+ outputLines.push(
126
+ uiTheme.fg(
127
+ "toolOutput",
128
+ `${uiTheme.format.ellipsis} (${remaining} more lines, ${totalLines} total) ${uiTheme.format.bracketLeft}Ctrl+O to expand${uiTheme.format.bracketRight}`,
129
+ ),
130
+ );
131
+ }
132
+ }
133
+
134
+ // Show LSP diagnostics if available
135
+ if (result.details?.diagnostics) {
136
+ outputLines.push(
137
+ formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp) =>
138
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
139
+ ),
140
+ );
141
+ }
142
+
143
+ return new Text(outputLines.join("\n"), 0, 0);
144
+ },
145
+ };
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";
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { parseFrontmatter } from "./helpers";
3
3
 
4
4
  describe("parseFrontmatter", () => {
package/src/index.ts CHANGED
@@ -81,6 +81,8 @@ export { ModelRegistry } from "./core/model-registry";
81
81
  export type { PromptTemplate } from "./core/prompt-templates";
82
82
  // SDK for programmatic usage
83
83
  export {
84
+ // Tool factories
85
+ BUILTIN_TOOLS,
84
86
  type BuildSystemPromptOptions,
85
87
  buildSystemPrompt,
86
88
  type CreateAgentSessionOptions,
@@ -88,14 +90,12 @@ export {
88
90
  // Factory
89
91
  createAgentSession,
90
92
  createBashTool,
91
- // Tool factories (for custom cwd)
92
- createCodingTools,
93
93
  createEditTool,
94
94
  createFindTool,
95
95
  createGrepTool,
96
96
  createLsTool,
97
- createReadOnlyTools,
98
97
  createReadTool,
98
+ createTools,
99
99
  createWriteTool,
100
100
  // Discovery
101
101
  discoverAuthStorage,
@@ -107,8 +107,7 @@ export {
107
107
  discoverPromptTemplates,
108
108
  discoverSkills,
109
109
  loadSettings,
110
- // Pre-built tools (use process.cwd())
111
- readOnlyTools,
110
+ type ToolSession,
112
111
  } from "./core/sdk";
113
112
  export {
114
113
  type BranchSummaryEntry,
@@ -154,27 +153,17 @@ export {
154
153
  } from "./core/skills";
155
154
  // Slash commands
156
155
  export { type FileSlashCommand, loadSlashCommands as discoverSlashCommands } from "./core/slash-commands";
157
- // Tools
156
+ // Tools (detail types only - factories exported from sdk)
158
157
  export {
159
158
  type BashToolDetails,
160
- bashTool,
161
- type CodingToolsOptions,
162
- codingTools,
163
- editTool,
164
159
  type FindToolDetails,
165
- findTool,
166
160
  type GitToolDetails,
167
161
  type GrepToolDetails,
168
162
  gitTool,
169
- grepTool,
170
163
  type LsToolDetails,
171
- lsTool,
172
164
  type ReadToolDetails,
173
- readTool,
174
165
  type TruncationResult,
175
166
  type WriteToolDetails,
176
- type WriteToolOptions,
177
- writeTool,
178
167
  } from "./core/tools/index";
179
168
  export type { FileDiagnosticsResult } from "./core/tools/lsp/index";
180
169
  // Main entry point
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";
@@ -24,7 +24,6 @@ import { SessionManager } from "./core/session-manager";
24
24
  import { SettingsManager } from "./core/settings-manager";
25
25
  import { resolvePromptInput } from "./core/system-prompt";
26
26
  import { printTimings, time } from "./core/timings";
27
- import { allTools } from "./core/tools/index";
28
27
  import { runMigrations, showDeprecationWarnings } from "./migrations";
29
28
  import { InteractiveMode, installTerminalCrashHandlers, runPrintMode, runRpcMode } from "./modes/index";
30
29
  import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme";
@@ -210,7 +209,9 @@ async function buildSessionOptions(
210
209
  modelRegistry: ModelRegistry,
211
210
  settingsManager: SettingsManager,
212
211
  ): Promise<CreateAgentSessionOptions> {
213
- const options: CreateAgentSessionOptions = {};
212
+ const options: CreateAgentSessionOptions = {
213
+ cwd: parsed.cwd ?? process.cwd(),
214
+ };
214
215
 
215
216
  // Auto-discover SYSTEM.md if no CLI system prompt provided
216
217
  const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
@@ -263,8 +264,7 @@ async function buildSessionOptions(
263
264
 
264
265
  // Tools
265
266
  if (parsed.tools) {
266
- options.tools = parsed.tools.map((name) => allTools[name]);
267
- options.explicitTools = parsed.tools;
267
+ options.toolNames = parsed.tools;
268
268
  }
269
269
 
270
270
  // Skills
@@ -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";