@oh-my-pi/pi-coding-agent 15.13.1 → 15.13.2

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 (71) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli.js +957 -214
  3. package/dist/types/config/model-registry.d.ts +1 -0
  4. package/dist/types/config/models-config-schema.d.ts +3 -0
  5. package/dist/types/config/models-config.d.ts +3 -0
  6. package/dist/types/config/settings-schema.d.ts +66 -0
  7. package/dist/types/edit/hashline/block-resolver.d.ts +1 -1
  8. package/dist/types/edit/index.d.ts +2 -0
  9. package/dist/types/modes/components/welcome.d.ts +1 -0
  10. package/dist/types/modes/controllers/input-controller.d.ts +4 -4
  11. package/dist/types/modes/rpc/rpc-types.d.ts +2 -1
  12. package/dist/types/sdk.d.ts +3 -0
  13. package/dist/types/session/session-dump-format.d.ts +2 -1
  14. package/dist/types/system-prompt.d.ts +11 -0
  15. package/dist/types/tools/ask.d.ts +2 -0
  16. package/dist/types/tools/ast-edit.d.ts +2 -0
  17. package/dist/types/tools/ast-grep.d.ts +2 -0
  18. package/dist/types/tools/browser.d.ts +2 -0
  19. package/dist/types/tools/debug.d.ts +2 -0
  20. package/dist/types/tools/eval.d.ts +2 -0
  21. package/dist/types/tools/find.d.ts +2 -0
  22. package/dist/types/tools/inspect-image.d.ts +2 -1
  23. package/dist/types/tools/irc.d.ts +2 -0
  24. package/dist/types/tools/ssh.d.ts +2 -0
  25. package/dist/types/tools/todo.d.ts +2 -0
  26. package/dist/types/tui/tree-list.d.ts +1 -0
  27. package/package.json +12 -12
  28. package/src/config/model-registry.ts +10 -0
  29. package/src/config/models-config-schema.ts +2 -0
  30. package/src/config/models-config.ts +1 -0
  31. package/src/config/settings-schema.ts +53 -0
  32. package/src/edit/hashline/block-resolver.ts +1 -1
  33. package/src/edit/hashline/execute.ts +1 -6
  34. package/src/edit/index.ts +48 -0
  35. package/src/eval/__tests__/js-context-manager.test.ts +41 -1
  36. package/src/eval/js/context-manager.ts +92 -26
  37. package/src/eval/js/worker-core.ts +1 -1
  38. package/src/internal-urls/docs-index.generated.ts +9 -2
  39. package/src/modes/components/welcome.ts +14 -4
  40. package/src/modes/controllers/input-controller.ts +21 -38
  41. package/src/modes/rpc/rpc-mode.ts +1 -0
  42. package/src/modes/rpc/rpc-types.ts +2 -2
  43. package/src/prompts/system/system-prompt.md +17 -21
  44. package/src/prompts/tools/ask.md +0 -8
  45. package/src/prompts/tools/ast-edit.md +0 -15
  46. package/src/prompts/tools/ast-grep.md +0 -13
  47. package/src/prompts/tools/browser.md +0 -21
  48. package/src/prompts/tools/debug.md +0 -13
  49. package/src/prompts/tools/eval.md +0 -9
  50. package/src/prompts/tools/find.md +0 -13
  51. package/src/prompts/tools/inspect-image.md +0 -9
  52. package/src/prompts/tools/irc.md +0 -15
  53. package/src/prompts/tools/patch.md +0 -13
  54. package/src/prompts/tools/ssh.md +0 -9
  55. package/src/prompts/tools/todo.md +1 -19
  56. package/src/sdk.ts +19 -0
  57. package/src/session/agent-session.ts +125 -19
  58. package/src/session/session-dump-format.ts +10 -31
  59. package/src/system-prompt.ts +31 -0
  60. package/src/tools/ask.ts +41 -0
  61. package/src/tools/ast-edit.ts +46 -0
  62. package/src/tools/ast-grep.ts +24 -0
  63. package/src/tools/browser.ts +52 -0
  64. package/src/tools/debug.ts +17 -0
  65. package/src/tools/eval.ts +20 -1
  66. package/src/tools/find.ts +24 -0
  67. package/src/tools/inspect-image.ts +27 -1
  68. package/src/tools/irc.ts +41 -0
  69. package/src/tools/ssh.ts +16 -0
  70. package/src/tools/todo.ts +82 -3
  71. package/src/tui/tree-list.ts +68 -19
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
3
- import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
3
+ import { type Api, completeSimple, type Model, type ToolExample } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import { z } from "zod/v4";
6
6
  import { extractTextContent } from "../commit/utils";
