@oh-my-pi/pi-coding-agent 13.15.3 → 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 (34) hide show
  1. package/CHANGELOG.md +16 -16
  2. package/package.json +7 -7
  3. package/src/config/model-registry.ts +215 -57
  4. package/src/config/settings-schema.ts +27 -0
  5. package/src/extensibility/extensions/types.ts +6 -1
  6. package/src/extensibility/hooks/types.ts +1 -1
  7. package/src/internal-urls/docs-index.generated.ts +1 -1
  8. package/src/modes/components/hook-editor.ts +57 -8
  9. package/src/modes/components/model-selector.ts +48 -29
  10. package/src/modes/components/settings-defs.ts +10 -1
  11. package/src/modes/components/settings-selector.ts +92 -5
  12. package/src/modes/controllers/extension-ui-controller.ts +32 -4
  13. package/src/modes/controllers/input-controller.ts +3 -3
  14. package/src/modes/controllers/selector-controller.ts +2 -2
  15. package/src/modes/interactive-mode.ts +7 -2
  16. package/src/modes/rpc/rpc-mode.ts +78 -30
  17. package/src/modes/rpc/rpc-types.ts +9 -1
  18. package/src/modes/theme/theme.ts +70 -0
  19. package/src/modes/types.ts +6 -1
  20. package/src/prompts/system/custom-system-prompt.md +5 -0
  21. package/src/prompts/system/system-prompt.md +6 -0
  22. package/src/prompts/tools/ask.md +1 -0
  23. package/src/prompts/tools/hashline.md +20 -5
  24. package/src/sdk.ts +9 -1
  25. package/src/session/agent-session.ts +12 -11
  26. package/src/system-prompt.ts +63 -2
  27. package/src/tools/ask.ts +109 -61
  28. package/src/tools/ast-edit.ts +2 -16
  29. package/src/tools/ast-grep.ts +2 -17
  30. package/src/tools/browser.ts +35 -17
  31. package/src/tools/grep.ts +4 -17
  32. package/src/tools/path-utils.ts +7 -0
  33. package/src/tools/render-utils.ts +27 -0
  34. package/src/tui/tree-list.ts +51 -22
package/src/tools/grep.ts CHANGED
@@ -457,6 +457,7 @@ export const grepToolRenderer = {
457
457
  items: lines,
458
458
  expanded,
459
459
  maxCollapsed: COLLAPSED_TEXT_LIMIT,
460
+ maxCollapsedLines: COLLAPSED_TEXT_LIMIT,
460
461
  itemType: "item",
461
462
  renderItem: line => uiTheme.fg("toolOutput", line),
462
463
  },
@@ -522,19 +523,6 @@ export const grepToolRenderer = {
522
523
  }
523
524
  }
524
525
 
525
- const getCollapsedMatchLimit = (groups: string[][], maxLines: number): number => {
526
- if (groups.length === 0) return 0;
527
- let usedLines = 0;
528
- let count = 0;
529
- for (const group of groups) {
530
- if (count > 0 && usedLines + group.length > maxLines) break;
531
- usedLines += group.length;
532
- count += 1;
533
- if (usedLines >= maxLines) break;
534
- }
535
- return count;
536
- };
537
-
538
526
  const truncationReasons: string[] = [];
539
527
  if (limits?.matchLimit) truncationReasons.push(`limit ${limits.matchLimit.reached} matches`);
540
528
  if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
@@ -551,14 +539,13 @@ export const grepToolRenderer = {
551
539
  const { expanded } = options;
552
540
  const key = new Hasher().bool(expanded).u32(width).digest();
553
541
  if (cached?.key === key) return cached.lines;
554
- const maxCollapsed = expanded
555
- ? matchGroups.length
556
- : getCollapsedMatchLimit(matchGroups, COLLAPSED_TEXT_LIMIT);
542
+ const collapsedMatchLineBudget = Math.max(COLLAPSED_TEXT_LIMIT - extraLines.length, 0);
557
543
  const matchLines = renderTreeList(
558
544
  {
559
545
  items: matchGroups,
560
546
  expanded,
561
- maxCollapsed,
547
+ maxCollapsed: matchGroups.length,
548
+ maxCollapsedLines: collapsedMatchLineBudget,
562
549
  itemType: "match",
563
550
  renderItem: group =>
564
551
  group.map(line => {
@@ -102,9 +102,16 @@ export function expandPath(filePath: string): string {
102
102
  /**
103
103
  * Resolve a path relative to the given cwd.
104
104
  * Handles ~ expansion and absolute paths.
105
+ *
106
+ * A bare root slash is treated as a workspace-root alias for tool inputs. Users
107
+ * often pass `/` to mean “search from here”, and letting tools escape to the
108
+ * filesystem root is almost never what they intended.
105
109
  */
106
110
  export function resolveToCwd(filePath: string, cwd: string): string {
107
111
  const expanded = expandPath(filePath);
112
+ if (/^\/+$/.test(expanded)) {
113
+ return cwd;
114
+ }
108
115
  if (path.isAbsolute(expanded)) {
109
116
  return expanded;
110
117
  }
@@ -8,6 +8,7 @@ import * as os from "node:os";
8
8
  import { type Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
9
9
  import { getIndentation, pluralize } from "@oh-my-pi/pi-utils";
10
10
  import type { Theme } from "../modes/theme/theme";
11
+ import { formatDimensionNote, type ResizedImage } from "../utils/image-resize";
11
12
 
12
13
  export { Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
13
14
 
@@ -527,6 +528,32 @@ export function shortenPath(filePath: string, homeDir?: string): string {
527
528
  return filePath;
528
529
  }
529
530
 
531
+ export function formatScreenshot(opts: {
532
+ saveFullRes: boolean;
533
+ savedMimeType: string;
534
+ savedByteLength: number;
535
+ dest: string;
536
+ resized: ResizedImage;
537
+ }): string[] {
538
+ const lines = ["Screenshot captured"];
539
+ if (opts.saveFullRes) {
540
+ lines.push(
541
+ `Saved: ${opts.savedMimeType} (${(opts.savedByteLength / 1024).toFixed(2)} KB) to ${shortenPath(opts.dest)}`,
542
+ );
543
+ lines.push(
544
+ `Model: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB, ${opts.resized.width}x${opts.resized.height})`,
545
+ );
546
+ } else {
547
+ lines.push(`Format: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB)`);
548
+ lines.push(`Dimensions: ${opts.resized.width}x${opts.resized.height}`);
549
+ }
550
+ const dimensionNote = formatDimensionNote(opts.resized);
551
+ if (dimensionNote) {
552
+ lines.push(dimensionNote);
553
+ }
554
+ return lines;
555
+ }
556
+
530
557
  export function wrapBrackets(text: string, theme: Theme): string {
531
558
  return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
532
559
  }
@@ -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