@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.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 (191) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/types/cli/file-processor.d.ts +1 -1
  3. package/dist/types/config/settings-schema.d.ts +45 -3
  4. package/dist/types/config/settings.d.ts +1 -1
  5. package/dist/types/debug/raw-sse.d.ts +2 -0
  6. package/dist/types/edit/file-read-cache.d.ts +15 -4
  7. package/dist/types/edit/index.d.ts +3 -8
  8. package/dist/types/edit/renderer.d.ts +1 -2
  9. package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
  10. package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
  11. package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
  12. package/dist/types/eval/js/shared/runtime.d.ts +14 -8
  13. package/dist/types/eval/py/executor.d.ts +1 -2
  14. package/dist/types/eval/py/kernel.d.ts +6 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -5
  16. package/dist/types/eval/session-id.d.ts +3 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +1 -3
  18. package/dist/types/hashline/anchors.d.ts +15 -9
  19. package/dist/types/hashline/constants.d.ts +0 -2
  20. package/dist/types/hashline/diff.d.ts +1 -2
  21. package/dist/types/hashline/executor.d.ts +52 -0
  22. package/dist/types/hashline/hash.d.ts +44 -93
  23. package/dist/types/hashline/index.d.ts +2 -1
  24. package/dist/types/hashline/input.d.ts +2 -9
  25. package/dist/types/hashline/recovery.d.ts +3 -9
  26. package/dist/types/hashline/tokenizer.d.ts +91 -0
  27. package/dist/types/hashline/types.d.ts +5 -7
  28. package/dist/types/modes/components/extensions/types.d.ts +0 -4
  29. package/dist/types/modes/types.d.ts +1 -0
  30. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  31. package/dist/types/sdk.d.ts +2 -0
  32. package/dist/types/session/agent-session.d.ts +11 -15
  33. package/dist/types/session/agent-storage.d.ts +11 -10
  34. package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
  35. package/dist/types/slash-commands/types.d.ts +0 -5
  36. package/dist/types/task/executor.d.ts +2 -0
  37. package/dist/types/tool-discovery/tool-index.d.ts +0 -50
  38. package/dist/types/tools/index.d.ts +2 -8
  39. package/dist/types/tools/match-line-format.d.ts +4 -4
  40. package/dist/types/tools/output-schema-validator.d.ts +64 -0
  41. package/dist/types/tools/review.d.ts +13 -0
  42. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  43. package/dist/types/tools/search.d.ts +4 -3
  44. package/dist/types/utils/edit-mode.d.ts +1 -1
  45. package/dist/types/web/kagi.d.ts +4 -2
  46. package/dist/types/web/parallel.d.ts +4 -3
  47. package/dist/types/web/scrapers/types.d.ts +2 -1
  48. package/dist/types/web/search/index.d.ts +12 -4
  49. package/dist/types/web/search/provider.d.ts +2 -1
  50. package/dist/types/web/search/providers/anthropic.d.ts +9 -4
  51. package/dist/types/web/search/providers/base.d.ts +34 -2
  52. package/dist/types/web/search/providers/brave.d.ts +8 -1
  53. package/dist/types/web/search/providers/codex.d.ts +13 -9
  54. package/dist/types/web/search/providers/exa.d.ts +10 -1
  55. package/dist/types/web/search/providers/gemini.d.ts +20 -23
  56. package/dist/types/web/search/providers/jina.d.ts +2 -1
  57. package/dist/types/web/search/providers/kagi.d.ts +4 -1
  58. package/dist/types/web/search/providers/kimi.d.ts +10 -1
  59. package/dist/types/web/search/providers/parallel.d.ts +3 -2
  60. package/dist/types/web/search/providers/perplexity.d.ts +5 -2
  61. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  62. package/dist/types/web/search/providers/synthetic.d.ts +5 -8
  63. package/dist/types/web/search/providers/tavily.d.ts +11 -4
  64. package/dist/types/web/search/providers/utils.d.ts +8 -6
  65. package/dist/types/web/search/providers/zai.d.ts +12 -3
  66. package/package.json +7 -7
  67. package/src/cli/file-processor.ts +12 -2
  68. package/src/cli.ts +0 -8
  69. package/src/commands/commit.ts +8 -8
  70. package/src/config/prompt-templates.ts +6 -6
  71. package/src/config/settings-schema.ts +47 -3
  72. package/src/config/settings.ts +5 -5
  73. package/src/debug/raw-sse.ts +68 -3
  74. package/src/edit/file-read-cache.ts +68 -25
  75. package/src/edit/index.ts +6 -37
  76. package/src/edit/renderer.ts +9 -47
  77. package/src/edit/streaming.ts +43 -56
  78. package/src/eval/__tests__/shared-executors.test.ts +520 -0
  79. package/src/eval/js/context-manager.ts +64 -53
  80. package/src/eval/js/shared/local-module-loader.ts +265 -0
  81. package/src/eval/js/shared/prelude.txt +4 -0
  82. package/src/eval/js/shared/rewrite-imports.ts +85 -0
  83. package/src/eval/js/shared/runtime.ts +129 -86
  84. package/src/eval/js/worker-core.ts +23 -38
  85. package/src/eval/py/executor.ts +155 -84
  86. package/src/eval/py/kernel.ts +10 -1
  87. package/src/eval/py/prelude.py +22 -24
  88. package/src/eval/py/runner.py +203 -85
  89. package/src/eval/py/tool-bridge.ts +17 -10
  90. package/src/eval/session-id.ts +8 -0
  91. package/src/exec/bash-executor.ts +27 -16
  92. package/src/extensibility/extensions/runner.ts +0 -1
  93. package/src/extensibility/extensions/types.ts +1 -3
  94. package/src/hashline/anchors.ts +56 -65
  95. package/src/hashline/apply.ts +29 -31
  96. package/src/hashline/constants.ts +0 -3
  97. package/src/hashline/diff-preview.ts +4 -5
  98. package/src/hashline/diff.ts +30 -4
  99. package/src/hashline/execute.ts +91 -26
  100. package/src/hashline/executor.ts +239 -0
  101. package/src/hashline/grammar.lark +12 -10
  102. package/src/hashline/hash.ts +69 -114
  103. package/src/hashline/index.ts +2 -1
  104. package/src/hashline/input.ts +48 -41
  105. package/src/hashline/prefixes.ts +21 -11
  106. package/src/hashline/recovery.ts +63 -71
  107. package/src/hashline/stream.ts +2 -2
  108. package/src/hashline/tokenizer.ts +467 -0
  109. package/src/hashline/types.ts +6 -8
  110. package/src/internal-urls/docs-index.generated.ts +7 -7
  111. package/src/modes/components/extensions/types.ts +0 -5
  112. package/src/modes/components/session-observer-overlay.ts +11 -2
  113. package/src/modes/components/tree-selector.ts +10 -2
  114. package/src/modes/controllers/command-controller.ts +1 -3
  115. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  116. package/src/modes/controllers/selector-controller.ts +5 -5
  117. package/src/modes/types.ts +4 -1
  118. package/src/modes/utils/ui-helpers.ts +4 -0
  119. package/src/prompts/agents/explore.md +1 -1
  120. package/src/prompts/tools/ast-edit.md +1 -1
  121. package/src/prompts/tools/ast-grep.md +1 -1
  122. package/src/prompts/tools/eval.md +1 -1
  123. package/src/prompts/tools/hashline.md +73 -94
  124. package/src/prompts/tools/read.md +4 -4
  125. package/src/prompts/tools/search.md +3 -3
  126. package/src/sdk.ts +17 -23
  127. package/src/session/agent-session.ts +59 -66
  128. package/src/session/agent-storage.ts +13 -14
  129. package/src/slash-commands/acp-builtins.ts +3 -3
  130. package/src/slash-commands/types.ts +0 -6
  131. package/src/task/executor.ts +26 -57
  132. package/src/task/index.ts +8 -4
  133. package/src/tool-discovery/tool-index.ts +0 -134
  134. package/src/tools/ast-edit.ts +36 -13
  135. package/src/tools/ast-grep.ts +45 -4
  136. package/src/tools/browser/tab-worker.ts +3 -2
  137. package/src/tools/eval.ts +2 -1
  138. package/src/tools/fetch.ts +23 -14
  139. package/src/tools/index.ts +2 -8
  140. package/src/tools/irc.ts +59 -5
  141. package/src/tools/match-line-format.ts +5 -7
  142. package/src/tools/output-schema-validator.ts +132 -0
  143. package/src/tools/read.ts +142 -31
  144. package/src/tools/review.ts +23 -0
  145. package/src/tools/search-tool-bm25.ts +3 -30
  146. package/src/tools/search.ts +48 -16
  147. package/src/tools/write.ts +3 -3
  148. package/src/tools/yield.ts +32 -41
  149. package/src/utils/edit-mode.ts +1 -2
  150. package/src/utils/file-mentions.ts +2 -2
  151. package/src/web/kagi.ts +15 -6
  152. package/src/web/parallel.ts +9 -6
  153. package/src/web/scrapers/types.ts +7 -1
  154. package/src/web/scrapers/youtube.ts +13 -7
  155. package/src/web/search/index.ts +37 -11
  156. package/src/web/search/provider.ts +5 -3
  157. package/src/web/search/providers/anthropic.ts +30 -21
  158. package/src/web/search/providers/base.ts +35 -2
  159. package/src/web/search/providers/brave.ts +4 -4
  160. package/src/web/search/providers/codex.ts +118 -89
  161. package/src/web/search/providers/exa.ts +3 -2
  162. package/src/web/search/providers/gemini.ts +58 -155
  163. package/src/web/search/providers/jina.ts +4 -4
  164. package/src/web/search/providers/kagi.ts +17 -11
  165. package/src/web/search/providers/kimi.ts +29 -13
  166. package/src/web/search/providers/parallel.ts +171 -23
  167. package/src/web/search/providers/perplexity.ts +38 -37
  168. package/src/web/search/providers/searxng.ts +3 -1
  169. package/src/web/search/providers/synthetic.ts +16 -19
  170. package/src/web/search/providers/tavily.ts +23 -18
  171. package/src/web/search/providers/utils.ts +11 -17
  172. package/src/web/search/providers/zai.ts +16 -8
  173. package/dist/types/hashline/parser.d.ts +0 -7
  174. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  175. package/dist/types/tools/vim.d.ts +0 -58
  176. package/dist/types/vim/buffer.d.ts +0 -41
  177. package/dist/types/vim/commands.d.ts +0 -6
  178. package/dist/types/vim/engine.d.ts +0 -47
  179. package/dist/types/vim/parser.d.ts +0 -3
  180. package/dist/types/vim/render.d.ts +0 -25
  181. package/dist/types/vim/types.d.ts +0 -182
  182. package/src/hashline/parser.ts +0 -246
  183. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  184. package/src/prompts/tools/vim.md +0 -98
  185. package/src/tools/vim.ts +0 -949
  186. package/src/vim/buffer.ts +0 -309
  187. package/src/vim/commands.ts +0 -382
  188. package/src/vim/engine.ts +0 -2409
  189. package/src/vim/parser.ts +0 -134
  190. package/src/vim/render.ts +0 -252
  191. package/src/vim/types.ts +0 -197
