@oh-my-pi/pi-coding-agent 15.7.3 → 15.7.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 (119) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/dist/types/config/settings-schema.d.ts +12 -22
  3. package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
  4. package/dist/types/eval/heartbeat.d.ts +45 -0
  5. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  6. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  7. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/types.d.ts +24 -5
  9. package/dist/types/extensibility/shared-events.d.ts +2 -2
  10. package/dist/types/internal-urls/local-protocol.d.ts +19 -9
  11. package/dist/types/internal-urls/types.d.ts +14 -0
  12. package/dist/types/lsp/client.d.ts +3 -0
  13. package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
  14. package/dist/types/lsp/index.d.ts +2 -0
  15. package/dist/types/lsp/utils.d.ts +4 -0
  16. package/dist/types/mcp/manager.d.ts +14 -5
  17. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  18. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  19. package/dist/types/modes/components/custom-editor.d.ts +0 -1
  20. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  21. package/dist/types/modes/controllers/command-controller.d.ts +2 -3
  22. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
  23. package/dist/types/modes/interactive-mode.d.ts +2 -2
  24. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  25. package/dist/types/modes/theme/theme.d.ts +1 -1
  26. package/dist/types/modes/types.d.ts +2 -2
  27. package/dist/types/session/agent-session.d.ts +10 -7
  28. package/dist/types/session/shake-types.d.ts +3 -3
  29. package/dist/types/task/repair-args.d.ts +52 -0
  30. package/dist/types/tiny/models.d.ts +0 -14
  31. package/dist/types/tiny/title-client.d.ts +28 -2
  32. package/dist/types/tiny/title-protocol.d.ts +8 -9
  33. package/dist/types/tools/ask.d.ts +8 -6
  34. package/dist/types/tools/eval-backends.d.ts +12 -0
  35. package/dist/types/tools/eval-render.d.ts +52 -0
  36. package/dist/types/tools/eval.d.ts +2 -35
  37. package/dist/types/tools/find.d.ts +1 -1
  38. package/dist/types/tools/index.d.ts +4 -11
  39. package/dist/types/tools/path-utils.d.ts +7 -0
  40. package/dist/types/tui/output-block.d.ts +11 -10
  41. package/examples/extensions/README.md +1 -0
  42. package/examples/extensions/thinking-note.ts +13 -0
  43. package/package.json +9 -9
  44. package/scripts/build-binary.ts +0 -1
  45. package/src/cli.ts +59 -0
  46. package/src/config/model-registry.ts +33 -4
  47. package/src/config/settings-schema.ts +13 -24
  48. package/src/config/settings.ts +10 -0
  49. package/src/discovery/claude.ts +41 -22
  50. package/src/edit/index.ts +23 -3
  51. package/src/eval/__tests__/agent-bridge.test.ts +90 -0
  52. package/src/eval/__tests__/heartbeat.test.ts +84 -0
  53. package/src/eval/__tests__/llm-bridge.test.ts +30 -0
  54. package/src/eval/agent-bridge.ts +44 -38
  55. package/src/eval/heartbeat.ts +74 -0
  56. package/src/eval/js/executor.ts +13 -9
  57. package/src/eval/llm-bridge.ts +20 -14
  58. package/src/eval/py/executor.ts +14 -18
  59. package/src/exec/bash-executor.ts +31 -5
  60. package/src/extensibility/custom-tools/types.ts +2 -2
  61. package/src/extensibility/extensions/loader.ts +16 -18
  62. package/src/extensibility/extensions/runner.ts +22 -17
  63. package/src/extensibility/extensions/types.ts +39 -5
  64. package/src/extensibility/shared-events.ts +2 -2
  65. package/src/internal-urls/docs-index.generated.ts +5 -5
  66. package/src/internal-urls/local-protocol.ts +23 -11
  67. package/src/internal-urls/types.ts +15 -0
  68. package/src/lsp/client.ts +28 -5
  69. package/src/lsp/diagnostics-ledger.ts +51 -0
  70. package/src/lsp/index.ts +9 -22
  71. package/src/lsp/utils.ts +21 -0
  72. package/src/mcp/manager.ts +87 -4
  73. package/src/modes/acp/acp-agent.ts +8 -4
  74. package/src/modes/components/assistant-message.ts +28 -1
  75. package/src/modes/components/custom-editor.ts +9 -7
  76. package/src/modes/components/hook-selector.ts +159 -32
  77. package/src/modes/components/tool-execution.ts +20 -4
  78. package/src/modes/controllers/command-controller.ts +7 -39
  79. package/src/modes/controllers/event-controller.ts +38 -28
  80. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  81. package/src/modes/controllers/input-controller.ts +0 -15
  82. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  83. package/src/modes/interactive-mode.ts +2 -1
  84. package/src/modes/rpc/rpc-mode.ts +17 -6
  85. package/src/modes/theme/theme-schema.json +30 -0
  86. package/src/modes/theme/theme.ts +39 -2
  87. package/src/modes/types.ts +2 -1
  88. package/src/modes/utils/ui-helpers.ts +5 -2
  89. package/src/prompts/system/project-prompt.md +3 -2
  90. package/src/prompts/system/subagent-system-prompt.md +12 -8
  91. package/src/prompts/system/system-prompt.md +8 -6
  92. package/src/prompts/tools/ask.md +2 -1
  93. package/src/prompts/tools/eval.md +1 -1
  94. package/src/session/agent-session.ts +75 -103
  95. package/src/session/shake-types.ts +4 -5
  96. package/src/slash-commands/builtin-registry.ts +2 -4
  97. package/src/task/executor.ts +14 -4
  98. package/src/task/index.ts +3 -2
  99. package/src/task/repair-args.ts +117 -0
  100. package/src/tiny/models.ts +0 -28
  101. package/src/tiny/title-client.ts +133 -43
  102. package/src/tiny/title-protocol.ts +11 -16
  103. package/src/tiny/worker.ts +6 -61
  104. package/src/tools/ask.ts +74 -32
  105. package/src/tools/ast-edit.ts +3 -0
  106. package/src/tools/ast-grep.ts +3 -0
  107. package/src/tools/eval-backends.ts +38 -0
  108. package/src/tools/eval-render.ts +750 -0
  109. package/src/tools/eval.ts +27 -754
  110. package/src/tools/find.ts +20 -6
  111. package/src/tools/gh.ts +1 -0
  112. package/src/tools/index.ts +7 -37
  113. package/src/tools/path-utils.ts +13 -2
  114. package/src/tools/read.ts +1 -0
  115. package/src/tools/renderers.ts +1 -1
  116. package/src/tools/search.ts +12 -1
  117. package/src/tools/write.ts +9 -1
  118. package/src/tui/output-block.ts +42 -79
  119. package/src/utils/git.ts +9 -3
