@oh-my-pi/pi-coding-agent 15.10.3 → 15.10.5

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 (161) hide show
  1. package/CHANGELOG.md +72 -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/__tests__/js-context-manager.test.d.ts +1 -0
  10. package/dist/types/eval/backend.d.ts +7 -0
  11. package/dist/types/eval/bridge-timeout.d.ts +1 -1
  12. package/dist/types/eval/{llm-bridge.d.ts → completion-bridge.d.ts} +8 -8
  13. package/dist/types/eval/idle-timeout.d.ts +1 -1
  14. package/dist/types/eval/js/context-manager.d.ts +1 -0
  15. package/dist/types/eval/js/executor.d.ts +2 -0
  16. package/dist/types/eval/js/index.d.ts +1 -1
  17. package/dist/types/eval/js/shared/helpers.d.ts +6 -0
  18. package/dist/types/eval/js/shared/runtime.d.ts +5 -0
  19. package/dist/types/eval/js/worker-protocol.d.ts +6 -0
  20. package/dist/types/eval/py/executor.d.ts +7 -0
  21. package/dist/types/eval/py/index.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +8 -1
  24. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  25. package/dist/types/internal-urls/local-protocol.d.ts +10 -0
  26. package/dist/types/mcp/oauth-flow.d.ts +2 -2
  27. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  28. package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
  29. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  30. package/dist/types/modes/components/status-line/types.d.ts +31 -2
  31. package/dist/types/modes/image-references.d.ts +8 -3
  32. package/dist/types/modes/interactive-mode.d.ts +1 -1
  33. package/dist/types/modes/theme/theme.d.ts +2 -1
  34. package/dist/types/modes/types.d.ts +2 -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/tools/ask.d.ts +1 -0
  38. package/dist/types/tools/browser/tab-worker.d.ts +15 -0
  39. package/dist/types/tools/index.d.ts +17 -0
  40. package/dist/types/tools/render-utils.d.ts +1 -1
  41. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  42. package/dist/types/utils/block-context.d.ts +35 -0
  43. package/dist/types/utils/image-loading.d.ts +12 -0
  44. package/package.json +29 -9
  45. package/src/capability/rule-buckets.ts +4 -2
  46. package/src/capability/rule.ts +10 -1
  47. package/src/cli/auth-broker-cli.ts +6 -7
  48. package/src/cli/auth-gateway-cli.ts +1 -1
  49. package/src/cli/list-models.ts +5 -0
  50. package/src/cli/update-cli.ts +138 -16
  51. package/src/config/model-registry.ts +81 -2
  52. package/src/debug/index.ts +4 -8
  53. package/src/discovery/at-imports.ts +273 -0
  54. package/src/discovery/builtin-rules/index.ts +4 -0
  55. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  56. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  57. package/src/discovery/helpers.ts +2 -1
  58. package/src/edit/diff.ts +114 -4
  59. package/src/edit/hashline/diff.ts +1 -1
  60. package/src/edit/hashline/execute.ts +1 -1
  61. package/src/edit/modes/patch.ts +6 -2
  62. package/src/edit/modes/replace.ts +1 -1
  63. package/src/edit/renderer.ts +12 -2
  64. package/src/eval/__tests__/agent-bridge.test.ts +13 -0
  65. package/src/eval/__tests__/{llm-bridge.test.ts → completion-bridge.test.ts} +60 -54
  66. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  67. package/src/eval/__tests__/js-context-manager.test.ts +241 -0
  68. package/src/eval/agent-bridge.ts +6 -1
  69. package/src/eval/backend.ts +15 -0
  70. package/src/eval/bridge-timeout.ts +1 -1
  71. package/src/eval/{llm-bridge.ts → completion-bridge.ts} +30 -27
  72. package/src/eval/idle-timeout.ts +1 -1
  73. package/src/eval/js/context-manager.ts +70 -8
  74. package/src/eval/js/executor.ts +3 -0
  75. package/src/eval/js/index.ts +7 -1
  76. package/src/eval/js/shared/helpers.ts +53 -6
  77. package/src/eval/js/shared/prelude.txt +4 -4
  78. package/src/eval/js/shared/runtime.ts +8 -0
  79. package/src/eval/js/tool-bridge.ts +3 -3
  80. package/src/eval/js/worker-core.ts +1 -0
  81. package/src/eval/js/worker-entry.ts +6 -0
  82. package/src/eval/js/worker-protocol.ts +6 -0
  83. package/src/eval/py/executor.ts +12 -0
  84. package/src/eval/py/index.ts +7 -1
  85. package/src/eval/py/prelude.py +46 -7
  86. package/src/eval/py/runner.py +1 -0
  87. package/src/exa/render.ts +1 -1
  88. package/src/export/ttsr.ts +122 -1
  89. package/src/extensibility/extensions/types.ts +8 -1
  90. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  91. package/src/extensibility/plugins/doctor.ts +1 -1
  92. package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
  93. package/src/goals/tools/goal-tool.ts +1 -1
  94. package/src/internal-urls/docs-index.generated.ts +8 -6
  95. package/src/internal-urls/local-protocol.ts +13 -0
  96. package/src/lsp/render.ts +8 -6
  97. package/src/mcp/oauth-flow.ts +3 -3
  98. package/src/mcp/render.ts +7 -1
  99. package/src/modes/components/custom-editor.ts +12 -6
  100. package/src/modes/components/login-dialog.ts +1 -1
  101. package/src/modes/components/oauth-selector.ts +4 -4
  102. package/src/modes/components/read-tool-group.ts +10 -3
  103. package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
  104. package/src/modes/components/status-line/index.ts +1 -0
  105. package/src/modes/components/status-line/types.ts +23 -8
  106. package/src/modes/components/tips.txt +1 -1
  107. package/src/modes/components/tool-execution.ts +1 -1
  108. package/src/modes/components/transcript-container.ts +17 -10
  109. package/src/modes/components/user-message.ts +6 -3
  110. package/src/modes/components/welcome.ts +1 -1
  111. package/src/modes/controllers/extension-ui-controller.ts +143 -127
  112. package/src/modes/controllers/input-controller.ts +36 -10
  113. package/src/modes/controllers/mcp-command-controller.ts +28 -12
  114. package/src/modes/controllers/selector-controller.ts +4 -11
  115. package/src/modes/controllers/ssh-command-controller.ts +2 -2
  116. package/src/modes/image-references.ts +13 -7
  117. package/src/modes/interactive-mode.ts +2 -2
  118. package/src/modes/rpc/rpc-mode.ts +1 -1
  119. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
  120. package/src/modes/theme/theme.ts +95 -1
  121. package/src/modes/types.ts +2 -1
  122. package/src/modes/utils/ui-helpers.ts +14 -5
  123. package/src/prompts/system/tiny-title-system.md +1 -1
  124. package/src/prompts/system/title-system.md +16 -3
  125. package/src/prompts/system/workflow-notice.md +1 -1
  126. package/src/prompts/tools/bash.md +1 -1
  127. package/src/prompts/tools/eval.md +6 -6
  128. package/src/sdk.ts +31 -14
  129. package/src/session/agent-session.ts +213 -155
  130. package/src/session/session-manager.ts +1 -1
  131. package/src/slash-commands/builtin-registry.ts +1 -1
  132. package/src/system-prompt.ts +15 -9
  133. package/src/task/render.ts +20 -8
  134. package/src/tools/ask.ts +14 -5
  135. package/src/tools/bash-interactive.ts +1 -1
  136. package/src/tools/bash.ts +14 -2
  137. package/src/tools/browser/render.ts +5 -2
  138. package/src/tools/browser/tab-worker.ts +211 -91
  139. package/src/tools/debug.ts +5 -2
  140. package/src/tools/eval-render.ts +8 -5
  141. package/src/tools/eval.ts +2 -2
  142. package/src/tools/gh-renderer.ts +29 -15
  143. package/src/tools/index.ts +32 -0
  144. package/src/tools/inspect-image-renderer.ts +12 -5
  145. package/src/tools/job.ts +9 -6
  146. package/src/tools/memory-render.ts +19 -5
  147. package/src/tools/read.ts +165 -18
  148. package/src/tools/render-utils.ts +3 -1
  149. package/src/tools/resolve.ts +1 -1
  150. package/src/tools/review.ts +1 -1
  151. package/src/tools/ssh.ts +4 -1
  152. package/src/tools/todo.ts +8 -1
  153. package/src/tools/tool-timeouts.ts +1 -1
  154. package/src/tools/write.ts +1 -1
  155. package/src/tui/code-cell.ts +1 -1
  156. package/src/utils/block-context.ts +312 -0
  157. package/src/utils/image-loading.ts +31 -1
  158. package/src/utils/title-generator.ts +2 -2
  159. package/src/web/search/providers/codex.ts +1 -1
  160. package/src/web/search/render.ts +14 -6
  161. /package/dist/types/eval/__tests__/{llm-bridge.test.d.ts → completion-bridge.test.d.ts} +0 -0
@@ -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
+ }
@@ -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);
@@ -33,7 +33,7 @@ const setTitleTool: Tool = {
33
33
  title: {
34
34
  type: "string",
35
35
  description:
36
- 'A concise 3-6 word title for the session, or exactly "none" when the message carries no concrete task yet (greeting, small talk, vague).',
36
+ 'A concise, sentence-case 3-7 word title for the session (capitalize only the first word and proper nouns), or exactly "none" when the message carries no concrete task yet (greeting, small talk, vague).',
37
37
  },
38
38
  },
39
39
  required: ["title"],
@@ -224,7 +224,7 @@ export async function generateTitleOnline(
224
224
  // account_uuid rather than the snapshot-at-call-site value.
225
225
  const metadata = metadataResolver?.(model.provider);
226
226
 
227
- // Title generation is a 3-6 word task, but some reasoning backends ignore
227
+ // Title generation is a 3-7 word task, but some reasoning backends ignore
228
228
  // disableReasoning. Keep the normal cheap budget for non-reasoning models
229
229
  // while reserving enough output room for reasoning models to still emit
230
230
  // the forced tool call after any unavoidable thinking tokens.
@@ -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