@@ -4,21 +4,18 @@
4
4
  * Uses Synthetic's zero-data-retention web search API for coding agents.
5
5
  * Endpoint: POST https://api.synthetic.new/v2/search
6
6
  */
7
+ import { type AuthStorage } from "@oh-my-pi/pi-ai";
7
8
  import type { SearchResponse } from "../../../web/search/types";
8
9
  import type { SearchParams } from "./base";
9
10
  import { SearchProvider } from "./base";
10
- /** Find Synthetic API key from environment or agent.db credentials. */
11
- export declare function findApiKey(): Promise<string | null>;
11
+ /** Resolve Synthetic API key through the shared auth storage pipeline. */
12
+ export declare function findApiKey(authStorage: AuthStorage, sessionId?: string, signal?: AbortSignal): Promise<string | undefined>;
12
13
  /** Execute Synthetic web search. */
13
- export declare function searchSynthetic(params: {
14
- query: string;
15
- num_results?: number;
16
- signal?: AbortSignal;
17
- }): Promise<SearchResponse>;
14
+ export declare function searchSynthetic(params: SearchParams): Promise<SearchResponse>;
18
15
  /** Search provider for Synthetic. */
19
16
  export declare class SyntheticProvider extends SearchProvider {
20
17
  readonly id = "synthetic";
21
18
  readonly label = "Synthetic";
22
- isAvailable(): Promise<boolean>;
19
+ isAvailable(authStorage: AuthStorage): boolean;
23
20
  search(params: SearchParams): Promise<SearchResponse>;
24
21
  }
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Tavily Web Search Provider
3
+ *
4
+ * Uses Tavily's agent-focused search API to return structured results with an
5
+ * optional synthesized answer.
6
+ */
7
+ import { type AuthStorage } from "@oh-my-pi/pi-ai";
1
8
  import type { SearchResponse } from "../../../web/search/types";
