@oh-my-pi/pi-coding-agent 15.10.4 → 15.10.6

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 (165) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/types/capability/rule-buckets.d.ts +1 -1
  3. package/dist/types/capability/rule.d.ts +6 -1
  4. package/dist/types/cli/update-cli.d.ts +11 -1
  5. package/dist/types/config/model-registry.d.ts +18 -1
  6. package/dist/types/discovery/at-imports.d.ts +15 -0
  7. package/dist/types/edit/diff.d.ts +3 -2
  8. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
  9. package/dist/types/eval/backend.d.ts +7 -0
  10. package/dist/types/eval/js/context-manager.d.ts +1 -0
  11. package/dist/types/eval/js/executor.d.ts +2 -0
  12. package/dist/types/eval/js/index.d.ts +1 -1
  13. package/dist/types/eval/js/shared/helpers.d.ts +6 -0
  14. package/dist/types/eval/js/shared/runtime.d.ts +5 -0
  15. package/dist/types/eval/js/worker-protocol.d.ts +6 -0
  16. package/dist/types/eval/py/executor.d.ts +7 -0
  17. package/dist/types/eval/py/index.d.ts +1 -1
  18. package/dist/types/exa/index.d.ts +1 -19
  19. package/dist/types/exa/mcp-client.d.ts +10 -3
  20. package/dist/types/exa/types.d.ts +0 -83
  21. package/dist/types/export/ttsr.d.ts +14 -0
  22. package/dist/types/extensibility/extensions/types.d.ts +8 -1
  23. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  24. package/dist/types/internal-urls/local-protocol.d.ts +10 -0
  25. package/dist/types/mcp/oauth-flow.d.ts +2 -2
  26. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  27. package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
  28. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +31 -2
  30. package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
  31. package/dist/types/modes/image-references.d.ts +8 -3
  32. package/dist/types/modes/interactive-mode.d.ts +9 -1
  33. package/dist/types/modes/theme/theme.d.ts +2 -1
  34. package/dist/types/modes/types.d.ts +3 -1
  35. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  36. package/dist/types/session/agent-session.d.ts +0 -2
  37. package/dist/types/task/render.d.ts +1 -0
  38. package/dist/types/tools/ask.d.ts +1 -0
  39. package/dist/types/tools/browser/tab-worker.d.ts +15 -0
  40. package/dist/types/tools/index.d.ts +17 -2
  41. package/dist/types/tools/render-utils.d.ts +1 -1
  42. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  43. package/dist/types/utils/block-context.d.ts +35 -0
  44. package/dist/types/utils/git.d.ts +6 -0
  45. package/dist/types/utils/image-loading.d.ts +12 -0
  46. package/package.json +29 -9
  47. package/src/capability/rule-buckets.ts +4 -2
  48. package/src/capability/rule.ts +10 -1
  49. package/src/cli/auth-broker-cli.ts +6 -7
  50. package/src/cli/auth-gateway-cli.ts +4 -3
  51. package/src/cli/list-models.ts +5 -0
  52. package/src/cli/update-cli.ts +138 -16
  53. package/src/commit/agentic/tools/split-commit.ts +8 -1
  54. package/src/config/model-provider-priority.ts +1 -0
  55. package/src/config/model-registry.ts +81 -2
  56. package/src/debug/index.ts +4 -8
  57. package/src/discovery/at-imports.ts +273 -0
  58. package/src/discovery/builtin-rules/index.ts +4 -0
  59. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  60. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  61. package/src/discovery/helpers.ts +2 -1
  62. package/src/edit/diff.ts +114 -4
  63. package/src/edit/hashline/diff.ts +1 -1
  64. package/src/edit/hashline/execute.ts +1 -1
  65. package/src/edit/modes/patch.ts +6 -2
  66. package/src/edit/modes/replace.ts +1 -1
  67. package/src/edit/renderer.ts +12 -2
  68. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  69. package/src/eval/backend.ts +15 -0
  70. package/src/eval/js/context-manager.ts +4 -2
  71. package/src/eval/js/executor.ts +3 -0
  72. package/src/eval/js/index.ts +7 -1
  73. package/src/eval/js/shared/helpers.ts +53 -6
  74. package/src/eval/js/shared/runtime.ts +8 -0
  75. package/src/eval/js/worker-core.ts +1 -0
  76. package/src/eval/js/worker-protocol.ts +6 -0
  77. package/src/eval/py/executor.ts +12 -0
  78. package/src/eval/py/index.ts +7 -1
  79. package/src/eval/py/prelude.py +43 -4
  80. package/src/eval/py/runner.py +1 -0
  81. package/src/exa/index.ts +1 -26
  82. package/src/exa/mcp-client.ts +10 -10
  83. package/src/exa/types.ts +0 -97
  84. package/src/export/ttsr.ts +122 -1
  85. package/src/extensibility/extensions/types.ts +8 -1
  86. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  87. package/src/extensibility/plugins/doctor.ts +1 -1
  88. package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
  89. package/src/goals/tools/goal-tool.ts +1 -1
  90. package/src/internal-urls/docs-index.generated.ts +7 -6
  91. package/src/internal-urls/local-protocol.ts +13 -0
  92. package/src/lsp/render.ts +8 -6
  93. package/src/mcp/oauth-flow.ts +3 -3
  94. package/src/mcp/render.ts +7 -1
  95. package/src/modes/components/agent-dashboard.ts +6 -4
  96. package/src/modes/components/custom-editor.ts +12 -6
  97. package/src/modes/components/login-dialog.ts +1 -1
  98. package/src/modes/components/oauth-selector.ts +4 -4
  99. package/src/modes/components/read-tool-group.ts +10 -3
  100. package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
  101. package/src/modes/components/status-line/index.ts +1 -0
  102. package/src/modes/components/status-line/types.ts +23 -8
  103. package/src/modes/components/tool-execution.ts +1 -1
  104. package/src/modes/components/transcript-container.ts +17 -10
  105. package/src/modes/components/user-message.ts +6 -3
  106. package/src/modes/components/welcome.ts +1 -1
  107. package/src/modes/controllers/event-controller.ts +8 -0
  108. package/src/modes/controllers/extension-ui-controller.ts +143 -127
  109. package/src/modes/controllers/input-controller.ts +60 -11
  110. package/src/modes/controllers/mcp-command-controller.ts +52 -17
  111. package/src/modes/controllers/selector-controller.ts +4 -11
  112. package/src/modes/controllers/ssh-command-controller.ts +2 -2
  113. package/src/modes/image-references.ts +13 -7
  114. package/src/modes/interactive-mode.ts +35 -3
  115. package/src/modes/rpc/rpc-mode.ts +1 -1
  116. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
  117. package/src/modes/theme/theme.ts +95 -1
  118. package/src/modes/types.ts +3 -1
  119. package/src/modes/utils/ui-helpers.ts +14 -5
  120. package/src/prompts/tools/bash.md +1 -1
  121. package/src/prompts/tools/eval.md +4 -4
  122. package/src/sdk.ts +31 -14
  123. package/src/session/agent-session.ts +290 -196
  124. package/src/session/session-manager.ts +1 -1
  125. package/src/slash-commands/builtin-registry.ts +9 -1
  126. package/src/system-prompt.ts +15 -9
  127. package/src/task/index.ts +9 -1
  128. package/src/task/render.ts +36 -14
  129. package/src/tools/ask.ts +14 -5
  130. package/src/tools/bash-interactive.ts +1 -1
  131. package/src/tools/bash.ts +14 -2
  132. package/src/tools/browser/render.ts +5 -2
  133. package/src/tools/browser/tab-worker.ts +211 -91
  134. package/src/tools/debug.ts +5 -2
  135. package/src/tools/eval-render.ts +6 -3
  136. package/src/tools/eval.ts +1 -1
  137. package/src/tools/gh-renderer.ts +29 -15
  138. package/src/tools/index.ts +32 -4
  139. package/src/tools/inspect-image-renderer.ts +12 -5
  140. package/src/tools/job.ts +9 -6
  141. package/src/tools/memory-render.ts +19 -5
  142. package/src/tools/read.ts +165 -18
  143. package/src/tools/render-utils.ts +3 -1
  144. package/src/tools/resolve.ts +1 -1
  145. package/src/tools/review.ts +1 -1
  146. package/src/tools/ssh.ts +4 -1
  147. package/src/tools/todo.ts +8 -1
  148. package/src/tools/tool-timeouts.ts +1 -1
  149. package/src/tools/write.ts +1 -1
  150. package/src/tui/code-cell.ts +1 -1
  151. package/src/utils/block-context.ts +312 -0
  152. package/src/utils/git.ts +41 -0
  153. package/src/utils/image-loading.ts +31 -1
  154. package/src/web/search/providers/codex.ts +1 -1
  155. package/src/web/search/render.ts +14 -6
  156. package/dist/types/exa/factory.d.ts +0 -13
  157. package/dist/types/exa/render.d.ts +0 -19
  158. package/dist/types/exa/researcher.d.ts +0 -9
  159. package/dist/types/exa/search.d.ts +0 -9
  160. package/dist/types/exa/websets.d.ts +0 -9
  161. package/src/exa/factory.ts +0 -60
  162. package/src/exa/render.ts +0 -244
  163. package/src/exa/researcher.ts +0 -36
  164. package/src/exa/search.ts +0 -47
  165. package/src/exa/websets.ts +0 -248
