@oh-my-pi/pi-coding-agent 13.15.2 → 13.16.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 (41) hide show
  1. package/CHANGELOG.md +26 -16
  2. package/package.json +7 -7
  3. package/src/config/keybindings.ts +6 -0
  4. package/src/config/model-registry.ts +215 -57
  5. package/src/config/settings-schema.ts +27 -0
  6. package/src/extensibility/extensions/types.ts +6 -1
  7. package/src/extensibility/hooks/types.ts +1 -1
  8. package/src/internal-urls/docs-index.generated.ts +1 -1
  9. package/src/modes/components/custom-editor.ts +6 -4
  10. package/src/modes/components/hook-editor.ts +57 -8
  11. package/src/modes/components/model-selector.ts +48 -29
  12. package/src/modes/components/settings-defs.ts +10 -1
  13. package/src/modes/components/settings-selector.ts +92 -5
  14. package/src/modes/controllers/extension-ui-controller.ts +32 -4
  15. package/src/modes/controllers/input-controller.ts +22 -9
  16. package/src/modes/controllers/selector-controller.ts +2 -2
  17. package/src/modes/interactive-mode.ts +7 -2
  18. package/src/modes/rpc/rpc-mode.ts +78 -30
  19. package/src/modes/rpc/rpc-types.ts +9 -1
  20. package/src/modes/theme/theme.ts +70 -0
  21. package/src/modes/types.ts +6 -1
  22. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  23. package/src/prompts/system/custom-system-prompt.md +5 -0
  24. package/src/prompts/system/system-prompt.md +6 -0
  25. package/src/prompts/tools/ask.md +1 -0
  26. package/src/prompts/tools/hashline.md +20 -5
  27. package/src/sdk.ts +9 -1
  28. package/src/session/agent-session.ts +338 -80
  29. package/src/session/messages.ts +23 -0
  30. package/src/session/session-manager.ts +65 -0
  31. package/src/system-prompt.ts +63 -2
  32. package/src/tools/ask.ts +109 -61
  33. package/src/tools/ast-edit.ts +2 -16
  34. package/src/tools/ast-grep.ts +2 -17
  35. package/src/tools/browser.ts +35 -17
  36. package/src/tools/grep.ts +4 -17
  37. package/src/tools/path-utils.ts +7 -0
  38. package/src/tools/render-utils.ts +27 -0
  39. package/src/tui/tree-list.ts +51 -22
  40. package/src/utils/image-input.ts +11 -1
  41. package/src/web/search/providers/codex.ts +10 -3
@@ -10,42 +10,71 @@ export interface TreeListOptions<T> {
10
10
  items: T[];
11
11
  expanded?: boolean;
12
12
  maxCollapsed?: number;
13
+ /** Strict total-line budget for collapsed mode. When set (and not expanded),
14
+ * rendered item lines plus the trailing summary line must fit within this budget.
15
+ */
16
+ maxCollapsedLines?: number;
13
17
  itemType?: string;
18
+ /** Called once per item with `isLast: false` during budget calculation;
19
+ * line count MUST NOT vary based on `isLast`. */
14
20
  renderItem: (item: T, context: TreeContext) => string | string[];
15
21
  }
16
22
 
