@oh-my-pi/pi-coding-agent 15.7.5 → 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.
- package/CHANGELOG.md +30 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +0 -1
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +8 -1
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/index.d.ts +4 -11
- package/dist/types/tui/output-block.d.ts +4 -3
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +10 -0
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +90 -0
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +9 -7
- package/src/modes/components/hook-selector.ts +159 -32
- package/src/modes/components/tool-execution.ts +20 -4
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/interactive-mode.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/session/agent-session.ts +65 -11
- package/src/tools/ask.ts +74 -32
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/index.ts +7 -37
- package/src/tools/renderers.ts +1 -1
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
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(
|
|
85
|
-
if (recommendedIndex === undefined || recommendedIndex < 0 || recommendedIndex >=
|
|
86
|
-
return
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
97
|
-
if (
|
|
98
|
-
if (typeof recommended === "number" && recommended >= 0 && recommended <
|
|
99
|
-
return [
|
|
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 [
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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 =
|
|
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:
|
|
248
|
+
const opts: ExtensionUISelectItem[] = [];
|
|
229
249
|
|
|
230
|
-
for (const opt of
|
|
231
|
-
const checkbox = selected.has(opt) ? theme.checkbox.checked : theme.checkbox.unchecked;
|
|
232
|
-
|
|
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.
|
|
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
|
|
301
|
-
const optionsWithNavigation = [...
|
|
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 =
|
|
327
|
+
const selectedIndex = questionOptions.findIndex(option => option.label === previouslySelected);
|
|
307
328
|
if (selectedIndex >= 0) initialIndex = selectedIndex;
|
|
308
329
|
} else if (customInput !== undefined) {
|
|
309
|
-
initialIndex =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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?:
|
|
603
|
+
options?: AskRenderOption[];
|
|
574
604
|
multi?: boolean;
|
|
575
605
|
questions?: Array<{
|
|
576
606
|
id: string;
|
|
577
607
|
question: string;
|
|
578
|
-
options:
|
|
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
|
}
|
|
@@ -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
|
+
}
|