@@ -43,6 +43,32 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
43
43
  readonly parameters = inspectImageSchema;
44
44
  readonly strict = false;
45
45
 
46
+ readonly examples: readonly ToolExample<z.input<typeof inspectImageSchema>>[] = [
47
+ {
48
+ caption: "OCR with strict formatting",
49
+ call: {
50
+ path: "screenshots/error.png",
51
+ question: "Extract all visible text verbatim. Return as bullet list in reading order.",
52
+ },
53
+ },
54
+ {
55
+ caption: "Screenshot debugging",
56
+ call: {
57
+ path: "screenshots/settings.png",
58
+ question:
59
+ "Identify the likely cause of the disabled Save button. Return: (1) observations, (2) likely cause, (3) confidence.",
60
+ },
61
+ },
62
+ {
63
+ caption: "Scene/object question",
64
+ call: {
65
+ path: "photos/shelf.jpg",
66
+ question:
67
+ "List all clearly visible product labels and their shelf positions (top/middle/bottom). If unreadable, say unreadable.",
68
+ },
69
+ },
70
+ ];
71
+
46
72
  constructor(
47
73
  private readonly session: ToolSession,
48
74
  private readonly completeImageRequest: typeof completeSimple = completeSimple,
package/src/tools/irc.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
13
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
13
14
  import { type Component, Text } from "@oh-my-pi/pi-tui";
14
15
  import { formatAge, formatDuration, prompt } from "@oh-my-pi/pi-utils";
15
16
  import { z } from "zod/v4";
@@ -96,6 +97,46 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
96
97
  readonly description: string;
97
98
  readonly parameters = ircSchema;
98
99
  readonly strict = true;
100
+
101
+ readonly examples: readonly ToolExample<z.input<typeof ircSchema>>[] = [
102
+ {
103
+ caption: "List peers",
104
+ call: { op: "list" },
105
+ },
106
+ {
107
+ caption: "Fire-and-forget DM — same send wakes idle/parked peers",
108
+ call: {
109
+ op: "send",
110
+ to: "AuthLoader",
111
+ message: "Still touching src/server/auth.ts? I need to add a 401 path.",
112
+ },
113
+ },
114
+ {
115
+ caption: "Round-trip when you cannot proceed without the answer",
116
+ call: {
117
+ op: "send",
118
+ to: "Main",
119
+ message: "JWT or session cookies for the auth flow?",
120
+ await: true,
121
+ },
122
+ },
123
+ {
124
+ caption: "Block until a specific peer answers",
125
+ call: { op: "wait", from: "AuthLoader", timeoutMs: 60000 },
126
+ },
127
+ {
128
+ caption: "Drain pending messages",
129
+ call: { op: "inbox" },
130
+ },
131
+ {
132
+ caption: "Broadcast to live peers (no replies expected)",
133
+ call: {
134
+ op: "send",
135
+ to: "all",
136
+ message: "About to refactor src/server/middleware/*. Anyone already in there?",
137
+ },
138
+ },
139
+ ];
99
140
  readonly loadMode = "discoverable";
100
141
  constructor(private readonly session: ToolSession) {
101
142
  this.description = prompt.render(ircDescription);
package/src/tools/ssh.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
2
3
  import type { Component } from "@oh-my-pi/pi-tui";
3
4
  import { prompt } from "@oh-my-pi/pi-utils";
4
5
  import { z } from "zod/v4";
@@ -135,6 +136,21 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
135
136
  readonly concurrency = "exclusive";
136
137
  readonly strict = true;
137
138
 
139
+ readonly examples: readonly ToolExample<z.input<typeof sshSchema>>[] = [
140
+ {
141
+ caption: "List files: Linux (on server1 (10.0.0.1) | linux/bash)",
142
+ call: { host: "server1", command: "ls -la /home/user" },
143
+ },
144
+ {
145
+ caption: "Show running processes: Windows cmd (on winbox (192.168.1.5) | windows/cmd)",
146
+ call: { host: "winbox", command: "tasklist /v" },
147
+ },
148
+ {
149
+ caption: "Get system info: macOS (on macbook (10.0.0.20) | macos/zsh)",
150
+ call: { host: "macbook", command: "uname -a && sw_vers" },
151
+ },
152
+ ];
153
+
138
154
  readonly #allowedHosts: Set<string>;
139
155
 
140
156
  constructor(
package/src/tools/todo.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
2
3
  import type { Component } from "@oh-my-pi/pi-tui";
3
4
  import { Text } from "@oh-my-pi/pi-tui";
4
5
  import { prompt } from "@oh-my-pi/pi-utils";
@@ -270,8 +271,20 @@ function getTaskTargets(phases: TodoPhase[], entry: TodoOpEntryValue, errors: st
270
271
  return phases.flatMap(phase => phase.tasks);
271
272
  }
272
273
 
274
+ /** Phase name for `init` given a flat `items` list with no explicit `phase`. */
275
+ const DEFAULT_INIT_PHASE = "Tasks";
276
+
273
277
  function initPhases(entry: TodoOpEntryValue, errors: string[]): TodoPhase[] {
274
- if (!entry.list) {
278
+ // Models routinely flatten the single-phase init into `{op:"init", items:[...]}`
279
+ // (optionally with a bare `phase`) instead of the canonical
280
+ // `list: [{phase, items}]`. Accept that shape by synthesizing a one-phase list
281
+ // so a common, recoverable mistake isn't a hard error.
282
+ const list =
283
+ entry.list ??
284
+ (entry.items && entry.items.length > 0
285
+ ? [{ phase: entry.phase ?? DEFAULT_INIT_PHASE, items: entry.items }]
286
+ : undefined);
287
+ if (!list) {
275
288
  errors.push("Missing list for init operation");
276
289
  return [];
277
290
  }
@@ -279,7 +292,7 @@ function initPhases(entry: TodoOpEntryValue, errors: string[]): TodoPhase[] {
279
292
  // (every targeting op resolves the first match), so reject them up front.
280
293
  const seenPhases = new Set<string>();
281
294
  const seenTasks = new Set<string>();
282
- for (const listEntry of entry.list) {
295
+ for (const listEntry of list) {
283
296
  if (seenPhases.has(listEntry.phase)) {
284
297
  errors.push(`Duplicate phase "${listEntry.phase}" in init list`);
285
298
  }
@@ -291,7 +304,7 @@ function initPhases(entry: TodoOpEntryValue, errors: string[]): TodoPhase[] {
291
304
  seenTasks.add(content);
292
305
  }
293
306
  }
294
- return entry.list.map(listEntry => ({
307
+ return list.map(listEntry => ({
295
308
  name: listEntry.phase,
296
309
  tasks: listEntry.items.map<TodoItem>(content => ({ content, status: "pending" })),
297
310
  }));
@@ -551,6 +564,71 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
551
564
  readonly parameters = todoSchema;
552
565
  readonly concurrency = "exclusive";
553
566
  readonly strict = true;
567
+
568
+ readonly examples: readonly ToolExample<z.input<typeof todoSchema>>[] = [
569
+ {
570
+ caption: "Initial setup (multi-phase)",
571
+ call: {
572
+ ops: [
573
+ {
574
+ op: "init",
575
+ list: [
576
+ { phase: "Foundation", items: ["Scaffold crate", "Wire workspace"] },
577
+ { phase: "Auth", items: ["Port credential store", "Wire OAuth providers"] },
578
+ { phase: "Verification", items: ["Run cargo test"] },
579
+ ],
580
+ },
581
+ ],
582
+ },
583
+ },
584
+ {
585
+ caption: "View current state (read-only)",
586
+ call: {
587
+ ops: [{ op: "view" }],
588
+ },
589
+ },
590
+ {
591
+ caption: "Initial setup (single phase)",
592
+ call: {
593
+ ops: [
594
+ {
595
+ op: "init",
596
+ list: [{ phase: "Implementation", items: ["Apply fix", "Run tests"] }],
597
+ },
598
+ ],
599
+ },
600
+ },
601
+ {
602
+ caption: "Complete one task",
603
+ call: {
604
+ ops: [{ op: "done", task: "Wire workspace" }],
605
+ },
606
+ },
607
+ {
608
+ caption: "Complete a whole phase",
609
+ call: {
610
+ ops: [{ op: "done", phase: "Auth" }],
611
+ },
612
+ },
613
+ {
614
+ caption: "Remove all tasks",
615
+ call: {
616
+ ops: [{ op: "rm" }],
617
+ },
618
+ },
619
+ {
620
+ caption: "Drop one task",
621
+ call: {
622
+ ops: [{ op: "drop", task: "Run cargo test" }],
623
+ },
624
+ },
625
+ {
626
+ caption: "Append tasks to a phase",
627
+ call: {
628
+ ops: [{ op: "append", phase: "Auth", items: ["Handle retries", "Run tests"] }],
629
+ },
630
+ },
631
+ ];
554
632
  readonly loadMode = "discoverable";
555
633
  constructor(private readonly session: ToolSession) {
556
634
  this.description = prompt.render(todoDescription);
@@ -839,6 +917,7 @@ export const todoToolRenderer = {
839
917
  expanded,
840
918
  maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
841
919
  itemType: "todo",
920
+ truncateFrom: "start",
842
921
  renderItem: todo => formatTodoLine(todo, uiTheme, "", completionKeys, spinnerFrame),
843
922
  },
844
923
  uiTheme,
@@ -17,23 +17,45 @@ export interface TreeListOptions<T> {
17
17
  */
18
18
  maxCollapsedLines?: number;
19
19
  itemType?: string;
20
+ truncateFrom?: "start" | "end";
20
21
  /** Called once per item with `isLast: false` during budget calculation;
21
22
  * line count MUST NOT vary based on `isLast`. */
22
23
  renderItem: (item: T, context: TreeContext) => string | string[];
23
24
  }
24
25
 
25
26
  export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): string[] {
26
- const { items, expanded = false, maxCollapsed = 8, maxCollapsedLines, itemType = "item", renderItem } = options;
27
+ const {
28
+ items,
29
+ expanded = false,
30
+ maxCollapsed = 8,
31
+ maxCollapsedLines,
32
+ itemType = "item",
33
+ truncateFrom = "end",
34
+ renderItem,
35
+ } = options;
27
36
  const maxItems = expanded ? items.length : Math.min(items.length, maxCollapsed);
28
37
  const linesBudget = !expanded && maxCollapsedLines !== undefined ? maxCollapsedLines : Infinity;
29
38
 
39
+ const candidateIndices: number[] = [];
40
+ if (truncateFrom === "start") {
41
+ const startCandidateIdx = Math.max(0, items.length - maxItems);
42
+ for (let i = startCandidateIdx; i < items.length; i++) {
43
+ candidateIndices.push(i);
44
+ }
45
+ } else {
46
+ for (let i = 0; i < maxItems; i++) {
47
+ candidateIndices.push(i);
48
+ }
49
+ }
50
+
30
51
  // Pre-render each candidate item once.
31
52
  // isLast cannot be known at this point (fittingCount is not yet determined);
32
53
  // renderItem implementations MUST NOT vary line count based on isLast.
33
54
  const preRendered: string[][] = [];
34
- for (let i = 0; i < maxItems; i++) {
35
- const rendered = renderItem(items[i], {
36
- index: i,
55
+ for (let i = 0; i < candidateIndices.length; i++) {
56
+ const itemIdx = candidateIndices[i];
57
+ const rendered = renderItem(items[itemIdx], {
58
+ index: itemIdx,
37
59
  isLast: false,
38
60
  depth: 0,
39
61
  theme,
@@ -43,28 +65,55 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
43
65
  preRendered.push(Array.isArray(rendered) ? rendered : rendered ? [rendered] : []);
44
66
  }
45
67
 
46
- // Determine how many items fit within the line budget.
47
- let fittingCount = maxItems;
68
+ let displayedSlice: { start: number; end: number };
69
+ let remaining: number;
48
70
  let fittedLineCount = 0;
49
- if (linesBudget !== Infinity) {
50
- fittingCount = 0;
51
- for (let i = 0; i < maxItems; i++) {
52
- const count = preRendered[i]!.length;
53
- const remainingAfter = items.length - (i + 1);
54
- const reservedSummaryLines = remainingAfter > 0 ? 1 : 0;
55
- if (fittedLineCount + count + reservedSummaryLines > linesBudget) break;
56
- fittedLineCount += count;
57
- fittingCount = i + 1;
71
+
72
+ if (truncateFrom === "start") {
73
+ let fittingCount = candidateIndices.length;
74
+ if (linesBudget !== Infinity) {
75
+ fittingCount = 0;
76
+ for (let i = candidateIndices.length - 1; i >= 0; i--) {
77
+ const count = preRendered[i].length;
78
+ const remainingBefore = candidateIndices[i];
79
+ const reservedSummaryLines = remainingBefore > 0 ? 1 : 0;
80
+ if (fittedLineCount + count + reservedSummaryLines > linesBudget) break;
81
+ fittedLineCount += count;
82
+ fittingCount++;
83
+ }
84
+ }
85
+ const start = candidateIndices.length - fittingCount;
86
+ displayedSlice = { start, end: candidateIndices.length };
87
+ remaining = candidateIndices.length > 0 ? candidateIndices[start] : 0;
88
+ } else {
89
+ let fittingCount = candidateIndices.length;
90
+ if (linesBudget !== Infinity) {
91
+ fittingCount = 0;
92
+ for (let i = 0; i < candidateIndices.length; i++) {
93
+ const count = preRendered[i].length;
94
+ const remainingAfter = items.length - (i + 1);
95
+ const reservedSummaryLines = remainingAfter > 0 ? 1 : 0;
96
+ if (fittedLineCount + count + reservedSummaryLines > linesBudget) break;
97
+ fittedLineCount += count;
98
+ fittingCount = i + 1;
99
+ }
58
100
  }
101
+ displayedSlice = { start: 0, end: fittingCount };
102
+ remaining = items.length - fittingCount;
59
103
  }
60
104
 
61
- const remaining = items.length - fittingCount;
62
105
  const hasSummary = !expanded && remaining > 0 && (linesBudget === Infinity || fittedLineCount < linesBudget);
63
106
 
64
107
  // Emit pre-rendered content with correct isLast-based branch prefixes.
65
108
  const lines: string[] = [];
66
- for (let i = 0; i < fittingCount; i++) {
67
- const isLast = !hasSummary && i === fittingCount - 1;
109
+
110
+ if (truncateFrom === "start" && hasSummary) {
111
+ lines.push(`${theme.fg("dim", theme.tree.branch)} ${theme.fg("muted", formatMoreItems(remaining, itemType))}`);
112
+ }
113
+
114
+ for (let i = displayedSlice.start; i < displayedSlice.end; i++) {
115
+ const isLast =
116
+ truncateFrom === "start" ? i === displayedSlice.end - 1 : !hasSummary && i === displayedSlice.end - 1;
68
117
  const branch = getTreeBranch(isLast, theme);
69
118
  const prefix = `${theme.fg("dim", branch)} `;
70
119
  const continuePrefix = `${theme.fg("dim", getTreeContinuePrefix(isLast, theme))}`;
@@ -76,7 +125,7 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
76
125
  }
77
126
  }
78
127
 
79
- if (hasSummary) {
128
+ if (truncateFrom === "end" && hasSummary) {
80
129
  lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", formatMoreItems(remaining, itemType))}`);
81
130
  }
82
131