17
23
  export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): string[] {
18
- const { items, expanded = false, maxCollapsed = 8, itemType = "item", renderItem } = options;
19
- const lines: string[] = [];
24
+ const { items, expanded = false, maxCollapsed = 8, maxCollapsedLines, itemType = "item", renderItem } = options;
20
25
  const maxItems = expanded ? items.length : Math.min(items.length, maxCollapsed);
26
+ const linesBudget = !expanded && maxCollapsedLines !== undefined ? maxCollapsedLines : Infinity;
21
27
 
28
+ // Pre-render each candidate item once.
29
+ // isLast cannot be known at this point (fittingCount is not yet determined);
30
+ // renderItem implementations MUST NOT vary line count based on isLast.
31
+ const preRendered: string[][] = [];
22
32
  for (let i = 0; i < maxItems; i++) {
23
- const isLast = i === maxItems - 1 && (expanded || items.length <= maxCollapsed);
24
- const branch = getTreeBranch(isLast, theme);
25
- const prefix = `${theme.fg("dim", branch)} `;
26
- const continuePrefix = `${theme.fg("dim", getTreeContinuePrefix(isLast, theme))}`;
27
- const context: TreeContext = {
33
+ const rendered = renderItem(items[i], {
28
34
  index: i,
29
- isLast,
35
+ isLast: false,
30
36
  depth: 0,
31
37
  theme,
32
- prefix,
33
- continuePrefix,
34
- };
35
- const rendered = renderItem(items[i], context);
36
- if (Array.isArray(rendered)) {
37
- if (rendered.length === 0) continue;
38
- lines.push(`${prefix}${replaceTabs(rendered[0])}`);
39
- for (let j = 1; j < rendered.length; j++) {
40
- lines.push(`${continuePrefix}${replaceTabs(rendered[j])}`);
41
- }
42
- } else {
43
- lines.push(`${prefix}${replaceTabs(rendered)}`);
38
+ prefix: "",
39
+ continuePrefix: "",
40
+ });
41
+ preRendered.push(Array.isArray(rendered) ? rendered : rendered ? [rendered] : []);
42
+ }
43
+
44
+ // Determine how many items fit within the line budget.
45
+ let fittingCount = maxItems;
46
+ let fittedLineCount = 0;
47
+ if (linesBudget !== Infinity) {
48
+ fittingCount = 0;
49
+ for (let i = 0; i < maxItems; i++) {
50
+ const count = preRendered[i]!.length;
51
+ const remainingAfter = items.length - (i + 1);
52
+ const reservedSummaryLines = remainingAfter > 0 ? 1 : 0;
53
+ if (fittedLineCount + count + reservedSummaryLines > linesBudget) break;
54
+ fittedLineCount += count;
55
+ fittingCount = i + 1;
56
+ }
57
+ }
58
+
59
+ const remaining = items.length - fittingCount;
60
+ const hasSummary = !expanded && remaining > 0 && (linesBudget === Infinity || fittedLineCount < linesBudget);
61
+
62
+ // Emit pre-rendered content with correct isLast-based branch prefixes.
63
+ const lines: string[] = [];
64
+ for (let i = 0; i < fittingCount; i++) {
65
+ const isLast = !hasSummary && i === fittingCount - 1;
66
+ const branch = getTreeBranch(isLast, theme);
67
+ const prefix = `${theme.fg("dim", branch)} `;
68
+ const continuePrefix = `${theme.fg("dim", getTreeContinuePrefix(isLast, theme))}`;
69
+ const itemLines = preRendered[i]!;
70
+ if (itemLines.length === 0) continue;
71
+ lines.push(`${prefix}${replaceTabs(itemLines[0]!)}`);
72
+ for (let j = 1; j < itemLines.length; j++) {
73
+ lines.push(`${continuePrefix}${replaceTabs(itemLines[j]!)}`);
44
74
  }
45
75
  }
46
76
 
47
- if (!expanded && items.length > maxItems) {
48
- const remaining = items.length - maxItems;
77
+ if (hasSummary) {
49
78
  lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", formatMoreItems(remaining, itemType))}`);
50
79
  }
51
80
 
@@ -1,12 +1,14 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
2
3
  import { formatBytes } from "@oh-my-pi/pi-utils";
3
4
  import { resolveReadPath } from "../tools/path-utils";
5
+ import { convertToPng } from "./image-convert";
4
6
  import { formatDimensionNote, resizeImage } from "./image-resize";
5
7
  import { detectSupportedImageMimeTypeFromFile } from "./mime";
6
8
 
7
9
  export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
8
10
  const MAX_IMAGE_METADATA_HEADER_BYTES = 256 * 1024;
9
-
11
+ export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
10
12
  export interface ImageMetadata {
11
13
  mimeType: string;
12
14
  bytes: number;
@@ -25,6 +27,14 @@ export interface LoadedImageInput {
25
27
  bytes: number;
26
28
  }
27
29
 
30
+ export async function ensureSupportedImageInput(image: ImageContent): Promise<ImageContent | null> {
31
+ if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
32
+ return image;
33
+ }
34
+ const converted = await convertToPng(image.data, image.mimeType);
35
+ return converted ? { type: "image", data: converted.data, mimeType: converted.mimeType } : null;
36
+ }
37
+
28
38
  export interface ReadImageMetadataOptions {
29
39
  path: string;
30
40
  cwd: string;
@@ -6,7 +6,7 @@
6
6
  * Returns synthesized answers with web search sources.
7
7
  */
8
8
  import * as os from "node:os";
9
- import { getAgentDbPath, readSseJson } from "@oh-my-pi/pi-utils";
9
+ import { $env, getAgentDbPath, readSseJson } from "@oh-my-pi/pi-utils";
10
10
  import packageJson from "../../../../package.json" with { type: "json" };
11
11
  import { AgentStorage } from "../../../session/agent-storage";
12
12
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
@@ -21,6 +21,11 @@ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
21
21
  const DEFAULT_INSTRUCTIONS =
22
22
  "You are a helpful assistant with web search capabilities. Search the web to answer the user's question accurately and cite your sources.";
23
23
 
24
+ function getModel(): string {
25
+ const configuredModel = $env.PI_CODEX_WEB_SEARCH_MODEL?.trim();
26
+ return configuredModel ? configuredModel : DEFAULT_MODEL;
27
+ }
28
+
24
29
  export interface CodexSearchParams {
25
30
  signal?: AbortSignal;
26
31
  query: string;
@@ -188,8 +193,10 @@ async function callCodexSearch(
188
193
  const url = `${CODEX_BASE_URL}${CODEX_RESPONSES_PATH}`;
189
194
  const headers = buildCodexHeaders(auth.accessToken, auth.accountId);
190
195
 
196
+ const requestedModel = getModel();
197
+
191
198
  const body: Record<string, unknown> = {
192
- model: DEFAULT_MODEL,
199
+ model: requestedModel,
193
200
  stream: true,
194
201
  store: false,
195
202
  input: [
@@ -226,7 +233,7 @@ async function callCodexSearch(
226
233
  // Parse SSE stream
227
234
  const answerParts: string[] = [];
228
235
  const sources: SearchSource[] = [];
229
- let model = DEFAULT_MODEL;
236
+ let model = requestedModel;
230
237
  let requestId = "";
231
238
  let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
232
239