@oh-my-pi/pi-coding-agent 15.8.2 → 15.9.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 +82 -29
- package/dist/types/capability/skill.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +36 -22
- package/dist/types/debug/protocol-probe.d.ts +38 -0
- package/dist/types/debug/terminal-info.d.ts +34 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/transports/stdio.d.ts +16 -7
- package/dist/types/modes/components/assistant-message.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/components/todo-reminder.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +36 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +20 -1
- package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
- package/dist/types/plan-mode/plan-protection.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/indexed-session-storage.d.ts +59 -0
- package/dist/types/session/redis-session-storage.d.ts +12 -85
- package/dist/types/session/session-manager.d.ts +20 -0
- package/dist/types/session/session-storage.d.ts +5 -7
- package/dist/types/session/sql-session-storage.d.ts +16 -85
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/telemetry-export.d.ts +19 -0
- package/dist/types/tiny/compiled-runtime.d.ts +35 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -2
- package/dist/types/tools/path-utils.d.ts +1 -1
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
- package/dist/types/utils/jj.d.ts +1 -1
- package/dist/types/utils/session-color.d.ts +7 -2
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +65 -1
- package/package.json +15 -9
- package/scripts/build-binary.ts +12 -0
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +1 -1
- package/src/config/settings-schema.ts +22 -55
- package/src/debug/index.ts +67 -1
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/terminal-info.ts +127 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -3
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +3 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +12 -0
- package/src/mcp/transports/stdio.ts +37 -12
- package/src/modes/acp/acp-event-mapper.ts +7 -7
- package/src/modes/components/assistant-message.ts +3 -2
- package/src/modes/components/hook-selector.ts +149 -14
- package/src/modes/components/session-selector.ts +36 -5
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/todo-reminder.ts +1 -1
- package/src/modes/components/tool-execution.ts +9 -4
- package/src/modes/components/transcript-container.ts +91 -0
- package/src/modes/controllers/event-controller.ts +12 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +18 -6
- package/src/modes/print-mode.ts +5 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +48 -8
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/priority.json +4 -0
- package/src/prompts/goals/goal-continuation.md +1 -1
- package/src/prompts/system/eager-todo.md +3 -3
- package/src/prompts/system/orchestrate-notice.md +5 -5
- package/src/prompts/system/plan-mode-approved.md +14 -17
- package/src/prompts/system/plan-mode-reference.md +3 -6
- package/src/prompts/system/subagent-system-prompt.md +11 -0
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/browser.md +5 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/prompts/tools/{todo-write.md → todo.md} +8 -0
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +29 -16
- package/src/session/indexed-session-storage.ts +430 -0
- package/src/session/redis-session-storage.ts +66 -377
- package/src/session/session-manager.ts +102 -22
- package/src/session/session-storage.ts +148 -68
- package/src/session/sql-session-storage.ts +131 -382
- package/src/slash-commands/helpers/todo.ts +2 -2
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +51 -1
- package/src/telemetry-export.ts +126 -0
- package/src/tiny/compiled-runtime.ts +179 -0
- package/src/tiny/worker.ts +24 -2
- package/src/tools/ask.ts +133 -87
- package/src/tools/fetch.ts +17 -4
- package/src/tools/find.ts +2 -2
- package/src/tools/index.ts +6 -4
- package/src/tools/path-utils.ts +16 -7
- package/src/tools/renderers.ts +2 -2
- package/src/tools/search.ts +6 -3
- package/src/tools/{todo-write.ts → todo.ts} +32 -35
- package/src/utils/jj.ts +28 -5
- package/src/utils/session-color.ts +39 -14
- package/src/web/search/index.ts +1 -1
- package/src/web/search/provider.ts +18 -34
- package/src/web/search/types.ts +73 -32
package/src/tools/ask.ts
CHANGED
|
@@ -16,7 +16,15 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type Component,
|
|
21
|
+
Container,
|
|
22
|
+
Markdown,
|
|
23
|
+
type MarkdownTheme,
|
|
24
|
+
renderInlineMarkdown,
|
|
25
|
+
TERMINAL,
|
|
26
|
+
Text,
|
|
27
|
+
} from "@oh-my-pi/pi-tui";
|
|
20
28
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
21
29
|
import * as z from "zod/v4";
|
|
22
30
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -164,6 +172,9 @@ interface UIContext {
|
|
|
164
172
|
onLeft?: () => void;
|
|
165
173
|
onRight?: () => void;
|
|
166
174
|
helpText?: string;
|
|
175
|
+
selectionMarker?: "radio" | "checkbox";
|
|
176
|
+
checkedIndices?: readonly number[];
|
|
177
|
+
markableCount?: number;
|
|
167
178
|
},
|
|
168
179
|
): Promise<string | undefined>;
|
|
169
180
|
editor(
|
|
@@ -191,6 +202,7 @@ async function askSingleQuestion(
|
|
|
191
202
|
prompt: string,
|
|
192
203
|
optionsToShow: ExtensionUISelectItem[],
|
|
193
204
|
initialIndex?: number,
|
|
205
|
+
marker?: { selectionMarker: "radio" | "checkbox"; checkedIndices?: readonly number[]; markableCount: number },
|
|
194
206
|
): Promise<{ choice: string | undefined; timedOut: boolean; navigation?: "back" | "forward" }> => {
|
|
195
207
|
let timeoutTriggered = false;
|
|
196
208
|
const onTimeout = () => {
|
|
@@ -207,6 +219,9 @@ async function askSingleQuestion(
|
|
|
207
219
|
outline: true,
|
|
208
220
|
onTimeout,
|
|
209
221
|
helpText,
|
|
222
|
+
selectionMarker: marker?.selectionMarker,
|
|
223
|
+
checkedIndices: marker?.checkedIndices,
|
|
224
|
+
markableCount: marker?.markableCount,
|
|
210
225
|
onLeft: navigation?.allowBack
|
|
211
226
|
? () => {
|
|
212
227
|
navigationAction = "back";
|
|
@@ -245,25 +260,27 @@ async function askSingleQuestion(
|
|
|
245
260
|
if (selectedIndex >= 0) cursorIndex = selectedIndex;
|
|
246
261
|
}
|
|
247
262
|
while (true) {
|
|
248
|
-
const opts: ExtensionUISelectItem[] =
|
|
249
|
-
|
|
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));
|
|
254
|
-
}
|
|
263
|
+
const opts: ExtensionUISelectItem[] = questionOptions.map(opt => toSelectOption(opt));
|
|
255
264
|
|
|
256
265
|
if (!navigation?.allowForward && selected.size > 0) {
|
|
257
266
|
opts.push(doneLabel);
|
|
258
267
|
}
|
|
259
268
|
opts.push(OTHER_OPTION);
|
|
260
269
|
|
|
270
|
+
const checkedIndices: number[] = [];
|
|
271
|
+
for (let i = 0; i < questionOptions.length; i++) {
|
|
272
|
+
if (selected.has(questionOptions[i]!.label)) checkedIndices.push(i);
|
|
273
|
+
}
|
|
261
274
|
const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
|
|
262
275
|
const {
|
|
263
276
|
choice,
|
|
264
277
|
timedOut: selectTimedOut,
|
|
265
278
|
navigation: arrowNavigation,
|
|
266
|
-
} = await selectOption(`${prefix}${promptWithProgress}`, opts, cursorIndex
|
|
279
|
+
} = await selectOption(`${prefix}${promptWithProgress}`, opts, cursorIndex, {
|
|
280
|
+
selectionMarker: "checkbox",
|
|
281
|
+
checkedIndices,
|
|
282
|
+
markableCount: questionOptions.length,
|
|
283
|
+
});
|
|
267
284
|
|
|
268
285
|
if (arrowNavigation) {
|
|
269
286
|
return { selectedOptions: Array.from(selected), customInput, timedOut, navigation: arrowNavigation };
|
|
@@ -295,20 +312,10 @@ async function askSingleQuestion(
|
|
|
295
312
|
cursorIndex = selectedIdx;
|
|
296
313
|
}
|
|
297
314
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
opt = choice.slice(checkedPrefix.length);
|
|
303
|
-
} else if (choice.startsWith(uncheckedPrefix)) {
|
|
304
|
-
opt = choice.slice(uncheckedPrefix.length);
|
|
305
|
-
}
|
|
306
|
-
if (opt) {
|
|
307
|
-
if (selected.has(opt)) {
|
|
308
|
-
selected.delete(opt);
|
|
309
|
-
} else {
|
|
310
|
-
selected.add(opt);
|
|
311
|
-
}
|
|
315
|
+
if (selected.has(choice)) {
|
|
316
|
+
selected.delete(choice);
|
|
317
|
+
} else {
|
|
318
|
+
selected.add(choice);
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
if (selectTimedOut) {
|
|
@@ -338,7 +345,10 @@ async function askSingleQuestion(
|
|
|
338
345
|
choice,
|
|
339
346
|
timedOut: selectTimedOut,
|
|
340
347
|
navigation: arrowNavigation,
|
|
341
|
-
} = await selectOption(promptWithProgress, optionsWithNavigation, initialIndex
|
|
348
|
+
} = await selectOption(promptWithProgress, optionsWithNavigation, initialIndex, {
|
|
349
|
+
selectionMarker: "radio",
|
|
350
|
+
markableCount: displayOptions.length,
|
|
351
|
+
});
|
|
342
352
|
timedOut = selectTimedOut;
|
|
343
353
|
|
|
344
354
|
if (arrowNavigation) {
|
|
@@ -419,7 +429,13 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
419
429
|
#sendAskNotification(): void {
|
|
420
430
|
const method = this.session.settings.get("ask.notify");
|
|
421
431
|
if (method === "off") return;
|
|
422
|
-
TERMINAL.sendNotification(
|
|
432
|
+
TERMINAL.sendNotification({
|
|
433
|
+
title: "Oh My Pi",
|
|
434
|
+
body: "Waiting for input",
|
|
435
|
+
type: "ask",
|
|
436
|
+
urgency: "normal",
|
|
437
|
+
actions: "focus",
|
|
438
|
+
});
|
|
423
439
|
}
|
|
424
440
|
|
|
425
441
|
async execute(
|
|
@@ -629,7 +645,65 @@ function renderCustomInput(
|
|
|
629
645
|
return text;
|
|
630
646
|
}
|
|
631
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Marker glyph for a question option. Single-choice questions render circular radio
|
|
650
|
+
* buttons (pick one); multi-select questions render rectangular checkboxes (pick many).
|
|
651
|
+
*/
|
|
652
|
+
function optionMarker(uiTheme: Theme, multi: boolean | undefined, selected: boolean): string {
|
|
653
|
+
if (multi) return selected ? uiTheme.checkbox.checked : uiTheme.checkbox.unchecked;
|
|
654
|
+
return selected ? uiTheme.radio.selected : uiTheme.radio.unselected;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Render the answered option list for a question: every offered option with its
|
|
659
|
+
* selection marker filled in, plus any custom free-text answer. This keeps the
|
|
660
|
+
* result visually identical to the question form (`renderCall`) so the answer
|
|
661
|
+
* reads in place rather than as a detached summary block.
|
|
662
|
+
*
|
|
663
|
+
* `linePrefix` is the indent that precedes each entry's tree branch — a single
|
|
664
|
+
* leading space for top-level (single-question) entries, or the question's
|
|
665
|
+
* vertical continuation for nested (multi-question) entries.
|
|
666
|
+
*/
|
|
667
|
+
function renderAnswerOptions(
|
|
668
|
+
uiTheme: Theme,
|
|
669
|
+
mdTheme: MarkdownTheme,
|
|
670
|
+
linePrefix: string,
|
|
671
|
+
options: string[] | undefined,
|
|
672
|
+
selectedOptions: string[] | undefined,
|
|
673
|
+
multi: boolean | undefined,
|
|
674
|
+
customInput: string | undefined,
|
|
675
|
+
): string {
|
|
676
|
+
const selected = new Set(selectedOptions ?? []);
|
|
677
|
+
// Prefer the full recorded option set; fall back to the selected labels when
|
|
678
|
+
// details omit the options array.
|
|
679
|
+
const list = options && options.length > 0 ? options : (selectedOptions ?? []);
|
|
680
|
+
|
|
681
|
+
// Nothing was chosen (and no custom answer) → a lone cancelled marker.
|
|
682
|
+
if (selected.size === 0 && customInput === undefined) {
|
|
683
|
+
return `${linePrefix}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let text = "";
|
|
687
|
+
for (let i = 0; i < list.length; i++) {
|
|
688
|
+
const label = list[i];
|
|
689
|
+
const isSelected = selected.has(label);
|
|
690
|
+
const isLastEntry = i === list.length - 1 && customInput === undefined;
|
|
691
|
+
const branch = isLastEntry ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
692
|
+
const marker = optionMarker(uiTheme, multi, isSelected);
|
|
693
|
+
const markerStyled = isSelected ? uiTheme.fg("success", marker) : uiTheme.fg("dim", marker);
|
|
694
|
+
const labelStyled = renderInlineMarkdown(label, mdTheme, t =>
|
|
695
|
+
isSelected ? uiTheme.fg("toolOutput", t) : uiTheme.fg("muted", t),
|
|
696
|
+
);
|
|
697
|
+
text += `${text ? "\n" : ""}${linePrefix}${uiTheme.fg("dim", branch)} ${markerStyled} ${labelStyled}`;
|
|
698
|
+
}
|
|
699
|
+
if (customInput !== undefined) {
|
|
700
|
+
text += renderCustomInput(uiTheme, linePrefix, customInput, true, text.length > 0);
|
|
701
|
+
}
|
|
702
|
+
return text;
|
|
703
|
+
}
|
|
704
|
+
|
|
632
705
|
export const askToolRenderer = {
|
|
706
|
+
mergeCallAndResult: true,
|
|
633
707
|
renderCall(args: AskRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
634
708
|
const label = formatTitle("Ask", uiTheme);
|
|
635
709
|
const mdTheme = getMarkdownTheme();
|
|
@@ -663,7 +737,7 @@ export const askToolRenderer = {
|
|
|
663
737
|
const isLastOpt = j === q.options.length - 1;
|
|
664
738
|
const optBranch = isLastOpt ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
665
739
|
const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
|
|
666
|
-
optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", uiTheme.
|
|
740
|
+
optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", optionMarker(uiTheme, q.multi, false))} ${optLabel}`;
|
|
667
741
|
if (opt.description?.trim()) {
|
|
668
742
|
const optContinuation = isLastOpt ? " " : uiTheme.tree.vertical;
|
|
669
743
|
const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t =>
|
|
@@ -697,7 +771,7 @@ export const askToolRenderer = {
|
|
|
697
771
|
const isLast = i === args.options.length - 1;
|
|
698
772
|
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
699
773
|
const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
|
|
700
|
-
optText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", uiTheme.
|
|
774
|
+
optText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", optionMarker(uiTheme, args.multi, false))} ${optLabel}`;
|
|
701
775
|
if (opt.description?.trim()) {
|
|
702
776
|
const continuation = isLast ? " " : uiTheme.tree.vertical;
|
|
703
777
|
const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t => uiTheme.fg("dim", t));
|
|
@@ -745,43 +819,27 @@ export const askToolRenderer = {
|
|
|
745
819
|
for (let i = 0; i < details.results.length; i++) {
|
|
746
820
|
const r = details.results[i];
|
|
747
821
|
const isLastQuestion = i === details.results.length - 1;
|
|
748
|
-
const
|
|
749
|
-
const continuation = isLastQuestion ? "
|
|
750
|
-
const
|
|
751
|
-
const statusIcon = hasSelection
|
|
752
|
-
? uiTheme.styledSymbol("status.success", "success")
|
|
753
|
-
: uiTheme.styledSymbol("status.warning", "warning");
|
|
822
|
+
const qBranch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
823
|
+
const continuation = isLastQuestion ? " " : uiTheme.tree.vertical;
|
|
824
|
+
const linePrefix = ` ${uiTheme.fg("dim", continuation)} `;
|
|
754
825
|
|
|
755
|
-
container.addChild(
|
|
756
|
-
new Text(` ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0),
|
|
757
|
-
);
|
|
826
|
+
container.addChild(new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0));
|
|
758
827
|
container.addChild(new Markdown(r.question, 3, 0, mdTheme, accentStyle));
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
if (answerLines.length > 0) {
|
|
772
|
-
container.addChild(new Text(answerLines.join("\n"), 0, 0));
|
|
773
|
-
}
|
|
774
|
-
if (r.customInput !== undefined) {
|
|
775
|
-
container.addChild(new Text(renderCustomInput(uiTheme, continuation, r.customInput, true, false), 0, 0));
|
|
776
|
-
} else if (r.selectedOptions.length === 0) {
|
|
777
|
-
container.addChild(
|
|
778
|
-
new Text(
|
|
779
|
-
`${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
|
|
780
|
-
0,
|
|
781
|
-
0,
|
|
828
|
+
container.addChild(
|
|
829
|
+
new Text(
|
|
830
|
+
renderAnswerOptions(
|
|
831
|
+
uiTheme,
|
|
832
|
+
mdTheme,
|
|
833
|
+
linePrefix,
|
|
834
|
+
r.options,
|
|
835
|
+
r.selectedOptions,
|
|
836
|
+
r.multi,
|
|
837
|
+
r.customInput,
|
|
782
838
|
),
|
|
783
|
-
|
|
784
|
-
|
|
839
|
+
0,
|
|
840
|
+
0,
|
|
841
|
+
),
|
|
842
|
+
);
|
|
785
843
|
}
|
|
786
844
|
return container;
|
|
787
845
|
}
|
|
@@ -800,33 +858,21 @@ export const askToolRenderer = {
|
|
|
800
858
|
container.addChild(new Text(header, 0, 0));
|
|
801
859
|
container.addChild(new Markdown(details.question, 1, 0, mdTheme, accentStyle));
|
|
802
860
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
if (answerLines.length > 0) {
|
|
817
|
-
container.addChild(new Text(answerLines.join("\n"), 0, 0));
|
|
818
|
-
}
|
|
819
|
-
if (details.customInput !== undefined) {
|
|
820
|
-
container.addChild(new Text(renderCustomInput(uiTheme, " ", details.customInput, true, false), 0, 0));
|
|
821
|
-
} else if (!details.selectedOptions || details.selectedOptions.length === 0) {
|
|
822
|
-
container.addChild(
|
|
823
|
-
new Text(
|
|
824
|
-
` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
|
|
825
|
-
0,
|
|
826
|
-
0,
|
|
861
|
+
container.addChild(
|
|
862
|
+
new Text(
|
|
863
|
+
renderAnswerOptions(
|
|
864
|
+
uiTheme,
|
|
865
|
+
mdTheme,
|
|
866
|
+
" ",
|
|
867
|
+
details.options,
|
|
868
|
+
details.selectedOptions,
|
|
869
|
+
details.multi,
|
|
870
|
+
details.customInput,
|
|
827
871
|
),
|
|
828
|
-
|
|
829
|
-
|
|
872
|
+
0,
|
|
873
|
+
0,
|
|
874
|
+
),
|
|
875
|
+
);
|
|
830
876
|
|
|
831
877
|
return container;
|
|
832
878
|
},
|
package/src/tools/fetch.ts
CHANGED
|
@@ -127,9 +127,21 @@ function buildLlmEndpointCandidates(url: string): string[] {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
*
|
|
130
|
+
* Repair a URL whose scheme `//` collapsed to a single `/`. Node's `path.normalize`/
|
|
131
|
+
* `path.resolve` collapse `//` → `/`, so any URL routed through path normalization arrives
|
|
132
|
+
* as `https:/host/x` instead of `https://host/x`. No local filesystem path begins with
|
|
133
|
+
* `http:/` or `https:/`, so repairing the scheme back to `//` is unambiguous.
|
|
134
|
+
*/
|
|
135
|
+
function repairCollapsedScheme(value: string): string {
|
|
136
|
+
const m = value.match(/^(https?):\/(?!\/)/i);
|
|
137
|
+
return m ? `${m[1]}://${value.slice(m[0].length)}` : value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Normalize URL (repair a collapsed scheme, then add a scheme if one is missing).
|
|
131
142
|
*/
|
|
132
143
|
function normalizeUrl(url: string): string {
|
|
144
|
+
url = repairCollapsedScheme(url);
|
|
133
145
|
if (!url.match(/^https?:\/\//i)) {
|
|
134
146
|
return `https://${url}`;
|
|
135
147
|
}
|
|
@@ -137,7 +149,7 @@ function normalizeUrl(url: string): string {
|
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
export function isReadableUrlPath(value: string): boolean {
|
|
140
|
-
return /^https
|
|
152
|
+
return /^https?:\/\/?/i.test(value) || /^www\./i.test(value);
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
// URL line selectors mirror the file form: `:50`, `:50-100`, `:50+150`, `:5-10,20-30`, `:raw`,
|
|
@@ -168,8 +180,9 @@ function isUrlSelectorToken(token: string): boolean {
|
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
export function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null {
|
|
171
|
-
const
|
|
172
|
-
const
|
|
183
|
+
const repaired = repairCollapsedScheme(readPath);
|
|
184
|
+
const embedded = tryExtractEmbeddedUrlSelector(repaired);
|
|
185
|
+
const urlPath = embedded?.path ?? repaired;
|
|
173
186
|
if (!isReadableUrlPath(urlPath)) {
|
|
174
187
|
return null;
|
|
175
188
|
}
|
package/src/tools/find.ts
CHANGED
|
@@ -368,8 +368,8 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
368
368
|
let timedOut = false;
|
|
369
369
|
try {
|
|
370
370
|
const result = await doGlob(useGitignore);
|
|
371
|
-
//
|
|
372
|
-
//
|
|
371
|
+
// Native glob returns a bounded mtime-ranked set; keep the JS sort for
|
|
372
|
+
// deterministic ordering across cached and uncached native paths.
|
|
373
373
|
result.matches.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
|
|
374
374
|
matches = result.matches;
|
|
375
375
|
} catch (error) {
|
package/src/tools/index.ts
CHANGED
|
@@ -53,7 +53,7 @@ import { reportFindingTool } from "./review";
|
|
|
53
53
|
import { SearchTool } from "./search";
|
|
54
54
|
import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
55
55
|
import { loadSshTool } from "./ssh";
|
|
56
|
-
import { type TodoPhase,
|
|
56
|
+
import { type TodoPhase, TodoTool } from "./todo";
|
|
57
57
|
import { WriteTool } from "./write";
|
|
58
58
|
import { YieldTool } from "./yield";
|
|
59
59
|
|
|
@@ -94,7 +94,7 @@ export * from "./review";
|
|
|
94
94
|
export * from "./search";
|
|
95
95
|
export * from "./search-tool-bm25";
|
|
96
96
|
export * from "./ssh";
|
|
97
|
-
export * from "./todo
|
|
97
|
+
export * from "./todo";
|
|
98
98
|
export * from "./tts";
|
|
99
99
|
export * from "./write";
|
|
100
100
|
export * from "./yield";
|
|
@@ -191,6 +191,8 @@ export interface ToolSession {
|
|
|
191
191
|
settings: Settings;
|
|
192
192
|
/** Plan mode state (if active) */
|
|
193
193
|
getPlanModeState?: () => PlanModeState | undefined;
|
|
194
|
+
/** Path of the session's active plan reference (e.g. `local://<title>.md`); defaults to `local://PLAN.md`. */
|
|
195
|
+
getPlanReferencePath?: () => string;
|
|
194
196
|
/** Goal mode state (if active or paused) */
|
|
195
197
|
getGoalModeState?: () => GoalModeState | undefined;
|
|
196
198
|
/** Goal runtime for the active agent session. */
|
|
@@ -317,7 +319,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
317
319
|
task: s => TaskTool.create(s),
|
|
318
320
|
job: JobTool.createIf,
|
|
319
321
|
irc: IrcTool.createIf,
|
|
320
|
-
|
|
322
|
+
todo: s => new TodoTool(s),
|
|
321
323
|
web_search: s => new WebSearchTool(s),
|
|
322
324
|
search_tool_bm25: SearchToolBm25Tool.createIf,
|
|
323
325
|
write: s => new WriteTool(s),
|
|
@@ -419,7 +421,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
419
421
|
if (name === "bash") return true;
|
|
420
422
|
if (name === "eval") return allowEval;
|
|
421
423
|
if (name === "debug") return session.settings.get("debug.enabled");
|
|
422
|
-
if (name === "
|
|
424
|
+
if (name === "todo") return !includeYield && session.settings.get("todo.enabled");
|
|
423
425
|
if (name === "find") return session.settings.get("find.enabled");
|
|
424
426
|
if (name === "search") return session.settings.get("search.enabled");
|
|
425
427
|
if (name === "github") return session.settings.get("github.enabled");
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -7,14 +7,22 @@ import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
|
|
|
7
7
|
import { ToolError } from "./tool-errors";
|
|
8
8
|
|
|
9
9
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// A single line-range chunk: `N`, `N-M`, `N+K`, or open-ended `N-`. `..` is
|
|
11
|
+
// accepted everywhere `-` is, as a forgiving alias for Rust/Python-style ranges
|
|
12
|
+
// (e.g. `2724..2727` == `2724-2727`, `2724..` == `2724-`); it is normalized to
|
|
13
|
+
// `-` in parseLineRangeChunk. Keep this fragment and LINE_RANGE_CHUNK_RE in sync.
|
|
14
|
+
const RANGE_CHUNK_SRC = String.raw`L?\d+(?:(?:[-+]|\.\.)L?\d+|-|\.\.)?`;
|
|
15
|
+
const RANGE_LIST_SRC = `${RANGE_CHUNK_SRC}(?:,${RANGE_CHUNK_SRC})*`;
|
|
16
|
+
const FILE_LINE_RANGE_RE = new RegExp(`^(?:${RANGE_LIST_SRC}|raw|conflicts)$`, "i");
|
|
17
|
+
const FILE_LINE_RANGE_ONLY_RE = new RegExp(`^${RANGE_LIST_SRC}$`, "i");
|
|
12
18
|
const FILE_RAW_ONLY_RE = /^raw$/i;
|
|
13
19
|
// Permissive selector chunk for internal URLs — accepts well-formed selectors
|
|
14
20
|
// plus common malformed shapes (e.g. `:-N`) so the read tool peels the entire
|
|
15
21
|
// selector chain off before dispatching to a protocol handler.
|
|
16
|
-
const INTERNAL_URL_SELECTOR_PART_RE =
|
|
17
|
-
|
|
22
|
+
const INTERNAL_URL_SELECTOR_PART_RE = new RegExp(
|
|
23
|
+
String.raw`^(?:raw|conflicts|${RANGE_LIST_SRC}|-\d+(?:[-+]\d+)?)$`,
|
|
24
|
+
"i",
|
|
25
|
+
);
|
|
18
26
|
// Schemes whose host grammar is identifier-shaped, so any trailing
|
|
19
27
|
// `:<selector-chunk>` is unambiguously a read-tool selector. `mcp://` is
|
|
20
28
|
// excluded because mcp resource URIs may legitimately contain colons.
|
|
@@ -144,9 +152,9 @@ export interface LineRange {
|
|
|
144
152
|
endLine: number | undefined;
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
const LINE_RANGE_CHUNK_RE = /^L?(\d+)(?:([-+])L?(\d+)?)?$/i;
|
|
155
|
+
const LINE_RANGE_CHUNK_RE = /^L?(\d+)(?:(\.\.|[-+])L?(\d+)?)?$/i;
|
|
148
156
|
|
|
149
|
-
/** Parse a single `N`, `N-M`, `N-`,
|
|
157
|
+
/** Parse a single `N`, `N-M`, `N-`, `N+K`, or `..`-aliased (`N..M`, `N..`) chunk. Throws via {@link ToolError} on invalid bounds. */
|
|
150
158
|
export function parseLineRangeChunk(sel: string): LineRange | null {
|
|
151
159
|
const lineMatch = LINE_RANGE_CHUNK_RE.exec(sel);
|
|
152
160
|
if (!lineMatch) return null;
|
|
@@ -154,7 +162,8 @@ export function parseLineRangeChunk(sel: string): LineRange | null {
|
|
|
154
162
|
if (rawStart < 1) {
|
|
155
163
|
throw new ToolError("Line selector 0 is invalid; lines are 1-indexed. Use :1.");
|
|
156
164
|
}
|
|
157
|
-
|
|
165
|
+
// `..` is a forgiving alias for `-` (e.g. `2724..2727` == `2724-2727`).
|
|
166
|
+
const sep = lineMatch[2] === ".." ? "-" : lineMatch[2];
|
|
158
167
|
const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : undefined;
|
|
159
168
|
let rawEnd: number | undefined;
|
|
160
169
|
if (sep === "+") {
|
package/src/tools/renderers.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { resolveToolRenderer } from "./resolve";
|
|
|
28
28
|
import { searchToolRenderer } from "./search";
|
|
29
29
|
import { searchToolBm25Renderer } from "./search-tool-bm25";
|
|
30
30
|
import { sshToolRenderer } from "./ssh";
|
|
31
|
-
import {
|
|
31
|
+
import { todoToolRenderer } from "./todo";
|
|
32
32
|
import { writeToolRenderer } from "./write";
|
|
33
33
|
|
|
34
34
|
type ToolRenderer = {
|
|
@@ -67,7 +67,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
67
67
|
search_tool_bm25: searchToolBm25Renderer as ToolRenderer,
|
|
68
68
|
ssh: sshToolRenderer as ToolRenderer,
|
|
69
69
|
task: taskToolRenderer as ToolRenderer,
|
|
70
|
-
|
|
70
|
+
todo: todoToolRenderer as ToolRenderer,
|
|
71
71
|
github: githubToolRenderer as ToolRenderer,
|
|
72
72
|
goal: goalToolRenderer as ToolRenderer,
|
|
73
73
|
web_search: webSearchToolRenderer as ToolRenderer,
|
package/src/tools/search.ts
CHANGED
|
@@ -61,9 +61,10 @@ const searchSchema = z
|
|
|
61
61
|
.object({
|
|
62
62
|
pattern: z.string().describe("regex pattern"),
|
|
63
63
|
paths: z
|
|
64
|
-
.union([searchPathEntrySchema, z.array(searchPathEntrySchema)
|
|
64
|
+
.union([searchPathEntrySchema, z.array(searchPathEntrySchema)])
|
|
65
|
+
.optional()
|
|
65
66
|
.describe(
|
|
66
|
-
|
|
67
|
+
'file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges. Omitted or empty -> searches the workspace root (".")',
|
|
67
68
|
),
|
|
68
69
|
i: z.boolean().optional().describe("case-insensitive search"),
|
|
69
70
|
gitignore: z.boolean().optional().describe("respect gitignore"),
|
|
@@ -638,7 +639,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
638
639
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
639
640
|
throw new ToolError("Skip must be a non-negative number");
|
|
640
641
|
}
|
|
641
|
-
const
|
|
642
|
+
const scopedPaths = toPathList(rawPaths);
|
|
643
|
+
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
644
|
+
const rawEntries = await expandDelimitedPathEntries(effectivePaths, this.session.cwd);
|
|
642
645
|
const pathSpecs = parsePathSpecs(rawEntries);
|
|
643
646
|
const paths = pathSpecs.map(spec => spec.clean);
|
|
644
647
|
const {
|