@@ -0,0 +1,312 @@
1
+ import { enclosingBlockBoundaries } from "@oh-my-pi/pi-natives";
2
+ import { logger } from "@oh-my-pi/pi-utils";
3
+
4
+ const OPEN_TO_CLOSE: Record<string, string> = {
5
+ "(": ")",
6
+ "[": "]",
7
+ "{": "}",
8
+ };
9
+
10
+ const CLOSE_TO_OPEN: Record<string, string> = {
11
+ ")": "(",
12
+ "]": "[",
13
+ "}": "{",
14
+ };
15
+
16
+ export interface LineSpan {
17
+ startLine: number;
18
+ endLine: number;
19
+ }
20
+
21
+ /** Where the source came from, so tree-sitter can pick a grammar. */
22
+ export interface BlockContextSource {
23
+ path?: string;
24
+ lang?: string;
25
+ }
26
+
27
+ export type LineEntry = { kind: "line"; lineNumber: number; text: string; context: boolean } | { kind: "ellipsis" };
28
+
29
+ interface StackEntry {
30
+ opener: string;
31
+ lineNumber: number;
32
+ text: string;
33
+ visible: boolean;
34
+ }
35
+
36
+ type ScannerMode = "code" | "single" | "double" | "template" | "blockComment";
37
+
38
+ function normalizeLineSpans(spans: readonly LineSpan[], totalLines: number): LineSpan[] {
39
+ if (totalLines <= 0) return [];
40
+ const normalized: LineSpan[] = [];
41
+ for (const span of spans) {
42
+ const startLine = Math.max(1, Math.trunc(span.startLine));
43
+ const endLine = Math.min(totalLines, Math.trunc(span.endLine));
44
+ if (endLine < startLine) continue;
45
+ normalized.push({ startLine, endLine });
46
+ }
47
+ if (normalized.length <= 1) return normalized;
48
+ normalized.sort((left, right) => left.startLine - right.startLine || left.endLine - right.endLine);
49
+ const merged: LineSpan[] = [];
50
+ for (const span of normalized) {
51
+ const previous = merged[merged.length - 1];
52
+ if (previous && span.startLine <= previous.endLine + 1) {
53
+ previous.endLine = Math.max(previous.endLine, span.endLine);
54
+ continue;
55
+ }
56
+ merged.push({ ...span });
57
+ }
58
+ return merged;
59
+ }
60
+
61
+ function visibleLineNumbers(spans: readonly LineSpan[]): Set<number> {
62
+ const visible = new Set<number>();
63
+ for (const span of spans) {
64
+ for (let line = span.startLine; line <= span.endLine; line++) {
65
+ visible.add(line);
66
+ }
67
+ }
68
+ return visible;
69
+ }
70
+
71
+ function hasEveryLineVisible(visible: ReadonlySet<number>, totalLines: number): boolean {
72
+ return totalLines > 0 && visible.size >= totalLines;
73
+ }
74
+
75
+ /** Collapse a set of visible line numbers into sorted, merged inclusive spans. */
76
+ function visibleSetToSpans(visible: ReadonlySet<number>): LineSpan[] {
77
+ const sorted = [...visible].sort((left, right) => left - right);
78
+ const spans: LineSpan[] = [];
79
+ for (const line of sorted) {
80
+ const previous = spans[spans.length - 1];
81
+ if (previous && line <= previous.endLine + 1) {
82
+ previous.endLine = line;
83
+ continue;
84
+ }
85
+ spans.push({ startLine: line, endLine: line });
86
+ }
87
+ return spans;
88
+ }
89
+
90
+ /**
91
+ * Tree-sitter-backed block boundaries. For each multi-line named node whose
92
+ * span crosses the visible window, the native side returns the boundary line
93
+ * outside that window (closer when the opener is shown, opener when the closer
94
+ * is shown). Returns `null` when the language is unrecognized or the source has
95
+ * a syntax error so the caller can fall back to a lexical bracket scan.
96
+ */
97
+ function nativeBlockContext(
98
+ fullLines: readonly string[],
99
+ visible: ReadonlySet<number>,
100
+ source: BlockContextSource,
101
+ ): Map<number, string> | null {
102
+ if (!source.path && !source.lang) return null;
103
+ const ranges = visibleSetToSpans(visible);
104
+ if (ranges.length === 0) return new Map();
105
+ let boundaries: number[] | null;
106
+ try {
107
+ boundaries = enclosingBlockBoundaries({
108
+ code: fullLines.join("\n"),
109
+ path: source.path,
110
+ lang: source.lang,
111
+ ranges,
112
+ });
113
+ } catch (error) {
114
+ logger.debug("enclosingBlockBoundaries failed; using lexical bracket fallback", { error });
115
+ return null;
116
+ }
117
+ if (boundaries === null) return null;
118
+ const context = new Map<number, string>();
119
+ for (const lineNumber of boundaries) {
120
+ if (visible.has(lineNumber)) continue;
121
+ context.set(lineNumber, fullLines[lineNumber - 1] ?? "");
122
+ }
123
+ return context;
124
+ }
125
+
126
+ function findMatchingStackIndex(stack: readonly StackEntry[], opener: string): number {
127
+ for (let index = stack.length - 1; index >= 0; index--) {
128
+ if (stack[index].opener === opener) return index;
129
+ }
130
+ return -1;
131
+ }
132
+
133
+ function isHashCommentStart(line: string, index: number): boolean {
134
+ if (line[index] !== "#") return false;
135
+ for (let i = 0; i < index; i++) {
136
+ const ch = line[i];
137
+ if (ch !== " " && ch !== "\t") return false;
138
+ }
139
+ return true;
140
+ }
141
+
142
+ /**
143
+ * Lexical bracket-matching fallback for sources tree-sitter can't parse
144
+ * (unknown extensions, syntax errors). Pairs `()[]{}` while skipping strings
145
+ * and line/block comments, and reports the matching line when one endpoint is
146
+ * visible and the other is not.
147
+ */
148
+ function lexicalBracketContext(fullLines: readonly string[], visible: ReadonlySet<number>): Map<number, string> {
149
+ const context = new Map<number, string>();
150
+ const stack: StackEntry[] = [];
151
+ let mode: ScannerMode = "code";
152
+ let escaped = false;
153
+
154
+ for (let lineIndex = 0; lineIndex < fullLines.length; lineIndex++) {
155
+ const lineNumber = lineIndex + 1;
156
+ const line = fullLines[lineIndex] ?? "";
157
+ const lineVisible = visible.has(lineNumber);
158
+ let index = 0;
159
+ while (index < line.length) {
160
+ const ch = line[index];
161
+ const next = index + 1 < line.length ? line[index + 1] : "";
162
+
163
+ if (mode === "blockComment") {
164
+ if (ch === "*" && next === "/") {
165
+ mode = "code";
166
+ index += 2;
167
+ continue;
168
+ }
169
+ index++;
170
+ continue;
171
+ }
172
+
173
+ if (mode === "single" || mode === "double" || mode === "template") {
174
+ if (escaped) {
175
+ escaped = false;
176
+ index++;
177
+ continue;
178
+ }
179
+ if (ch === "\\") {
180
+ escaped = true;
181
+ index++;
182
+ continue;
183
+ }
184
+ if (
185
+ (mode === "single" && ch === "'") ||
186
+ (mode === "double" && ch === '"') ||
187
+ (mode === "template" && ch === "`")
188
+ ) {
189
+ mode = "code";
190
+ }
191
+ index++;
192
+ continue;
193
+ }
194
+
195
+ if (ch === "/" && next === "/") break;
196
+ if (ch === "/" && next === "*") {
197
+ mode = "blockComment";
198
+ index += 2;
199
+ continue;
200
+ }
201
+ if (isHashCommentStart(line, index)) break;
202
+ if (ch === "'") {
203
+ mode = "single";
204
+ escaped = false;
205
+ index++;
206
+ continue;
207
+ }
208
+ if (ch === '"') {
209
+ mode = "double";
210
+ escaped = false;
211
+ index++;
212
+ continue;
213
+ }
214
+ if (ch === "`") {
215
+ mode = "template";
216
+ escaped = false;
217
+ index++;
218
+ continue;
219
+ }
220
+
221
+ if (OPEN_TO_CLOSE[ch]) {
222
+ stack.push({ opener: ch, lineNumber, text: line, visible: lineVisible });
223
+ index++;
224
+ continue;
225
+ }
226
+
227
+ const opener = CLOSE_TO_OPEN[ch];
228
+ if (opener) {
229
+ const matchIndex = findMatchingStackIndex(stack, opener);
230
+ if (matchIndex !== -1) {
231
+ const [matched] = stack.splice(matchIndex);
232
+ if (matched) {
233
+ if (lineVisible && !matched.visible) context.set(matched.lineNumber, matched.text);
234
+ if (matched.visible && !lineVisible) context.set(lineNumber, line);
235
+ }
236
+ }
237
+ }
238
+
239
+ index++;
240
+ }
241
+
242
+ if (mode === "single" || mode === "double") {
243
+ mode = "code";
244
+ escaped = false;
245
+ }
246
+ }
247
+
248
+ for (const lineNumber of visible) context.delete(lineNumber);
249
+ return context;
250
+ }
251
+
252
+ /**
253
+ * Resolve the off-window boundary lines for a visible window: tree-sitter
254
+ * syntactic spans first (covers brace and indentation languages), falling back
255
+ * to a lexical bracket scan when the grammar is unavailable. Returns a map of
256
+ * `lineNumber → source text` for the lines to surface, never including a line
257
+ * already visible.
258
+ */
259
+ export function findBlockContextLines(
260
+ fullLines: readonly string[],
261
+ visibleInput: ReadonlySet<number> | readonly number[],
262
+ source: BlockContextSource = {},
263
+ ): Map<number, string> {
264
+ const visible = visibleInput instanceof Set ? visibleInput : new Set(visibleInput);
265
+ if (visible.size === 0 || hasEveryLineVisible(visible, fullLines.length)) return new Map();
266
+ return nativeBlockContext(fullLines, visible, source) ?? lexicalBracketContext(fullLines, visible);
267
+ }
268
+
269
+ /**
270
+ * Build display entries for `visibleSpans` plus any off-window block-boundary
271
+ * lines, in source order, with `{ kind: "ellipsis" }` markers inserted across
272
+ * non-contiguous gaps. `options.lineText` lets callers substitute display text
273
+ * (e.g. column-truncated lines) for a given line number.
274
+ */
275
+ export function buildLineEntriesWithBlockContext(
276
+ fullLines: readonly string[],
277
+ visibleSpans: readonly LineSpan[],
278
+ source: BlockContextSource = {},
279
+ options: {
280
+ lineText?: (lineNumber: number, sourceText: string, context: boolean) => string;
281
+ } = {},
282
+ ): LineEntry[] {
283
+ const spans = normalizeLineSpans(visibleSpans, fullLines.length);
284
+ const visible = visibleLineNumbers(spans);
285
+ const context = findBlockContextLines(fullLines, visible, source);
286
+ const allLines = new Set<number>(visible);
287
+ for (const lineNumber of context.keys()) allLines.add(lineNumber);
288
+
289
+ const sorted = [...allLines].sort((left, right) => left - right);
290
+ const entries: LineEntry[] = [];
291
+ let previousLine: number | undefined;
292
+ for (const lineNumber of sorted) {
293
+ if (previousLine !== undefined && lineNumber > previousLine + 1) {
294
+ entries.push({ kind: "ellipsis" });
295
+ }
296
+ const sourceText = fullLines[lineNumber - 1] ?? "";
297
+ const isContext = context.has(lineNumber);
298
+ entries.push({
299
+ kind: "line",
300
+ lineNumber,
301
+ text: options.lineText?.(lineNumber, sourceText, isContext) ?? sourceText,
302
+ context: isContext,
303
+ });
304
+ previousLine = lineNumber;
305
+ }
306
+
307
+ return entries;
308
+ }
309
+
310
+ export function lineEntriesToPlainText(entries: readonly LineEntry[], ellipsis = "…"): string {
311
+ return entries.map(entry => (entry.kind === "ellipsis" ? ellipsis : entry.text)).join("\n");
312
+ }
package/src/utils/git.ts CHANGED
@@ -45,6 +45,10 @@ export interface StageHunksOptions {
45
45
  readonly rawDiff?: string;
46
46
  readonly signal?: AbortSignal;
47
47
  }