package/src/tools/ask.ts CHANGED
@@ -20,6 +20,7 @@ import { type Component, Container, Markdown, renderInlineMarkdown, TERMINAL, Te
20
20
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
21
21
  import * as z from "zod/v4";
22
22
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
23
+ import type { ExtensionUISelectItem } from "../extensibility/extensions";
23
24
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
24
25
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
25
26
  import { renderStatusLine } from "../tui";
@@ -33,6 +34,7 @@ import { ToolAbortError } from "./tool-errors";
33
34
 
34
35
  const OptionItem = z.object({
35
36
  label: z.string().describe("display label"),
37
+ description: z.string().describe("optional explanatory text displayed below the label").optional(),
36
38
  });
37
39
 
38
40
  const QuestionItem = z.object({
@@ -69,6 +71,23 @@ export interface AskToolDetails {
69
71
  results?: QuestionResult[];
70
72
  }
71
73
 
74
+ interface AskOption {
75
+ label: string;
76
+ description?: string;
77
+ }
78
+
79
+ function getAskOptionLabel(option: AskOption): string {
80
+ return option.label;
81
+ }
82
+
83
+ function getSelectOptionLabel(option: ExtensionUISelectItem): string {
84
+ return typeof option === "string" ? option : option.label;
85
+ }
86
+
87
+ function toSelectOption(option: AskOption, label = option.label): ExtensionUISelectItem {
88
+ return option.description ? { label, description: option.description } : label;
89
+ }
90
+
72
91
  // =============================================================================
73
92
  // Constants
74
93
  // =============================================================================
@@ -81,24 +100,25 @@ function getDoneOptionLabel(): string {
81
100
  }
82
101
 
83
102
  /** Add "(Recommended)" suffix to the option at the given index if not already present */
84
- function addRecommendedSuffix(labels: string[], recommendedIndex?: number): string[] {
85
- if (recommendedIndex === undefined || recommendedIndex < 0 || recommendedIndex >= labels.length) {
86
- return labels;
103
+ function addRecommendedSuffix(options: AskOption[], recommendedIndex?: number): ExtensionUISelectItem[] {
104
+ if (recommendedIndex === undefined || recommendedIndex < 0 || recommendedIndex >= options.length) {
105
+ return options.map(option => toSelectOption(option));
87
106
  }
88
- return labels.map((label, i) => {
89
- if (i === recommendedIndex && !label.endsWith(RECOMMENDED_SUFFIX)) {
90
- return label + RECOMMENDED_SUFFIX;
91
- }
92
- return label;
107
+ return options.map((option, i) => {
108
+ const label =
109
+ i === recommendedIndex && !option.label.endsWith(RECOMMENDED_SUFFIX)
110
+ ? option.label + RECOMMENDED_SUFFIX
111
+ : option.label;
112
+ return toSelectOption(option, label);
93
113
  });
94
114
  }
95
115
 
96
- function getAutoSelectionOnTimeout(optionLabels: string[], recommended?: number): string[] {
97
- if (optionLabels.length === 0) return [];
98
- if (typeof recommended === "number" && recommended >= 0 && recommended < optionLabels.length) {
99
- return [optionLabels[recommended]];
116
+ function getAutoSelectionOnTimeout(options: AskOption[], recommended?: number): string[] {
117
+ if (options.length === 0) return [];
118
+ if (typeof recommended === "number" && recommended >= 0 && recommended < options.length) {
119
+ return [options[recommended]!.label];
100
120
  }
101
- return [optionLabels[0]];
121
+ return [options[0]!.label];
102
122
  }
103
123
 
104
124
  /** Strip "(Recommended)" suffix from a label */
@@ -134,7 +154,7 @@ interface AskSingleQuestionOptions {
134
154
  interface UIContext {
135
155
  select(
136
156
  prompt: string,
137
- options: string[],
157
+ options: ExtensionUISelectItem[],
138
158
  options_?: {
139
159
  initialIndex?: number;
140
160
  timeout?: number;
@@ -157,7 +177,7 @@ interface UIContext {
157
177
  async function askSingleQuestion(
158
178
  ui: UIContext,
159
179
  question: string,
160
- optionLabels: string[],
180
+ questionOptions: AskOption[],
161
181
  multi: boolean,
162
182
  options: AskSingleQuestionOptions = {},
163
183
  ): Promise<SelectionResult> {
@@ -169,7 +189,7 @@ async function askSingleQuestion(
169
189
 
170
190
  const selectOption = async (
171
191
  prompt: string,
172
- optionsToShow: string[],
192
+ optionsToShow: ExtensionUISelectItem[],
173
193
  initialIndex?: number,
174
194
  ): Promise<{ choice: string | undefined; timedOut: boolean; navigation?: "back" | "forward" }> => {
175
195
  let timeoutTriggered = false;
@@ -218,18 +238,19 @@ async function askSingleQuestion(
218
238
  const promptWithProgress = navigation?.progressText ? `${question} (${navigation.progressText})` : question;
219
239
  if (multi) {
220
240
  const selected = new Set<string>(selectedOptions);
221
- let cursorIndex = Math.min(Math.max(recommended ?? 0, 0), Math.max(optionLabels.length - 1, 0));
241
+ let cursorIndex = Math.min(Math.max(recommended ?? 0, 0), Math.max(questionOptions.length - 1, 0));
222
242
  const firstSelected = selectedOptions[0];
223
243
  if (firstSelected) {
224
- const selectedIndex = optionLabels.indexOf(firstSelected);
244
+ const selectedIndex = questionOptions.findIndex(option => option.label === firstSelected);
225
245
  if (selectedIndex >= 0) cursorIndex = selectedIndex;
226
246
  }
227
247
  while (true) {
228
- const opts: string[] = [];
248
+ const opts: ExtensionUISelectItem[] = [];
229
249
 
230
- for (const opt of optionLabels) {
231
- const checkbox = selected.has(opt) ? theme.checkbox.checked : theme.checkbox.unchecked;
232
- opts.push(`${checkbox} ${opt}`);
250
+ for (const opt of questionOptions) {
251
+ const checkbox = selected.has(opt.label) ? theme.checkbox.checked : theme.checkbox.unchecked;
252
+ const displayLabel = `${checkbox} ${opt.label}`;
253
+ opts.push(toSelectOption(opt, displayLabel));
233
254
  }
234
255
 
235
256
  if (!navigation?.allowForward && selected.size > 0) {
@@ -269,7 +290,7 @@ async function askSingleQuestion(
269
290
  break;
270
291
  }
271
292
 
272
- const selectedIdx = opts.indexOf(choice);
293
+ const selectedIdx = opts.findIndex(opt => getSelectOptionLabel(opt) === choice);
273
294
  if (selectedIdx >= 0) {
274
295
  cursorIndex = selectedIdx;
275
296
  }
@@ -297,16 +318,16 @@ async function askSingleQuestion(
297
318
  }
298
319
  selectedOptions = Array.from(selected);
299
320
  } else {
300
- const displayLabels = addRecommendedSuffix(optionLabels, recommended);
301
- const optionsWithNavigation = [...displayLabels, OTHER_OPTION];
321
+ const displayOptions = addRecommendedSuffix(questionOptions, recommended);
322
+ const optionsWithNavigation: ExtensionUISelectItem[] = [...displayOptions, OTHER_OPTION];
302
323
 
303
324
  let initialIndex = recommended;
304
325
  const previouslySelected = selectedOptions[0];
305
326
  if (previouslySelected) {
306
- const selectedIndex = optionLabels.indexOf(previouslySelected);
327
+ const selectedIndex = questionOptions.findIndex(option => option.label === previouslySelected);
307
328
  if (selectedIndex >= 0) initialIndex = selectedIndex;
308
329
  } else if (customInput !== undefined) {
309
- initialIndex = displayLabels.length;
330
+ initialIndex = displayOptions.length;
310
331
  }
311
332
  if (initialIndex !== undefined) {
312
333
  const maxIndex = Math.max(optionsWithNavigation.length - 1, 0);
@@ -346,7 +367,7 @@ async function askSingleQuestion(
346
367
  }
347
368
 
348
369
  if (timedOut && selectedOptions.length === 0 && customInput === undefined) {
349
- selectedOptions = getAutoSelectionOnTimeout(optionLabels, recommended);
370
+ selectedOptions = getAutoSelectionOnTimeout(questionOptions, recommended);
350
371
  }
351
372
 
352
373
  return { selectedOptions, customInput, timedOut };
@@ -442,12 +463,16 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
442
463
  q: AskParams["questions"][number],
443
464
  options?: { previous?: QuestionResult; navigation?: NavigationControls },
444
465
  ) => {
445
- const optionLabels = q.options.map(o => o.label);
466
+ const questionOptions = q.options.map(option => ({
467
+ label: option.label,
468
+ ...(option.description?.trim() ? { description: option.description.trim() } : {}),
469
+ }));
470
+ const optionLabels = questionOptions.map(getAskOptionLabel);
446
471
  try {
447
472
  const { selectedOptions, customInput, navigation, cancelled, timedOut } = await askSingleQuestion(
448
473
  ui,
449
474
  q.question,
450
- optionLabels,
475
+ questionOptions,
451
476
  q.multi ?? false,
452
477
  {
453
478
  recommended: q.recommended,
@@ -568,14 +593,19 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
568
593
  // TUI Renderer
569
594
  // =============================================================================
570
595
 
596
+ interface AskRenderOption {
597
+ label: string;
598
+ description?: string;
599
+ }
600
+
571
601
  interface AskRenderArgs {
572
602
  question?: string;
573
- options?: Array<{ label: string }>;
603
+ options?: AskRenderOption[];
574
604
  multi?: boolean;
575
605
  questions?: Array<{
576
606
  id: string;
577
607
  question: string;
578
- options: Array<{ label: string }>;
608
+ options: AskRenderOption[];
579
609
  multi?: boolean;
580
610
  }>;
581
611
  }
@@ -634,6 +664,13 @@ export const askToolRenderer = {
634
664
  const optBranch = isLastOpt ? uiTheme.tree.last : uiTheme.tree.branch;
635
665
  const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
636
666
  optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${optLabel}`;
667
+ if (opt.description?.trim()) {
668
+ const optContinuation = isLastOpt ? " " : uiTheme.tree.vertical;
669
+ const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t =>
670
+ uiTheme.fg("dim", t),
671
+ );
672
+ optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optContinuation)} ${uiTheme.fg("dim", "↳")} ${description}`;
673
+ }
637
674
  }
638
675
  container.addChild(new Text(optText, 0, 0));
639
676
  }
@@ -661,6 +698,11 @@ export const askToolRenderer = {
661
698
  const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
662
699
  const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
663
700
  optText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${optLabel}`;
701
+ if (opt.description?.trim()) {
702
+ const continuation = isLast ? " " : uiTheme.tree.vertical;
703
+ const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t => uiTheme.fg("dim", t));
704
+ optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", "↳")} ${description}`;
705
+ }
664
706
  }
665
707
  container.addChild(new Text(optText, 0, 0));
666
708
  }
@@ -230,6 +230,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
230
230
  rawPaths: params.paths,
231
231
  cwd: this.session.cwd,
232
232
  internalUrlAction: "rewrite",
233
+ settings: this.session.settings,
234
+ signal,
235
+ localProtocolOptions: this.session.localProtocolOptions,
233
236
  });
234
237
  const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
235
238
 
@@ -155,6 +155,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
155
155
  rawPaths: params.paths,
156
156
  cwd: this.session.cwd,
157
157
  internalUrlAction: "search",
158
+ settings: this.session.settings,
159
+ signal,
160
+ localProtocolOptions: this.session.localProtocolOptions,
158
161
  });
159
162
  const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
160
163
 
@@ -0,0 +1,38 @@
1
+ import { $env, $flag } from "@oh-my-pi/pi-utils";
2
+ import type { ToolSession } from ".";
3
+
4
+ export interface EvalBackendsAllowance {
5
+ python: boolean;
6
+ js: boolean;
7
+ }
8
+
9
+ /**
10
+ * Parse PI_PY / PI_JS environment variables. Each is a boolean flag; unset
11
+ * means "not specified, defer to settings". Returns null when neither is set
12
+ * so the caller can fall through to `readEvalBackendsAllowance` per key.
13
+ */
14
+ function getEvalBackendsFromEnv(): EvalBackendsAllowance | null {
15
+ const pyEnv = $env.PI_PY;
16
+ const jsEnv = $env.PI_JS;
17
+ if (pyEnv === undefined && jsEnv === undefined) return null;
18
+ return {
19
+ python: pyEnv === undefined ? true : $flag("PI_PY"),
20
+ js: jsEnv === undefined ? true : $flag("PI_JS"),
21
+ };
22
+ }
23
+
24
+ /** Read per-backend allowance from settings (defaults true). */
25
+ export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance {
26
+ return {
27
+ python: session.settings.get("eval.py") ?? true,
28
+ js: session.settings.get("eval.js") ?? true,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Materialize the active eval backend allowance: PI_PY / PI_JS env flags
34
+ * override the per-key settings; otherwise settings (defaults true) win.
35
+ */
36
+ export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance {
37
+ return getEvalBackendsFromEnv() ?? readEvalBackendsAllowance(session);
38
+ }