2
9
  import type { SearchParams } from "./base";
3
10
  import { SearchProvider } from "./base";
@@ -7,16 +14,16 @@ export interface TavilySearchParams {
7
14
  recency?: "day" | "week" | "month" | "year";
8
15
  signal?: AbortSignal;
9
16
  }
10
- /** Find Tavily API key from environment or agent.db credentials. */
11
- export declare function findApiKey(): Promise<string | null>;
17
+ /** Find Tavily API key through AuthStorage's unified refresh pipeline. */
18
+ export declare function findApiKey(authStorage: AuthStorage, sessionId: string | undefined, signal: AbortSignal | undefined): Promise<string | null>;
12
19
  /** Exported for testing. Builds the Tavily request body from unified params. */
13
20
  export declare function buildRequestBody(params: TavilySearchParams): Record<string, unknown>;
14
21
  /** Execute Tavily web search. */
15
- export declare function searchTavily(params: TavilySearchParams): Promise<SearchResponse>;
22
+ export declare function searchTavily(params: SearchParams): Promise<SearchResponse>;
16
23
  /** Search provider for Tavily web search. */
17
24
  export declare class TavilyProvider extends SearchProvider {
18
25
  readonly id = "tavily";
19
26
  readonly label = "Tavily";
20
- isAvailable(): Promise<boolean>;
27
+ isAvailable(authStorage: AuthStorage): boolean;
21
28
  search(params: SearchParams): Promise<SearchResponse>;
22
29
  }