48
+ export interface HunkSelectionValidationError {
49
+ readonly path: string;
50
+ readonly message: string;
51
+ }
48
52
 
49
53
  export interface DiffOptions {
50
54
  readonly allowFailure?: boolean;
@@ -678,6 +682,43 @@ function selectHunks(file: FileHunks, selector: HunkSelection["hunks"]): FileHun
678
682
  return file.hunks;
679
683
  }
680
684
 
685
+ export function createHunkSelectionValidator(
686
+ rawDiff: string,
687
+ ): (selections: readonly HunkSelection[]) => HunkSelectionValidationError[] {
688
+ const fileDiffMap = new Map(parseFileDiffs(rawDiff).map(entry => [entry.filename, entry]));
689
+ return selections => validateHunkSelectionsFromMap(fileDiffMap, selections);
690
+ }
691
+
692
+ function validateHunkSelectionsFromMap(
693
+ fileDiffMap: ReadonlyMap<string, FileDiff>,
694
+ selections: readonly HunkSelection[],
695
+ ): HunkSelectionValidationError[] {
696
+ const errors: HunkSelectionValidationError[] = [];
697
+
698
+ for (const selection of selections) {
699
+ const fileDiff = fileDiffMap.get(selection.path);
700
+ if (!fileDiff) continue;
701
+ if (selection.hunks.type === "all") continue;
702
+ if (fileDiff.isBinary) {
703
+ errors.push({ path: selection.path, message: `Cannot select hunks for binary file ${selection.path}` });
704
+ continue;
705
+ }
706
+ const selected = selectHunks(parseFileHunks(fileDiff), selection.hunks);
707
+ if (selected.length === 0) {
708
+ errors.push({ path: selection.path, message: `No hunks selected for ${selection.path}` });
709
+ }
710
+ }
711
+
712
+ return errors;
713
+ }
714
+
715
+ export function validateHunkSelections(
716
+ rawDiff: string,
717
+ selections: readonly HunkSelection[],
718
+ ): HunkSelectionValidationError[] {
719
+ return createHunkSelectionValidator(rawDiff)(selections);
720
+ }
721
+
681
722
  function parseStatusPorcelain(text: string): GitStatusSummary {
682
723
  let staged = 0;
683
724
  let unstaged = 0;
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import type { ImageContent } from "@oh-my-pi/pi-ai";
3
3
  import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils";
4
4
  import { resolveReadPath } from "../tools/path-utils";
5
- import { formatDimensionNote, resizeImage } from "./image-resize";
5
+ import { formatDimensionNote, type ImageResizeOptions, resizeImage } from "./image-resize";
6
6
 
7
7
  export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
8
8
  export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
@@ -50,6 +50,36 @@ export async function ensureSupportedImageInput(image: ImageContent): Promise<Im
50
50
  }
