@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.
- package/CHANGELOG.md +26 -16
- package/package.json +7 -7
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +215 -57
- package/src/config/settings-schema.ts +27 -0
- package/src/extensibility/extensions/types.ts +6 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/custom-editor.ts +6 -4
- package/src/modes/components/hook-editor.ts +57 -8
- package/src/modes/components/model-selector.ts +48 -29
- package/src/modes/components/settings-defs.ts +10 -1
- package/src/modes/components/settings-selector.ts +92 -5
- package/src/modes/controllers/extension-ui-controller.ts +32 -4
- package/src/modes/controllers/input-controller.ts +22 -9
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +78 -30
- package/src/modes/rpc/rpc-types.ts +9 -1
- package/src/modes/theme/theme.ts +70 -0
- package/src/modes/types.ts +6 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/tools/ask.md +1 -0
- package/src/prompts/tools/hashline.md +20 -5
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +338 -80
- package/src/session/messages.ts +23 -0
- package/src/session/session-manager.ts +65 -0
- package/src/system-prompt.ts +63 -2
- package/src/tools/ask.ts +109 -61
- package/src/tools/ast-edit.ts +2 -16
- package/src/tools/ast-grep.ts +2 -17
- package/src/tools/browser.ts +35 -17
- package/src/tools/grep.ts +4 -17
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/render-utils.ts +27 -0
- package/src/tui/tree-list.ts +51 -22
- package/src/utils/image-input.ts +11 -1
- package/src/web/search/providers/codex.ts +10 -3
package/src/system-prompt.ts
CHANGED
|
@@ -16,6 +16,57 @@ import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile
|
|
|
16
16
|
import { loadSkills, type Skill } from "./extensibility/skills";
|
|
17
17
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
18
18
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
19
|
+
import { formatPromptContent } from "./utils/prompt-format";
|
|
20
|
+
|
|
21
|
+
interface AlwaysApplyRule {
|
|
22
|
+
name: string;
|
|
23
|
+
content: string;
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePromptBlock(content: string): string {
|
|
28
|
+
return formatPromptContent(content, { renderPhase: "post-render" }).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function splitComparablePromptBlocks(content: string | null | undefined): string[] {
|
|
32
|
+
const normalized = firstNonEmpty(content);
|
|
33
|
+
if (!normalized) return [];
|
|
34
|
+
|
|
35
|
+
return normalizePromptBlock(normalized)
|
|
36
|
+
.split(/\n{2,}/)
|
|
37
|
+
.map(block => block.trim())
|
|
38
|
+
.filter(block => block.length > 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function promptSourceContainsRule(source: string | null | undefined, ruleContent: string): boolean {
|
|
42
|
+
const sourceBlocks = splitComparablePromptBlocks(source);
|
|
43
|
+
const ruleBlocks = splitComparablePromptBlocks(ruleContent);
|
|
44
|
+
if (sourceBlocks.length === 0 || ruleBlocks.length === 0 || ruleBlocks.length > sourceBlocks.length) return false;
|
|
45
|
+
|
|
46
|
+
for (let start = 0; start <= sourceBlocks.length - ruleBlocks.length; start += 1) {
|
|
47
|
+
if (ruleBlocks.every((block, offset) => sourceBlocks[start + offset] === block)) return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function dedupeAlwaysApplyRules(
|
|
54
|
+
alwaysApplyRules: AlwaysApplyRule[] | undefined,
|
|
55
|
+
promptSources: Array<string | null | undefined>,
|
|
56
|
+
): AlwaysApplyRule[] {
|
|
57
|
+
if (!alwaysApplyRules || alwaysApplyRules.length === 0) return [];
|
|
58
|
+
|
|
59
|
+
return alwaysApplyRules.filter(
|
|
60
|
+
rule => !promptSources.some(source => promptSourceContainsRule(source, rule.content)),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dedupePromptSource(source: string | null | undefined, otherSources: Array<string | null | undefined>): string {
|
|
65
|
+
const resolvedSource = firstNonEmpty(source);
|
|
66
|
+
if (!resolvedSource) return "";
|
|
67
|
+
|
|
68
|
+
return otherSources.some(otherSource => promptSourceContainsRule(otherSource, resolvedSource)) ? "" : resolvedSource;
|
|
69
|
+
}
|
|
19
70
|
|
|
20
71
|
function firstNonEmpty(...values: (string | undefined | null)[]): string | null {
|
|
21
72
|
for (const value of values) {
|
|
@@ -379,6 +430,8 @@ export interface BuildSystemPromptOptions {
|
|
|
379
430
|
mcpDiscoveryServerSummaries?: string[];
|
|
380
431
|
/** Encourage the agent to delegate via tasks unless changes are trivial. */
|
|
381
432
|
eagerTasks?: boolean;
|
|
433
|
+
/** Rules with alwaysApply=true — their full content is injected into the prompt. */
|
|
434
|
+
alwaysApplyRules?: AlwaysApplyRule[];
|
|
382
435
|
}
|
|
383
436
|
|
|
384
437
|
/** Build the system prompt with tools, guidelines, and context */
|
|
@@ -398,6 +451,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
398
451
|
contextFiles: providedContextFiles,
|
|
399
452
|
skills: providedSkills,
|
|
400
453
|
rules,
|
|
454
|
+
alwaysApplyRules,
|
|
401
455
|
intentField,
|
|
402
456
|
mcpDiscoveryMode = false,
|
|
403
457
|
mcpDiscoveryServerSummaries = [],
|
|
@@ -519,10 +573,16 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
519
573
|
const hasRead = tools?.has("read");
|
|
520
574
|
const filteredSkills = hasRead ? skills : [];
|
|
521
575
|
|
|
576
|
+
const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
|
|
577
|
+
resolvedCustomPrompt,
|
|
578
|
+
resolvedAppendPrompt,
|
|
579
|
+
]);
|
|
580
|
+
const promptSources = [effectiveSystemPromptCustomization, resolvedCustomPrompt, resolvedAppendPrompt];
|
|
581
|
+
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
582
|
+
|
|
522
583
|
const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
|
|
523
584
|
const data = {
|
|
524
|
-
|
|
525
|
-
systemPromptCustomization: resolvedCustomPrompt ? "" : (systemPromptCustomization ?? ""),
|
|
585
|
+
systemPromptCustomization: effectiveSystemPromptCustomization,
|
|
526
586
|
customPrompt: resolvedCustomPrompt,
|
|
527
587
|
appendPrompt: resolvedAppendPrompt ?? "",
|
|
528
588
|
tools: toolNames,
|
|
@@ -533,6 +593,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
533
593
|
agentsMdSearch,
|
|
534
594
|
skills: filteredSkills,
|
|
535
595
|
rules: rules ?? [],
|
|
596
|
+
alwaysApplyRules: injectedAlwaysApplyRules,
|
|
536
597
|
date,
|
|
537
598
|
dateTime,
|
|
538
599
|
cwd: promptCwd,
|
package/src/tools/ask.ts
CHANGED
|
@@ -147,9 +147,11 @@ interface UIContext {
|
|
|
147
147
|
helpText?: string;
|
|
148
148
|
},
|
|
149
149
|
): Promise<string | undefined>;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
editor(
|
|
151
|
+
title: string,
|
|
152
|
+
prefill?: string,
|
|
153
|
+
dialogOptions?: { signal?: AbortSignal },
|
|
154
|
+
editorOptions?: { promptStyle?: boolean },
|
|
153
155
|
): Promise<string | undefined>;
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -207,15 +209,11 @@ async function askSingleQuestion(
|
|
|
207
209
|
return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
|
|
208
210
|
};
|
|
209
211
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
};
|
|
215
|
-
const input = signal
|
|
216
|
-
? await untilAborted(signal, () => ui.input("Enter your response:", { signal, timeout, onTimeout }))
|
|
217
|
-
: await ui.input("Enter your response:", { signal, timeout, onTimeout });
|
|
218
|
-
return { input, timedOut: inputTimedOut };
|
|
212
|
+
const promptForCustomInput = async (): Promise<{ input: string | undefined }> => {
|
|
213
|
+
const dialogOptions = signal ? { signal } : undefined;
|
|
214
|
+
const showCustomInput = () => ui.editor("Enter your response:", undefined, dialogOptions, { promptStyle: true });
|
|
215
|
+
const input = signal ? await untilAborted(signal, showCustomInput) : await showCustomInput();
|
|
216
|
+
return { input };
|
|
219
217
|
};
|
|
220
218
|
|
|
221
219
|
const promptWithProgress = navigation?.progressText ? `${question} (${navigation.progressText})` : question;
|
|
@@ -264,9 +262,11 @@ async function askSingleQuestion(
|
|
|
264
262
|
timedOut = true;
|
|
265
263
|
break;
|
|
266
264
|
}
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
265
|
+
const customResult = await promptForCustomInput();
|
|
266
|
+
if (customResult.input === undefined) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
customInput = customResult.input;
|
|
270
270
|
break;
|
|
271
271
|
}
|
|
272
272
|
|
|
@@ -306,7 +306,7 @@ async function askSingleQuestion(
|
|
|
306
306
|
if (previouslySelected) {
|
|
307
307
|
const selectedIndex = optionLabels.indexOf(previouslySelected);
|
|
308
308
|
if (selectedIndex >= 0) initialIndex = selectedIndex;
|
|
309
|
-
} else if (customInput) {
|
|
309
|
+
} else if (customInput !== undefined) {
|
|
310
310
|
initialIndex = displayLabels.length;
|
|
311
311
|
}
|
|
312
312
|
if (initialIndex !== undefined) {
|
|
@@ -330,11 +330,13 @@ async function askSingleQuestion(
|
|
|
330
330
|
}
|
|
331
331
|
} else if (choice === OTHER_OPTION) {
|
|
332
332
|
if (!selectTimedOut) {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
335
|
-
|
|
333
|
+
const customResult = await promptForCustomInput();
|
|
334
|
+
if (customResult.input !== undefined) {
|
|
335
|
+
customInput = customResult.input;
|
|
336
|
+
selectedOptions = [];
|
|
337
|
+
}
|
|
338
|
+
// If editor was dismissed (undefined), keep prior selectedOptions/customInput intact
|
|
336
339
|
}
|
|
337
|
-
selectedOptions = [];
|
|
338
340
|
} else {
|
|
339
341
|
selectedOptions = [stripRecommendedSuffix(choice)];
|
|
340
342
|
customInput = undefined;
|
|
@@ -344,7 +346,7 @@ async function askSingleQuestion(
|
|
|
344
346
|
}
|
|
345
347
|
}
|
|
346
348
|
|
|
347
|
-
if (timedOut && selectedOptions.length === 0 &&
|
|
349
|
+
if (timedOut && selectedOptions.length === 0 && customInput === undefined) {
|
|
348
350
|
selectedOptions = getAutoSelectionOnTimeout(optionLabels, recommended);
|
|
349
351
|
}
|
|
350
352
|
|
|
@@ -352,7 +354,7 @@ async function askSingleQuestion(
|
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
function formatQuestionResult(result: QuestionResult): string {
|
|
355
|
-
if (result.customInput) {
|
|
357
|
+
if (result.customInput !== undefined) {
|
|
356
358
|
return `${result.id}: "${result.customInput}"`;
|
|
357
359
|
}
|
|
358
360
|
if (result.selectedOptions.length > 0) {
|
|
@@ -415,7 +417,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
415
417
|
const extensionUi = context.ui;
|
|
416
418
|
const ui: UIContext = {
|
|
417
419
|
select: (prompt, options, dialogOptions) => extensionUi.select(prompt, options, dialogOptions),
|
|
418
|
-
|
|
420
|
+
editor: (title, prefill, dialogOptions, editorOptions) =>
|
|
421
|
+
extensionUi.editor(title, prefill, dialogOptions, editorOptions),
|
|
419
422
|
};
|
|
420
423
|
|
|
421
424
|
// Determine timeout based on settings and plan mode
|
|
@@ -467,7 +470,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
467
470
|
const [q] = params.questions;
|
|
468
471
|
const { optionLabels, selectedOptions, customInput, cancelled, timedOut } = await askQuestion(q);
|
|
469
472
|
|
|
470
|
-
if (!timedOut && (cancelled || (selectedOptions.length === 0 &&
|
|
473
|
+
if (!timedOut && (cancelled || (selectedOptions.length === 0 && customInput === undefined))) {
|
|
471
474
|
context.abort();
|
|
472
475
|
throw new ToolAbortError("Ask tool was cancelled by the user");
|
|
473
476
|
}
|
|
@@ -479,16 +482,23 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
479
482
|
customInput,
|
|
480
483
|
};
|
|
481
484
|
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
? `User selected: ${selectedOptions.join(", ")}`
|
|
488
|
-
: `User selected: ${selectedOptions[0]}`;
|
|
489
|
-
} else {
|
|
490
|
-
responseText = "User cancelled the selection";
|
|
485
|
+
const responseParts: string[] = [];
|
|
486
|
+
if (selectedOptions.length > 0) {
|
|
487
|
+
responseParts.push(
|
|
488
|
+
q.multi ? `User selected: ${selectedOptions.join(", ")}` : `User selected: ${selectedOptions[0]}`,
|
|
489
|
+
);
|
|
491
490
|
}
|
|
491
|
+
if (customInput !== undefined) {
|
|
492
|
+
responseParts.push(
|
|
493
|
+
customInput.includes("\n")
|
|
494
|
+
? `User provided custom input:\n${customInput
|
|
495
|
+
.split("\n")
|
|
496
|
+
.map(line => ` ${line}`)
|
|
497
|
+
.join("\n")}`
|
|
498
|
+
: `User provided custom input: ${customInput}`,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const responseText = responseParts.length > 0 ? responseParts.join("\n") : "User cancelled the selection";
|
|
492
502
|
|
|
493
503
|
return { content: [{ type: "text" as const, text: responseText }], details };
|
|
494
504
|
}
|
|
@@ -570,6 +580,25 @@ interface AskRenderArgs {
|
|
|
570
580
|
}>;
|
|
571
581
|
}
|
|
572
582
|
|
|
583
|
+
/** Render custom input as a single block with continuation lines (not one entry per line) */
|
|
584
|
+
function renderCustomInput(
|
|
585
|
+
uiTheme: Theme,
|
|
586
|
+
prefix: string,
|
|
587
|
+
customInput: string,
|
|
588
|
+
isLastEntry: boolean,
|
|
589
|
+
includeLeadingNewline = true,
|
|
590
|
+
): string {
|
|
591
|
+
const lines = customInput.split("\n");
|
|
592
|
+
const branch = isLastEntry ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
593
|
+
const firstLine = lines[0] ?? "";
|
|
594
|
+
let text = `${includeLeadingNewline ? "\n" : ""}${prefix}${uiTheme.fg("dim", branch)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", firstLine)}`;
|
|
595
|
+
const continuationIndent = isLastEntry ? " " : `${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
|
|
596
|
+
for (let i = 1; i < lines.length; i++) {
|
|
597
|
+
text += `\n${prefix}${continuationIndent} ${uiTheme.fg("toolOutput", lines[i])}`;
|
|
598
|
+
}
|
|
599
|
+
return text;
|
|
600
|
+
}
|
|
601
|
+
|
|
573
602
|
export const askToolRenderer = {
|
|
574
603
|
renderCall(args: AskRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
575
604
|
const label = formatTitle("Ask", uiTheme);
|
|
@@ -658,7 +687,7 @@ export const askToolRenderer = {
|
|
|
658
687
|
// Multi-part results
|
|
659
688
|
if (details.results && details.results.length > 0) {
|
|
660
689
|
const hasAnySelection = details.results.some(
|
|
661
|
-
r => r.customInput || (r.selectedOptions && r.selectedOptions.length > 0),
|
|
690
|
+
r => r.customInput !== undefined || (r.selectedOptions && r.selectedOptions.length > 0),
|
|
662
691
|
);
|
|
663
692
|
const header = renderStatusLine(
|
|
664
693
|
{
|
|
@@ -676,7 +705,7 @@ export const askToolRenderer = {
|
|
|
676
705
|
const isLastQuestion = i === details.results.length - 1;
|
|
677
706
|
const branch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
678
707
|
const continuation = isLastQuestion ? " " : `${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
|
|
679
|
-
const hasSelection = r.customInput || r.selectedOptions.length > 0;
|
|
708
|
+
const hasSelection = r.customInput !== undefined || r.selectedOptions.length > 0;
|
|
680
709
|
const statusIcon = hasSelection
|
|
681
710
|
? uiTheme.styledSymbol("status.success", "success")
|
|
682
711
|
: uiTheme.styledSymbol("status.warning", "warning");
|
|
@@ -686,23 +715,30 @@ export const askToolRenderer = {
|
|
|
686
715
|
);
|
|
687
716
|
container.addChild(new Markdown(r.question, 3, 0, mdTheme, accentStyle));
|
|
688
717
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
answerText = `${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
718
|
+
const answerLines: string[] = [];
|
|
719
|
+
for (let j = 0; j < r.selectedOptions.length; j++) {
|
|
720
|
+
const isLast = j === r.selectedOptions.length - 1 && r.customInput === undefined;
|
|
721
|
+
const optBranch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
722
|
+
const selectedLabel = renderInlineMarkdown(r.selectedOptions[j], mdTheme, t =>
|
|
723
|
+
uiTheme.fg("toolOutput", t),
|
|
724
|
+
);
|
|
725
|
+
answerLines.push(
|
|
726
|
+
`${continuation}${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${selectedLabel}`,
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (answerLines.length > 0) {
|
|
730
|
+
container.addChild(new Text(answerLines.join("\n"), 0, 0));
|
|
703
731
|
}
|
|
704
|
-
if (
|
|
705
|
-
container.addChild(new Text(
|
|
732
|
+
if (r.customInput !== undefined) {
|
|
733
|
+
container.addChild(new Text(renderCustomInput(uiTheme, continuation, r.customInput, true, false), 0, 0));
|
|
734
|
+
} else if (r.selectedOptions.length === 0) {
|
|
735
|
+
container.addChild(
|
|
736
|
+
new Text(
|
|
737
|
+
`${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
|
|
738
|
+
0,
|
|
739
|
+
0,
|
|
740
|
+
),
|
|
741
|
+
);
|
|
706
742
|
}
|
|
707
743
|
}
|
|
708
744
|
return container;
|
|
@@ -715,28 +751,40 @@ export const askToolRenderer = {
|
|
|
715
751
|
return new Text(fallback, 0, 0);
|
|
716
752
|
}
|
|
717
753
|
|
|
718
|
-
const hasSelection =
|
|
754
|
+
const hasSelection =
|
|
755
|
+
details.customInput !== undefined || (details.selectedOptions && details.selectedOptions.length > 0);
|
|
719
756
|
const header = renderStatusLine({ icon: hasSelection ? "success" : "warning", title: "Ask" }, uiTheme);
|
|
720
757
|
const container = new Container();
|
|
721
758
|
container.addChild(new Text(header, 0, 0));
|
|
722
759
|
container.addChild(new Markdown(details.question, 1, 0, mdTheme, accentStyle));
|
|
723
760
|
|
|
724
|
-
|
|
725
|
-
if (details.
|
|
726
|
-
answerText = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", details.customInput)}`;
|
|
727
|
-
} else if (details.selectedOptions && details.selectedOptions.length > 0) {
|
|
761
|
+
const answerLines: string[] = [];
|
|
762
|
+
if (details.selectedOptions && details.selectedOptions.length > 0) {
|
|
728
763
|
for (let i = 0; i < details.selectedOptions.length; i++) {
|
|
729
|
-
const isLast = i === details.selectedOptions.length - 1;
|
|
764
|
+
const isLast = i === details.selectedOptions.length - 1 && details.customInput === undefined;
|
|
730
765
|
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
731
766
|
const selectedLabel = renderInlineMarkdown(details.selectedOptions[i], mdTheme, t =>
|
|
732
767
|
uiTheme.fg("toolOutput", t),
|
|
733
768
|
);
|
|
734
|
-
|
|
769
|
+
answerLines.push(
|
|
770
|
+
` ${uiTheme.fg("dim", branch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${selectedLabel}`,
|
|
771
|
+
);
|
|
735
772
|
}
|
|
736
|
-
} else {
|
|
737
|
-
answerText = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
738
773
|
}
|
|
739
|
-
|
|
774
|
+
if (answerLines.length > 0) {
|
|
775
|
+
container.addChild(new Text(answerLines.join("\n"), 0, 0));
|
|
776
|
+
}
|
|
777
|
+
if (details.customInput !== undefined) {
|
|
778
|
+
container.addChild(new Text(renderCustomInput(uiTheme, " ", details.customInput, true, false), 0, 0));
|
|
779
|
+
} else if (!details.selectedOptions || details.selectedOptions.length === 0) {
|
|
780
|
+
container.addChild(
|
|
781
|
+
new Text(
|
|
782
|
+
` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
|
|
783
|
+
0,
|
|
784
|
+
0,
|
|
785
|
+
),
|
|
786
|
+
);
|
|
787
|
+
}
|
|
740
788
|
|
|
741
789
|
return container;
|
|
742
790
|
},
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -435,18 +435,6 @@ export const astEditToolRenderer = {
|
|
|
435
435
|
group => !group[0]?.startsWith("Safety cap reached") && !group[0]?.startsWith("Parse issues:"),
|
|
436
436
|
);
|
|
437
437
|
|
|
438
|
-
const getCollapsedChangeLimit = (groups: string[][], maxLines: number): number => {
|
|
439
|
-
if (groups.length === 0) return 0;
|
|
440
|
-
let usedLines = 0;
|
|
441
|
-
let count = 0;
|
|
442
|
-
for (const group of groups) {
|
|
443
|
-
if (count > 0 && usedLines + group.length > maxLines) break;
|
|
444
|
-
usedLines += group.length;
|
|
445
|
-
count += 1;
|
|
446
|
-
if (usedLines >= maxLines) break;
|
|
447
|
-
}
|
|
448
|
-
return count;
|
|
449
|
-
};
|
|
450
438
|
const badge = { label: "proposed", color: "warning" as const };
|
|
451
439
|
const header = renderStatusLine(
|
|
452
440
|
{ icon: limitReached ? "warning" : "success", title: "AST Edit", description, badge, meta },
|
|
@@ -471,14 +459,12 @@ export const astEditToolRenderer = {
|
|
|
471
459
|
const { expanded } = options;
|
|
472
460
|
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
473
461
|
if (cached?.key === key) return cached.lines;
|
|
474
|
-
const maxCollapsed = expanded
|
|
475
|
-
? changeGroups.length
|
|
476
|
-
: getCollapsedChangeLimit(changeGroups, COLLAPSED_CHANGE_LIMIT);
|
|
477
462
|
const changeLines = renderTreeList(
|
|
478
463
|
{
|
|
479
464
|
items: changeGroups,
|
|
480
465
|
expanded,
|
|
481
|
-
maxCollapsed,
|
|
466
|
+
maxCollapsed: changeGroups.length,
|
|
467
|
+
maxCollapsedLines: COLLAPSED_CHANGE_LIMIT,
|
|
482
468
|
itemType: "change",
|
|
483
469
|
renderItem: group =>
|
|
484
470
|
group.map(line => {
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -402,19 +402,6 @@ export const astGrepToolRenderer = {
|
|
|
402
402
|
group => !group[0]?.startsWith("Result limit reached") && !group[0]?.startsWith("Parse issues:"),
|
|
403
403
|
);
|
|
404
404
|
|
|
405
|
-
const getCollapsedMatchLimit = (groups: string[][], maxLines: number): number => {
|
|
406
|
-
if (groups.length === 0) return 0;
|
|
407
|
-
let usedLines = 0;
|
|
408
|
-
let count = 0;
|
|
409
|
-
for (const group of groups) {
|
|
410
|
-
if (count > 0 && usedLines + group.length > maxLines) break;
|
|
411
|
-
usedLines += group.length;
|
|
412
|
-
count += 1;
|
|
413
|
-
if (usedLines >= maxLines) break;
|
|
414
|
-
}
|
|
415
|
-
return count;
|
|
416
|
-
};
|
|
417
|
-
|
|
418
405
|
const extraLines: string[] = [];
|
|
419
406
|
if (limitReached) {
|
|
420
407
|
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path pattern or increase limit"));
|
|
@@ -434,14 +421,12 @@ export const astGrepToolRenderer = {
|
|
|
434
421
|
const { expanded } = options;
|
|
435
422
|
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
436
423
|
if (cached?.key === key) return cached.lines;
|
|
437
|
-
const maxCollapsed = expanded
|
|
438
|
-
? matchGroups.length
|
|
439
|
-
: getCollapsedMatchLimit(matchGroups, COLLAPSED_MATCH_LIMIT);
|
|
440
424
|
const matchLines = renderTreeList(
|
|
441
425
|
{
|
|
442
426
|
items: matchGroups,
|
|
443
427
|
expanded,
|
|
444
|
-
maxCollapsed,
|
|
428
|
+
maxCollapsed: matchGroups.length,
|
|
429
|
+
maxCollapsedLines: COLLAPSED_MATCH_LIMIT,
|
|
445
430
|
itemType: "match",
|
|
446
431
|
renderItem: group =>
|
|
447
432
|
group.map(line => {
|
package/src/tools/browser.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { Readability } from "@mozilla/readability";
|
|
@@ -18,9 +19,10 @@ import type {
|
|
|
18
19
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
19
20
|
import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
|
|
20
21
|
import type { ToolSession } from "../sdk";
|
|
21
|
-
import {
|
|
22
|
+
import { resizeImage } from "../utils/image-resize";
|
|
22
23
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
23
24
|
import type { OutputMeta } from "./output-meta";
|
|
25
|
+
import { expandPath, resolveToCwd } from "./path-utils";
|
|
24
26
|
import stealthTamperingScript from "./puppeteer/00_stealth_tampering.txt" with { type: "text" };
|
|
25
27
|
import stealthActivityScript from "./puppeteer/01_stealth_activity.txt" with { type: "text" };
|
|
26
28
|
import stealthHairlineScript from "./puppeteer/02_stealth_hairline.txt" with { type: "text" };
|
|
@@ -35,6 +37,7 @@ import stealthPluginsScript from "./puppeteer/10_stealth_plugins.txt" with { typ
|
|
|
35
37
|
import stealthHardwareScript from "./puppeteer/11_stealth_hardware.txt" with { type: "text" };
|
|
36
38
|
import stealthCodecsScript from "./puppeteer/12_stealth_codecs.txt" with { type: "text" };
|
|
37
39
|
import stealthWorkerScript from "./puppeteer/13_stealth_worker.txt" with { type: "text" };
|
|
40
|
+
import { formatScreenshot } from "./render-utils";
|
|
38
41
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
39
42
|
import { toolResult } from "./tool-result";
|
|
40
43
|
import { clampTimeout } from "./tool-timeouts";
|
|
@@ -1364,23 +1367,38 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1364
1367
|
{ type: "image", data: buffer.toBase64(), mimeType: "image/png" },
|
|
1365
1368
|
{ maxBytes: 0.75 * 1024 * 1024 },
|
|
1366
1369
|
);
|
|
1367
|
-
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
`
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
lines.push(dimensionNote);
|
|
1370
|
+
// Resolve destination: user-defined path > screenshotDir (auto-named) > temp file.
|
|
1371
|
+
const screenshotDir = (() => {
|
|
1372
|
+
const v = this.session.settings.get("browser.screenshotDir") as string | undefined;
|
|
1373
|
+
return v ? expandPath(v) : undefined;
|
|
1374
|
+
})();
|
|
1375
|
+
const paramPath = params.path ? resolveToCwd(params.path as string, this.session.cwd) : undefined;
|
|
1376
|
+
let dest: string;
|
|
1377
|
+
if (paramPath) {
|
|
1378
|
+
dest = paramPath;
|
|
1379
|
+
} else if (screenshotDir) {
|
|
1380
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -1);
|
|
1381
|
+
dest = path.join(screenshotDir, `screenshot-${ts}.png`);
|
|
1382
|
+
} else {
|
|
1383
|
+
dest = path.join(os.tmpdir(), `omp-sshots-${Snowflake.next()}.png`);
|
|
1382
1384
|
}
|
|
1383
|
-
|
|
1385
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
1386
|
+
// Full-res buffer when saving to a user-defined location; resized (API copy) for temp-only.
|
|
1387
|
+
const saveFullRes = !!(paramPath || screenshotDir);
|
|
1388
|
+
const savedBuffer = saveFullRes ? buffer : resized.buffer;
|
|
1389
|
+
const savedMimeType = saveFullRes ? "image/png" : resized.mimeType;
|
|
1390
|
+
await Bun.write(dest, savedBuffer);
|
|
1391
|
+
details.screenshotPath = dest;
|
|
1392
|
+
details.mimeType = savedMimeType;
|
|
1393
|
+
details.bytes = savedBuffer.length;
|
|
1394
|
+
|
|
1395
|
+
const lines = formatScreenshot({
|
|
1396
|
+
saveFullRes,
|
|
1397
|
+
savedMimeType,
|
|
1398
|
+
savedByteLength: savedBuffer.length,
|
|
1399
|
+
dest,
|
|
1400
|
+
resized,
|
|
1401
|
+
});
|
|
1384
1402
|
return toolResult(details)
|
|
1385
1403
|
.content([
|
|
1386
1404
|
{ type: "text", text: lines.join("\n") },
|
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
|
|
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 => {
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -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
|
}
|