@@ -1,17 +1,19 @@
1
+ import type { AgentStorage } from "../../../session/agent-storage";
1
2
  import { SearchProviderError, type SearchProviderId, type SearchSource } from "../../../web/search/types";
2
3
  /**
3
4
  * Search for an API credential by checking an env-derived key first,
4
5
  * then falling back to agent.db stored credentials for the given providers.
5
6
  *
7
+ * The caller MUST supply an open {@link AgentStorage} handle so the helper
8
+ * never reaches out to global filesystem state; both the unified web_search
9
+ * chain and one-shot CLI calls open storage exactly once and thread it
10
+ * through every provider.
11
+ *
12
+ * @param storage - Open agent storage handle
6
13
  * @param envKey - Pre-resolved environment variable value (or null)
7
14
  * @param storageProviders - Provider names to look up in AgentStorage
8
15
  */
9
- export declare function findCredential(envKey: string | null | undefined, ...storageProviders: string[]): Promise<string | null>;
10
- /**
11
- * Probe whether a provider's API key lookup resolves to a truthy value.
12
- * Swallows lookup errors and reports unavailability.
13
- */
14
- export declare function isApiKeyAvailable(findApiKey: () => string | null | Promise<string | null>): Promise<boolean>;
16
+ export declare function findCredential(storage: AgentStorage | null | undefined, envKey: string | null | undefined, ...storageProviders: string[]): string | null;
15
17
  /**
16
18
  * Default hard ceiling for a single web-search round-trip. 60s tolerates
17
19
  * legitimate slow LLM-mediated responses (anthropic web_search_20250305,
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Z.AI Web Search Provider
3
+ *
4
+ * Calls Z.AI's remote MCP server (`webSearchPrime`) and adapts results into
5
+ * the unified SearchResponse shape used by the web search tool.
6
+ */
7
+ import { type AuthStorage } from "@oh-my-pi/pi-ai";
1
8
  import type { SearchResponse } from "../../../web/search/types";
2
9
  import type { SearchParams } from "./base";
3
10
  import { SearchProvider } from "./base";
@@ -5,15 +12,17 @@ export interface ZaiSearchParams {
5
12
  query: string;
6
13
  num_results?: number;
7
14
  signal?: AbortSignal;
15
+ authStorage: AuthStorage;
16
+ sessionId?: string;
8
17
  }
9
- /** Find Z.AI API credentials from environment or saved auth storage. */
10
- export declare function findApiKey(): Promise<string | null>;
18
+ /** Resolve Z.AI API credentials through the unified auth storage pipeline. */
19
+ export declare function findApiKey(authStorage: AuthStorage, sessionId?: string, signal?: AbortSignal): Promise<string | null>;
11
20
  /** Execute Z.AI web search via remote MCP endpoint. */
12
21
  export declare function searchZai(params: ZaiSearchParams): Promise<SearchResponse>;
13
22
  /** Search provider for Z.AI web search MCP. */