51
51
  }
52
52
 
53
+ export interface NormalizeModelContextImagesOptions {
54
+ resize?: ImageResizeOptions;
55
+ }
56
+
57
+ /**
58
+ * Normalize image blocks before they enter agent/model context. This keeps
59
+ * provider request construction from having to resize an unbounded batch of
60
+ * large images on the streaming hot path. Images are processed sequentially on
61
+ * purpose: `resizeImage` may fan out multiple encoders for one image, so the
62
+ * outer image batch must stay bounded.
63
+ */
64
+ export async function normalizeModelContextImages(
65
+ images: ImageContent[] | undefined,
66
+ options?: NormalizeModelContextImagesOptions,
67
+ ): Promise<ImageContent[] | undefined> {
68
+ if (!images || images.length === 0) return undefined;
69
+ const normalized: ImageContent[] = [];
70
+ for (const image of images) {
71
+ try {
72
+ const resized = await resizeImage(image, options?.resize);
73
+ normalized.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
74
+ } catch {
75
+ // Preserve existing caller behavior for decode/resize failures: keep the
76
+ // user's image block rather than dropping it from the turn.
77
+ normalized.push(image);
78
+ }
79
+ }
80
+ return normalized;
81
+ }
82
+
53
83
  export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
54
84
  const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