14
23
  export declare class ZaiProvider extends SearchProvider {
15
24
  readonly id = "zai";
16
25
  readonly label = "Z.AI";
17
- isAvailable(): Promise<boolean>;
26
+ isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean;
18
27
  search(params: SearchParams): Promise<SearchResponse>;
19
28
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.3.2",
4
+ "version": "15.4.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,12 +47,12 @@
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@babel/parser": "^7.29.3",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/omp-stats": "15.3.2",
51
- "@oh-my-pi/pi-agent-core": "15.3.2",
52
- "@oh-my-pi/pi-ai": "15.3.2",
53
- "@oh-my-pi/pi-natives": "15.3.2",
54
- "@oh-my-pi/pi-tui": "15.3.2",
55
- "@oh-my-pi/pi-utils": "15.3.2",
50
+ "@oh-my-pi/omp-stats": "15.4.1",
51
+ "@oh-my-pi/pi-agent-core": "15.4.1",
52
+ "@oh-my-pi/pi-ai": "15.4.1",
53
+ "@oh-my-pi/pi-natives": "15.4.1",
54
+ "@oh-my-pi/pi-tui": "15.4.1",
55
+ "@oh-my-pi/pi-utils": "15.4.1",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
57
  "@types/turndown": "5.0.6",
58
58
  "@xterm/headless": "^6.0.0",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Process @file CLI arguments into text content and image attachments
2
+ * Process @file CLI arguments into text, document content, and image attachments
3
3
  */
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
@@ -9,11 +9,13 @@ import chalk from "chalk";
9
9
  import { resolveReadPath } from "../tools/path-utils";
10
10
  import { formatBytes } from "../tools/render-utils";
11
11
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
12
+ import { convertFileWithMarkit } from "../utils/markit";
12
13
 
13
14
  // Keep CLI startup responsive and avoid OOM when users pass huge files.
14
15
  // If a file exceeds these limits, we include it as a path-only <file/> block.
15
16
  const MAX_CLI_TEXT_BYTES = 5 * 1024 * 1024; // 5MB
16
17
  const MAX_CLI_IMAGE_BYTES = 25 * 1024 * 1024; // 25MB
18
+ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
17
19
 
18
20
  export interface ProcessedFiles {
19
21
  text: string;
@@ -25,7 +27,7 @@ export interface ProcessFileOptions {
25
27
  autoResizeImages?: boolean;
26
28
  }
27
29
 
28
- /** Process @file arguments into text content and image attachments */
30
+ /** Process @file arguments into text, document content, and image attachments */
29
31
  export async function processFileArguments(fileArgs: string[], options?: ProcessFileOptions): Promise<ProcessedFiles> {
30
32
  const autoResizeImages = options?.autoResizeImages ?? true;
31
33
  let text = "";
@@ -43,6 +45,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process
43
45
 
44
46
  const imageMetadata = await readImageMetadata(absolutePath);
45
47
  const mimeType = imageMetadata?.mimeType;
48
+ const ext = path.extname(absolutePath).toLowerCase();
46
49
  const maxBytes = mimeType ? MAX_CLI_IMAGE_BYTES : MAX_CLI_TEXT_BYTES;
47
50
  if (stat.size > maxBytes) {
48
51
  console.error(
@@ -106,6 +109,13 @@ export async function processFileArguments(fileArgs: string[], options?: Process
106
109
  } else {
107
110
  text += `<file name="${absolutePath}"></file>\n`;
108
111
  }
112
+ } else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
113
+ const result = await convertFileWithMarkit(absolutePath);
114
+ if (result.ok) {
115
+ text += `<file name="${absolutePath}">\n${result.content}\n</file>\n`;
116
+ } else {
117
+ text += `<file name="${absolutePath}">[Cannot read ${ext} file: ${result.error || "conversion failed"}]</file>\n`;
118
+ }
109
119
  } else {
110
120
  // Handle text file
111
121
  try {
package/src/cli.ts CHANGED
@@ -1,14 +1,6 @@
1
1
  #!/usr/bin/env bun
2
- import { installH2Fetch } from "@oh-my-pi/pi-ai";
3
2
  import { APP_NAME, MIN_BUN_VERSION, procmgr, VERSION } from "@oh-my-pi/pi-utils";
4
3
 
5
- // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
6
- // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
7
- // can't toggle from JS, so we patch globalThis.fetch to pass
8
- // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
9
- // `HTTP2Unsupported`. See @oh-my-pi/pi-ai/utils/h2-fetch for details.
10
- installH2Fetch();
11
-
12
4
  // Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
13
5
  // Otherwise every child bun process (subagents, plugin installs, ptree spawns,
14
6
  // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
@@ -32,14 +32,14 @@ export default class Commit extends Command {
32
32
  };
33
33
 
34
34
  await initTheme();
35
- // The agentic commit flow opens HTTP/2 keep-alive sockets to the model
36
- // provider (via `installH2Fetch`) and spins up an AgentSession with
37
- // background async-job + extension machinery. `session.dispose()` releases
38
- // what it can, but Bun's fetch keeps idle connections warm and a few
39
- // timers (Settings autosave, OAuth refresh) stay armed long enough to
40
- // pin the event loop after the commit is already written. Mirror the
41
- // `runPrintMode` exit pattern from `main.ts` so the CLI returns to the
42
- // shell instead of stranding the user on Ctrl+C (issue #1041).
35
+ // The agentic commit flow opens keep-alive sockets to the model provider
36
+ // and spins up an AgentSession with background async-job + extension
37
+ // machinery. `session.dispose()` releases what it can, but Bun's fetch
38
+ // keeps idle connections warm and a few timers (Settings autosave, OAuth
39
+ // refresh) stay armed long enough to pin the event loop after the commit
40
+ // is already written. Mirror the `runPrintMode` exit pattern from
41
+ // `main.ts` so the CLI returns to the shell instead of stranding the user
42
+ // on Ctrl+C (issue #1041).
43
43
  await runCommitCommand(cmd);
44
44
  await postmortem.quit(0);
45
45
  }
@@ -8,7 +8,7 @@ import {
8
8
  parseFrontmatter,
9
9
  prompt,
10
10
  } from "@oh-my-pi/pi-utils";
11
- import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
11
+ import { HL_LINE_BODY_SEP } from "../hashline/hash";
12
12
  import { jtdToTypeScript } from "../tools/jtd-to-typescript";
13
13
  import { parseCommandArgs, substituteArgs } from "../utils/command-args";
14
14
 
@@ -34,7 +34,7 @@ function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; t
34
34
  const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
35
35
  const raw = typeof content === "string" ? content : String(content ?? "");
36
36
  const text = raw.replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r");
37
- const ref = `${num}${computeLineHash(num, text)}`;
37
+ const ref = `${num}`;
38
38
  return { num, text, ref };
39
39
  }
40
40
 
@@ -124,11 +124,11 @@ function resolveHashlineRef(state: HashlineHelperState, args: unknown[]): string
124
124
  }
125
125
 
126
126
  /**
127
- * {{href lineNum "content"}} — compute a real hashline ref for prompt examples.
127
+ * {{href lineNum "content"}} — compute a hashline line ref for prompt examples.
128
128
  * {{href lineNum}} — quote the ref remembered by the earlier {{hline lineNum "..."}}
129
129
  * {{href}} — quote the ref from the previous {{hline}} call.
130
130
  * {{href "[" "]"}} — wrap the previous {{hline}} ref with pre/post chars.
131
- * Returns `"lineNumBIGRAM"` (e.g., `"42nd"`), or `"[42nd]"` when pre/post are supplied.
131
+ * Returns `"lineNum"` (e.g., `"42"`), or `"[42]"` when pre/post are supplied.
132
132
  */
133
133
  prompt.registerHelper("href", function (this: unknown, ...args: unknown[]): string {
134
134
  const { positional, options } = splitHelperArgs(args);
@@ -143,7 +143,7 @@ prompt.registerHelper("hrefr", function (this: unknown, ...args: unknown[]): str
143
143
 
144
144
  /**
145
145
  * {{hline lineNum "content"}} — format a full read-style line with prefix.
146
- * Returns `"lineNumBIGRAM|content"` (pipe between anchor and content).
146
+ * Returns `"lineNum:content"` (colon between line number and content).
147
147
  */
148
148
  prompt.registerHelper("hline", function (this: unknown, ...args: unknown[]): string {
149
149
  const { positional, options } = splitHelperArgs(args);
@@ -151,7 +151,7 @@ prompt.registerHelper("hline", function (this: unknown, ...args: unknown[]): str
151
151
  const { num, ref, text } = formatHashlineRef(lineNum, content);
152
152
  const state = getHashlineHelperState(this, options);
153
153
  rememberHashlineRef(state, num, ref);
154
- return `${ref}${HL_BODY_SEP}${text}`;
154
+ return `${ref}${HL_LINE_BODY_SEP}${text}`;
155
155
  });
156
156
 
157
157
  const INLINE_ARG_SHELL_PATTERN = /\$(?:ARGUMENTS|@(?:\[\d+(?::\d*)?\])?|\d+)/;
@@ -1528,7 +1528,7 @@ export const SETTINGS_SCHEMA = {
1528
1528
  ui: {
1529
1529
  tab: "editing",
1530
1530
  label: "Edit Mode",
1531
- description: "Select the edit tool variant (replace, patch, hashline, vim, or apply_patch)",
1531
+ description: "Select the edit tool variant (replace, patch, hashline, or apply_patch)",
1532
1532
  },
1533
1533
  },
1534
1534
 
@@ -1603,7 +1603,8 @@ export const SETTINGS_SCHEMA = {
1603
1603
  ui: {
1604
1604
  tab: "editing",
1605
1605
  label: "Hash Lines",
1606
- description: "Include line hashes in read output for hashline edit mode (LINE+ID|content)",
1606
+ description:
1607
+ "Include file-hash headers and line numbers in read output for hashline edit mode (¶PATH#hash plus LINE:content)",
1607
1608
  },
1608
1609
  },
1609
1610
 
@@ -1905,6 +1906,24 @@ export const SETTINGS_SCHEMA = {
1905
1906
  },
1906
1907
  },
1907
1908
 
1909
+ "irc.timeoutMs": {
1910
+ type: "number",
1911
+ default: 120_000,
1912
+ ui: {
1913
+ tab: "tools",
1914
+ label: "IRC Timeout",
1915
+ description:
1916
+ "Drop IRC messages whose recipient does not respond within this many milliseconds (0 disables the timeout)",
1917
+ options: [
1918
+ { value: "0", label: "Disabled" },
1919
+ { value: "30000", label: "30 seconds" },
1920
+ { value: "60000", label: "1 minute" },
1921
+ { value: "120000", label: "2 minutes" },
1922
+ { value: "300000", label: "5 minutes" },
1923
+ ],
1924
+ },
1925
+ },
1926
+
1908
1927
  // Optional tools
1909
1928
 
1910
1929
  "renderMermaid.enabled": {
@@ -2383,6 +2402,17 @@ export const SETTINGS_SCHEMA = {
2383
2402
  },
2384
2403
  },
2385
2404
 
2405
+ "task.enableLsp": {
2406
+ type: "boolean",
2407
+ default: false,
2408
+ ui: {
2409
+ tab: "tasks",
2410
+ label: "LSP in Subagents",
2411
+ description:
2412
+ "Allow subagents spawned via the task tool to use the lsp tool. Off by default to keep subagents cheap; enable when LSP-aware delegation is worth the extra tokens.",
2413
+ },
2414
+ },
2415
+
2386
2416
  "task.maxRecursionDepth": {
2387
2417
  type: "number",
2388
2418
  default: 2,
@@ -2552,7 +2582,21 @@ export const SETTINGS_SCHEMA = {
2552
2582
  label: "Perplexity",
2553
2583
  description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY",
2554
2584
  },
2555
- { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
2585
+ {
2586
+ value: "anthropic",
2587
+ label: "Anthropic",
2588
+ description: "Claude's native web_search tool (uses Anthropic OAuth or ANTHROPIC_API_KEY)",
2589
+ },
2590
+ {
2591
+ value: "codex",
2592
+ label: "OpenAI",
2593
+ description: "OpenAI's native web_search (uses ChatGPT OAuth via /login openai-codex)",
2594
+ },
2595
+ {
2596
+ value: "gemini",
2597
+ label: "Gemini",
2598
+ description: "Google Search grounding via Gemini (uses google-gemini-cli or google-antigravity OAuth)",
2599
+ },
2556
2600
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
2557
2601
  { value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
2558
2602
  { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
@@ -408,7 +408,7 @@ export class Settings {
408
408
 
409
409
  /**
410
410
  * Get the edit variant for a specific model.
411
- * Returns "patch", "replace", "hashline", "vim", "apply_patch", or null (use global default).
411
+ * Returns "patch", "replace", "hashline", "apply_patch", or null (use global default).
412
412
  */
413
413
  getEditVariantForModel(model: string | undefined): EditMode | null {
414
414
  if (!model) return null;
@@ -643,22 +643,22 @@ export class Settings {
643
643
  }
644
644
  }
645
645
 
646
- // edit.mode: removed "atom" variant is now "hashline"
646
+ // edit.mode: removed "atom" and "vim" variants map back to "hashline"
647
647
  const editObj = raw.edit as Record<string, unknown> | undefined;
648
648
  if (editObj) {
649
- if (editObj.mode === "atom") {
649
+ if (editObj.mode === "atom" || editObj.mode === "vim") {
650
650
  editObj.mode = "hashline";
651
651
  }
652
652
  const modelVariants = editObj.modelVariants as Record<string, unknown> | undefined;
653
653
  if (modelVariants && typeof modelVariants === "object" && !Array.isArray(modelVariants)) {
654
654
  for (const [pattern, variant] of Object.entries(modelVariants)) {
655
- if (variant === "atom") {
655
+ if (variant === "atom" || variant === "vim") {
656
656
  modelVariants[pattern] = "hashline";
657
657
  }
658
658
  }
659
659
  }
660
660
  }
661
- if (raw["edit.mode"] === "atom") {
661
+ if (raw["edit.mode"] === "atom" || raw["edit.mode"] === "vim") {
662
662
  raw["edit.mode"] = "hashline";
663
663
  }
664
664
 
@@ -2,15 +2,56 @@ import { type Component, matchesKey, padding, replaceTabs, truncateToWidth, visi
2
2
  import { sanitizeText } from "@oh-my-pi/pi-utils";
3
3
  import { theme } from "../modes/theme/theme";
4
4
  import { copyToClipboard } from "../utils/clipboard";
5
- import { formatRawSseIsoTime, type RawSseDebugBuffer, rawSseRecordLines } from "./raw-sse-buffer";
5
+ import {
6
+ formatRawSseIsoTime,
7
+ type RawSseDebugBuffer,
8
+ type RawSseDebugRecord,
9
+ rawSseRecordLines,
10
+ } from "./raw-sse-buffer";
6
11
 
7
12
  const MIN_VIEWER_WIDTH = 20;
8
13
  const VIEWER_FRAME_LINES = 5;
14
+ // `data:` lines below this width render fine on a single row; anything wider gets pretty-printed
15
+ // across multiple `data:` lines so streamed JSON blobs stop getting clipped by `truncateToWidth`.
16
+ const PRETTY_PRINT_DATA_THRESHOLD = 100;
9
17
 
10
18
  function sanitizeFrameLine(line: string, width: number): string {
11
19
  return truncateToWidth(replaceTabs(sanitizeText(line)), width);
12
20
  }
13
21
 
22
+ // Walks the SSE wire lines and replaces single-line `data: <json>` payloads with
23
+ // multi-line `data: <indented-json>` entries when the JSON is wide enough to clip.
24
+ // Multi-line `data:` is still valid SSE (the spec joins lines with `\n`), so the
25
+ // transformed view round-trips back to the same event when copied.
26
+ /** @internal Exported for tests. */
27
+ export function expandPrettyDataLines(raw: readonly string[]): string[] {
28
+ const out: string[] = [];
29
+ for (const line of raw) {
30
+ if (!line.startsWith("data: ") || line.length <= PRETTY_PRINT_DATA_THRESHOLD) {
31
+ out.push(line);
32
+ continue;
33
+ }
34
+ const body = line.slice("data: ".length);
35
+ const trimmed = body.trim();
36
+ if (trimmed.length === 0 || (trimmed[0] !== "{" && trimmed[0] !== "[")) {
37
+ out.push(line);
38
+ continue;
39
+ }
40
+ let parsed: unknown;
41
+ try {
42
+ parsed = JSON.parse(trimmed);
43
+ } catch {
44
+ out.push(line);
45
+ continue;
46
+ }
47
+ const pretty = JSON.stringify(parsed, null, 2);
48
+ for (const prettyLine of pretty.split("\n")) {
49
+ out.push(`data: ${prettyLine}`);
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+
14
55
  export interface RawSseViewerOptions {
15
56
  buffer: RawSseDebugBuffer;
16
57
  terminalRows: number;
@@ -30,6 +71,12 @@ export class RawSseViewerComponent implements Component {
30
71
  #followTail = true;
31
72
  #lastRenderWidth = MIN_VIEWER_WIDTH;
32
73
  #statusMessage: string | undefined;
74
+ // Pretty-printed wire lines keyed by `record.sequence`. Pretty-printing is
75
+ // the JSON.parse + JSON.stringify per `data:` line, so we cache the result —
76
+ // the render path runs on every keypress and from `#maxScrollOffset()`.
77
+ // Sequences are monotonic; we prune entries below the oldest live record
78
+ // after each render so the cache tracks the buffer's eviction window.
79
+ readonly #prettyLinesCache = new Map<number, string[]>();
33
80
 
34
81
  constructor(options: RawSseViewerOptions) {
35
82
  this.#buffer = options.buffer;
@@ -131,8 +178,9 @@ export class RawSseViewerComponent implements Component {
131
178
  );
132
179
  lines.push("");
133
180
  }
181
+ const firstSequence = snapshot.records[0]?.sequence;
134
182
  for (const record of snapshot.records) {
135
- for (const line of rawSseRecordLines(record)) {
183
+ for (const line of this.#prettyLinesFor(record)) {
136
184
  lines.push(sanitizeFrameLine(line, innerWidth));
137
185
  }
138
186
  if (record.kind === "event" && record.truncated) {
@@ -140,14 +188,31 @@ export class RawSseViewerComponent implements Component {
140
188
  }
141
189
  lines.push("");
142
190
  }
191
+ if (firstSequence !== undefined) this.#pruneCache(firstSequence);
143
192
  return lines;
144
193
  }
145
194
 
195
+ #prettyLinesFor(record: RawSseDebugRecord): string[] {
196
+ const cached = this.#prettyLinesCache.get(record.sequence);
197
+ if (cached) return cached;
198
+ const expanded = expandPrettyDataLines(rawSseRecordLines(record));
199
+ this.#prettyLinesCache.set(record.sequence, expanded);
200
+ return expanded;
201
+ }
202
+
203
+ #pruneCache(firstSequence: number): void {
204
+ // Bounded by the buffer eviction rate; with `MAX_RAW_SSE_EVENTS = 1000`
205
+ // this rarely runs and only walks freshly-evicted entries.
206
+ for (const key of this.#prettyLinesCache.keys()) {
207
+ if (key < firstSequence) this.#prettyLinesCache.delete(key);
208
+ }
209
+ }
210
+
146
211
  #summaryText(): string {
147
212
  const snapshot = this.#buffer.snapshot();
148
213
  const last = snapshot.lastUpdatedAt ? ` last=${formatRawSseIsoTime(snapshot.lastUpdatedAt)}` : "";
149
214
  const follow = this.#followTail ? "follow:on" : "follow:off";
150
- return ` # raw SSE | events=${snapshot.totalEvents} records=${snapshot.records.length}${last} | ${follow} | Esc back Ctrl+C copy End follow`;
215
+ return ` # raw provider stream (SSE + WS) | events=${snapshot.totalEvents} records=${snapshot.records.length}${last} | ${follow} | Esc back Ctrl+C copy End follow`;
151
216
  }
152
217
 
153
218
  #statusText(): string {