55
85
  const resolvedPath = options.resolvedPath ?? resolveReadPath(options.path, options.cwd);
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import * as os from "node:os";
10
10
  import { type AuthStorage, getBundledModels } from "@oh-my-pi/pi-ai";
11
- import { decodeJwt } from "@oh-my-pi/pi-ai/utils/oauth/openai-codex";
11
+ import { decodeJwt } from "@oh-my-pi/pi-ai/oauth/openai-codex";
12
12
  import { $env, readSseJson } from "@oh-my-pi/pi-utils";
13
13
  import packageJson from "../../../../package.json" with { type: "json" };
14
14
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
@@ -117,13 +117,21 @@ export function renderSearchResult(
117
117
  : searchQueries[0]
118
118
  ? truncateToWidth(searchQueries[0], 80)
119
119
  : undefined;
120
+ const success = sourceCount > 0;
120
121
  const header = renderStatusLine(
121
- {
122
- icon: sourceCount > 0 ? "success" : "warning",
123
- title: "Web Search",
124
- description: providerLabel,
125
- meta: [formatCount("source", sourceCount)],
126
- },
122
+ success
123
+ ? {
124
+ iconOverride: theme.styledSymbol("tool.webSearch", "accent"),
125
+ title: "Web Search",
126
+ description: providerLabel,
127
+ meta: [formatCount("source", sourceCount)],
128
+ }
129
+ : {
130
+ icon: "warning",
131
+ title: "Web Search",
132
+ description: providerLabel,
133
+ meta: [formatCount("source", sourceCount)],
134
+ },
127
135
  theme,
128
136
  );
129
137
 
@@ -1,13 +0,0 @@
1
- /**
2
- * Shared factory for creating Exa tools with consistent error handling and response formatting.
3
- */
4
- import type { TSchema } from "@oh-my-pi/pi-ai";
5
- import type { CustomTool } from "../extensibility/custom-tools/types";
6
- import type { ExaRenderDetails } from "./types";
7
- /** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
8
- export declare function createExaTool(name: string, label: string, description: string, parameters: TSchema, mcpToolName: string, options?: {
9
- /** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
10
- formatResponse?: boolean;
11
- /** Transform params before passing to callExaTool */
12
- transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
13
- }): CustomTool<TSchema, ExaRenderDetails>;
@@ -1,19 +0,0 @@
1
- /**
2
- * Exa TUI Rendering
3
- *
4
- * Tree-based rendering with collapsed/expanded states for Exa search results.
5
- */
6
- import type { Component } from "@oh-my-pi/pi-tui";
7
- import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
- import type { Theme } from "../modes/theme/theme";
9
- import type { ExaRenderDetails } from "./types";
10
- /** Render Exa result with tree-based layout */
11
- export declare function renderExaResult(result: {
12
- content: Array<{
13
- type: string;
14
- text?: string;
15
- }>;
16
- details?: ExaRenderDetails;
17
- }, options: RenderResultOptions, uiTheme: Theme): Component;
18
- /** Render Exa call (query/args preview) */
19
- export declare function renderExaCall(args: Record<string, unknown>, toolName: string, uiTheme: Theme): Component;
@@ -1,9 +0,0 @@
1
- /**
2
- * Exa Researcher Tools
3
- *
4
- * Async research tasks with polling for completion.
5
- */
6
- import type { TSchema } from "@oh-my-pi/pi-ai";
7
- import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import type { ExaRenderDetails } from "./types";
9
- export declare const researcherTools: CustomTool<TSchema, ExaRenderDetails>[];
@@ -1,9 +0,0 @@
1
- /**
2
- * Exa Search Tools
3
- *
4
- * Basic neural/keyword search, deep research, code search, and URL crawling.
5
- */
6
- import type { TSchema } from "@oh-my-pi/pi-ai";
7
- import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import type { ExaRenderDetails } from "./types";
9
- export declare const searchTools: CustomTool<TSchema, ExaRenderDetails>[];
@@ -1,9 +0,0 @@
1
- /**
2
- * Exa Websets Tools
3
- *
4
- * CRUD operations for websets, items, searches, enrichments, and monitoring.
5
- */
6
- import type { TSchema } from "@oh-my-pi/pi-ai";
7
- import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import type { ExaRenderDetails } from "./types";
9
- export declare const websetsTools: CustomTool<TSchema, ExaRenderDetails>[];
@@ -1,60 +0,0 @@
1
- /**
2
- * Shared factory for creating Exa tools with consistent error handling and response formatting.
3
- */
4
- import type { TSchema } from "@oh-my-pi/pi-ai";
5
- import type { CustomTool } from "../extensibility/custom-tools/types";
6
- import { callExaTool, findApiKey, formatGenericResponse, formatSearchResults, isSearchResponse } from "./mcp-client";
7
- import type { ExaRenderDetails } from "./types";
8
-
9
- /** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
10
- export function createExaTool(
11
- name: string,
12
- label: string,
13
- description: string,
14
- parameters: TSchema,
15
- mcpToolName: string,
16
- options?: {
17
- /** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
18
- formatResponse?: boolean;
19
- /** Transform params before passing to callExaTool */
20
- transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
21
- },
22
- ): CustomTool<TSchema, ExaRenderDetails> {
23
- const formatResponse = options?.formatResponse ?? true;
24
- const transformParams = options?.transformParams;
25
-
26
- return {
27
- name,
28
- label,
29
- description,
30
- parameters,
31
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
32
- try {
33
- const apiKey = findApiKey();
34
- // Exa MCP endpoint is publicly accessible; API key is optional
35
- const rawArgs = params as Record<string, unknown>;
36
- const args = transformParams ? transformParams(rawArgs) : rawArgs;
37
- const response = await callExaTool(mcpToolName, args, apiKey);
38
-
39
- if (formatResponse && isSearchResponse(response)) {
40
- const formatted = formatSearchResults(response);
41
- return {
42
- content: [{ type: "text" as const, text: formatted }],
43
- details: { response, toolName: name },
44
- };
45
- }
46
-
47
- return {
48
- content: [{ type: "text" as const, text: formatGenericResponse(response) }],
49
- details: { raw: response, toolName: name },
50
- };
51
- } catch (error) {
52
- const message = error instanceof Error ? error.message : String(error);
53
- return {
54
- content: [{ type: "text" as const, text: `Error: ${message}` }],
55
- details: { error: message, toolName: name },
56
- };
57
- }
58
- },
59
- };
60
- }