@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.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 +62 -0
- package/dist/cli.js +3799 -3729
- package/dist/types/async/job-manager.d.ts +8 -0
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +19 -9
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +100 -0
- package/dist/types/modes/components/model-hub.d.ts +52 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/async/job-manager.ts +9 -0
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +18 -7
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +32 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +769 -0
- package/src/modes/components/model-hub.ts +2002 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +160 -97
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +8 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +50 -24
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +186 -49
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
package/src/tools/ask.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Markdown,
|
|
24
24
|
type MarkdownTheme,
|
|
25
25
|
renderInlineMarkdown,
|
|
26
|
+
replaceTabs,
|
|
26
27
|
TERMINAL,
|
|
27
28
|
Text,
|
|
28
29
|
truncateToWidth,
|
|
@@ -44,17 +45,34 @@ import { ToolAbortError } from "./tool-errors";
|
|
|
44
45
|
// Types
|
|
45
46
|
// =============================================================================
|
|
46
47
|
|
|
48
|
+
const OTHER_OPTION = "Other (type your own)";
|
|
49
|
+
const CHAT_ABOUT_THIS_OPTION = "Chat about this";
|
|
50
|
+
const NEXT_OPTION = "Next →";
|
|
51
|
+
const RESERVED_OPTION_LABELS: Record<string, true> = {
|
|
52
|
+
[OTHER_OPTION]: true,
|
|
53
|
+
[CHAT_ABOUT_THIS_OPTION]: true,
|
|
54
|
+
[NEXT_OPTION]: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
47
57
|
const OptionItem = arkType({
|
|
48
58
|
label: arkType("string").describe("display label"),
|
|
49
59
|
"description?": arkType("string").describe("optional explanatory text displayed below the label"),
|
|
60
|
+
"preview?": arkType("string").describe("optional rich preview content for interactive ask dialogs"),
|
|
50
61
|
});
|
|
51
62
|
|
|
52
63
|
const QuestionItem = arkType({
|
|
53
64
|
id: arkType("string").describe("question id"),
|
|
54
65
|
question: arkType("string").describe("question text"),
|
|
66
|
+
"header?": arkType("string").describe("optional short display chip for rich ask dialogs"),
|
|
55
67
|
options: OptionItem.array().describe("available options"),
|
|
56
68
|
"multi?": arkType("boolean").describe("allow multiple selections"),
|
|
57
69
|
"recommended?": arkType("number").describe("recommended option index"),
|
|
70
|
+
}).narrow((question, ctx) => {
|
|
71
|
+
const reserved = question.options.find(option => RESERVED_OPTION_LABELS[option.label] === true);
|
|
72
|
+
return (
|
|
73
|
+
reserved === undefined ||
|
|
74
|
+
ctx.mustBe(`defined with option labels that do not collide with reserved runtime labels: ${reserved.label}`)
|
|
75
|
+
);
|
|
58
76
|
});
|
|
59
77
|
|
|
60
78
|
const askSchema = arkType({
|
|
@@ -71,6 +89,8 @@ export interface QuestionResult {
|
|
|
71
89
|
multi: boolean;
|
|
72
90
|
selectedOptions: string[];
|
|
73
91
|
customInput?: string;
|
|
92
|
+
/** Optional note attached to the selected answer in the rich ask dialog. */
|
|
93
|
+
note?: string;
|
|
74
94
|
/** True when the answer was auto-selected because the dialog timed out. */
|
|
75
95
|
timedOut?: boolean;
|
|
76
96
|
}
|
|
@@ -81,10 +101,16 @@ export interface AskToolDetails {
|
|
|
81
101
|
multi?: boolean;
|
|
82
102
|
selectedOptions?: string[];
|
|
83
103
|
customInput?: string;
|
|
104
|
+
/** Optional note attached to the selected answer in the rich ask dialog. */
|
|
105
|
+
note?: string;
|
|
84
106
|
/** True when the answer was auto-selected because the dialog timed out. */
|
|
85
107
|
timedOut?: boolean;
|
|
86
108
|
/** Multi-part question mode */
|
|
87
109
|
results?: QuestionResult[];
|
|
110
|
+
/** Chat redirect: the user chose "Chat about this" instead of answering. */
|
|
111
|
+
chatRedirect?: boolean;
|
|
112
|
+
/** Questions surfaced when chatRedirect is true. */
|
|
113
|
+
questions?: string[];
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
interface AskOption {
|
|
@@ -108,7 +134,6 @@ function toSelectOption(option: AskOption, label = option.label): ExtensionUISel
|
|
|
108
134
|
// Constants
|
|
109
135
|
// =============================================================================
|
|
110
136
|
|
|
111
|
-
const OTHER_OPTION = "Other (type your own)";
|
|
112
137
|
const RECOMMENDED_SUFFIX = " (Recommended)";
|
|
113
138
|
// Window after the timeout deadline within which an `undefined` selection is
|
|
114
139
|
// attributed to a UI-enforced timeout (for surfaces that close the dialog at
|
|
@@ -361,6 +386,7 @@ function formatCustomInputTitle(
|
|
|
361
386
|
interface SelectionResult {
|
|
362
387
|
selectedOptions: string[];
|
|
363
388
|
customInput?: string;
|
|
389
|
+
note?: string;
|
|
364
390
|
timedOut: boolean;
|
|
365
391
|
navigation?: "back" | "forward";
|
|
366
392
|
cancelled?: boolean;
|
|
@@ -375,7 +401,7 @@ interface AskSingleQuestionOptions {
|
|
|
375
401
|
recommended?: number;
|
|
376
402
|
timeout?: number;
|
|
377
403
|
signal?: AbortSignal;
|
|
378
|
-
initialSelection?: Pick<SelectionResult, "selectedOptions" | "customInput">;
|
|
404
|
+
initialSelection?: Pick<SelectionResult, "selectedOptions" | "customInput" | "note">;
|
|
379
405
|
navigation?: NavigationControls;
|
|
380
406
|
}
|
|
381
407
|
|
|
@@ -419,6 +445,7 @@ async function askSingleQuestion(
|
|
|
419
445
|
const doneLabel = getDoneOptionLabel();
|
|
420
446
|
let selectedOptions = [...(initialSelection?.selectedOptions ?? [])];
|
|
421
447
|
let customInput = initialSelection?.customInput;
|
|
448
|
+
const note = initialSelection?.note;
|
|
422
449
|
let timedOut = false;
|
|
423
450
|
|
|
424
451
|
const selectOption = async (
|
|
@@ -547,14 +574,14 @@ async function askSingleQuestion(
|
|
|
547
574
|
});
|
|
548
575
|
|
|
549
576
|
if (arrowNavigation) {
|
|
550
|
-
return { selectedOptions: Array.from(selected), customInput, timedOut, navigation: arrowNavigation };
|
|
577
|
+
return { selectedOptions: Array.from(selected), customInput, note, timedOut, navigation: arrowNavigation };
|
|
551
578
|
}
|
|
552
579
|
if (choice === undefined) {
|
|
553
580
|
if (selectTimedOut) {
|
|
554
581
|
timedOut = true;
|
|
555
582
|
break;
|
|
556
583
|
}
|
|
557
|
-
return { selectedOptions: Array.from(selected), customInput, timedOut, cancelled: true };
|
|
584
|
+
return { selectedOptions: Array.from(selected), customInput, note, timedOut, cancelled: true };
|
|
558
585
|
}
|
|
559
586
|
if (choice === doneLabel) break;
|
|
560
587
|
|
|
@@ -621,11 +648,11 @@ async function askSingleQuestion(
|
|
|
621
648
|
timedOut = selectTimedOut;
|
|
622
649
|
|
|
623
650
|
if (arrowNavigation) {
|
|
624
|
-
return { selectedOptions, customInput, timedOut, navigation: arrowNavigation };
|
|
651
|
+
return { selectedOptions, customInput, note, timedOut, navigation: arrowNavigation };
|
|
625
652
|
}
|
|
626
653
|
if (choice === undefined) {
|
|
627
654
|
if (!timedOut) {
|
|
628
|
-
return { selectedOptions, customInput, timedOut, cancelled: true };
|
|
655
|
+
return { selectedOptions, customInput, note, timedOut, cancelled: true };
|
|
629
656
|
}
|
|
630
657
|
break;
|
|
631
658
|
}
|
|
@@ -652,7 +679,7 @@ async function askSingleQuestion(
|
|
|
652
679
|
selectedOptions = getAutoSelectionOnTimeout(questionOptions, recommended);
|
|
653
680
|
}
|
|
654
681
|
if (navigation?.allowForward) {
|
|
655
|
-
return { selectedOptions, customInput, timedOut, navigation: "forward" };
|
|
682
|
+
return { selectedOptions, customInput, note, timedOut, navigation: "forward" };
|
|
656
683
|
}
|
|
657
684
|
}
|
|
658
685
|
|
|
@@ -660,20 +687,58 @@ async function askSingleQuestion(
|
|
|
660
687
|
selectedOptions = getAutoSelectionOnTimeout(questionOptions, recommended);
|
|
661
688
|
}
|
|
662
689
|
|
|
663
|
-
return { selectedOptions, customInput, timedOut };
|
|
690
|
+
return { selectedOptions, customInput, note, timedOut };
|
|
664
691
|
}
|
|
665
692
|
|
|
666
693
|
function formatQuestionResult(result: QuestionResult): string {
|
|
694
|
+
const noteSuffix = result.note ? ` (note: ${result.note})` : "";
|
|
667
695
|
if (result.customInput !== undefined) {
|
|
668
|
-
return `${result.id}: "${result.customInput}"`;
|
|
696
|
+
return `${result.id}: "${result.customInput}"${noteSuffix}`;
|
|
669
697
|
}
|
|
670
698
|
if (result.selectedOptions.length > 0) {
|
|
671
|
-
const suffix = result.timedOut ? " (auto-selected after timeout)" : ""
|
|
699
|
+
const suffix = `${result.timedOut ? " (auto-selected after timeout)" : ""}${noteSuffix}`;
|
|
672
700
|
return result.multi
|
|
673
701
|
? `${result.id}: [${result.selectedOptions.join(", ")}]${suffix}`
|
|
674
702
|
: `${result.id}: ${result.selectedOptions[0]}${suffix}`;
|
|
675
703
|
}
|
|
676
|
-
return `${result.id}: (cancelled)`;
|
|
704
|
+
return `${result.id}: (cancelled)${noteSuffix}`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function formatSingleQuestionResponse(result: {
|
|
708
|
+
selectedOptions: string[];
|
|
709
|
+
customInput?: string;
|
|
710
|
+
note?: string;
|
|
711
|
+
timedOut?: boolean;
|
|
712
|
+
multi: boolean;
|
|
713
|
+
}): string {
|
|
714
|
+
const responseParts: string[] = [];
|
|
715
|
+
if (result.selectedOptions.length > 0) {
|
|
716
|
+
const selectedText = result.multi
|
|
717
|
+
? `User selected: ${result.selectedOptions.join(", ")}`
|
|
718
|
+
: `User selected: ${result.selectedOptions[0]}`;
|
|
719
|
+
responseParts.push(result.timedOut ? `${selectedText} (auto-selected after timeout)` : selectedText);
|
|
720
|
+
}
|
|
721
|
+
if (result.customInput !== undefined) {
|
|
722
|
+
responseParts.push(
|
|
723
|
+
result.customInput.includes("\n")
|
|
724
|
+
? `User provided custom input:\n${result.customInput
|
|
725
|
+
.split("\n")
|
|
726
|
+
.map(line => ` ${line}`)
|
|
727
|
+
.join("\n")}`
|
|
728
|
+
: `User provided custom input: ${result.customInput}`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
if (result.note) {
|
|
732
|
+
responseParts.push(
|
|
733
|
+
result.note.includes("\n")
|
|
734
|
+
? `User added note:\n${result.note
|
|
735
|
+
.split("\n")
|
|
736
|
+
.map(line => ` ${line}`)
|
|
737
|
+
.join("\n")}`
|
|
738
|
+
: `User added note: ${result.note}`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return responseParts.length > 0 ? responseParts.join("\n") : "User cancelled the selection";
|
|
677
742
|
}
|
|
678
743
|
|
|
679
744
|
// =============================================================================
|
|
@@ -810,6 +875,95 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
810
875
|
vocalizer.speak(params.questions.map(q => q.question).join("\n"));
|
|
811
876
|
}
|
|
812
877
|
|
|
878
|
+
const richAskDialog = extensionUi.askDialog;
|
|
879
|
+
if (richAskDialog) {
|
|
880
|
+
try {
|
|
881
|
+
const showRichDialog = () =>
|
|
882
|
+
richAskDialog(
|
|
883
|
+
params.questions.map(q => ({
|
|
884
|
+
id: q.id,
|
|
885
|
+
question: q.question,
|
|
886
|
+
...(q.header?.trim() ? { header: q.header } : {}),
|
|
887
|
+
options: q.options.map(option => ({
|
|
888
|
+
label: option.label,
|
|
889
|
+
...(option.description?.trim() ? { description: option.description.trim() } : {}),
|
|
890
|
+
...(option.preview?.trim() ? { preview: option.preview } : {}),
|
|
891
|
+
})),
|
|
892
|
+
...(q.multi !== undefined ? { multi: q.multi } : {}),
|
|
893
|
+
...(q.recommended !== undefined ? { recommended: q.recommended } : {}),
|
|
894
|
+
})),
|
|
895
|
+
{ timeout: timeout ?? undefined, signal },
|
|
896
|
+
);
|
|
897
|
+
const richResult = signal ? await untilAborted(signal, showRichDialog) : await showRichDialog();
|
|
898
|
+
if (!richResult) {
|
|
899
|
+
context.abort();
|
|
900
|
+
throw new ToolAbortError("Ask tool was cancelled by the user");
|
|
901
|
+
}
|
|
902
|
+
if (richResult.kind === "chat") {
|
|
903
|
+
const questionText = params.questions.map(q => q.question).join("\n");
|
|
904
|
+
return {
|
|
905
|
+
content: [
|
|
906
|
+
{
|
|
907
|
+
type: "text" as const,
|
|
908
|
+
text: `User chose to chat about this instead of answering.\n\nQuestions asked:\n${questionText}`,
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
details: { chatRedirect: true, questions: params.questions.map(q => q.question) },
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (richResult.results.length !== params.questions.length) {
|
|
915
|
+
throw new Error("Ask dialog returned a result count that does not match the requested questions");
|
|
916
|
+
}
|
|
917
|
+
const results: QuestionResult[] = [];
|
|
918
|
+
for (let index = 0; index < params.questions.length; index++) {
|
|
919
|
+
const question = params.questions[index];
|
|
920
|
+
const result = richResult.results[index];
|
|
921
|
+
if (!question || !result || result.id !== question.id) {
|
|
922
|
+
throw new Error("Ask dialog returned results that do not match the requested question order");
|
|
923
|
+
}
|
|
924
|
+
results.push({
|
|
925
|
+
id: question.id,
|
|
926
|
+
question: question.question,
|
|
927
|
+
options: question.options.map(option => option.label),
|
|
928
|
+
multi: question.multi ?? false,
|
|
929
|
+
selectedOptions: result.selectedOptions,
|
|
930
|
+
customInput: result.customInput,
|
|
931
|
+
note: result.note,
|
|
932
|
+
timedOut: result.timedOut,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
if (params.questions.length === 1) {
|
|
936
|
+
const result = results[0];
|
|
937
|
+
if (
|
|
938
|
+
!result ||
|
|
939
|
+
(!result.timedOut && result.selectedOptions.length === 0 && result.customInput === undefined)
|
|
940
|
+
) {
|
|
941
|
+
context.abort();
|
|
942
|
+
throw new ToolAbortError("Ask tool was cancelled by the user");
|
|
943
|
+
}
|
|
944
|
+
const details: AskToolDetails = {
|
|
945
|
+
question: result.question,
|
|
946
|
+
options: result.options,
|
|
947
|
+
multi: result.multi,
|
|
948
|
+
selectedOptions: result.selectedOptions,
|
|
949
|
+
customInput: result.customInput,
|
|
950
|
+
note: result.note,
|
|
951
|
+
timedOut: result.timedOut,
|
|
952
|
+
};
|
|
953
|
+
const responseText = formatSingleQuestionResponse(result);
|
|
954
|
+
return { content: [{ type: "text" as const, text: responseText }], details };
|
|
955
|
+
}
|
|
956
|
+
const details: AskToolDetails = { results };
|
|
957
|
+
const responseText = `User answers:\n${results.map(formatQuestionResult).join("\n")}`;
|
|
958
|
+
return { content: [{ type: "text" as const, text: responseText }], details };
|
|
959
|
+
} catch (error) {
|
|
960
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
961
|
+
throw new ToolAbortError("Ask input was cancelled");
|
|
962
|
+
}
|
|
963
|
+
throw error;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
813
967
|
const askQuestion = async (
|
|
814
968
|
q: AskParams["questions"][number],
|
|
815
969
|
options?: { previous?: QuestionResult; navigation?: NavigationControls },
|
|
@@ -820,7 +974,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
820
974
|
}));
|
|
821
975
|
const optionLabels = questionOptions.map(getAskOptionLabel);
|
|
822
976
|
try {
|
|
823
|
-
const { selectedOptions, customInput, navigation, cancelled, timedOut } = await askSingleQuestion(
|
|
977
|
+
const { selectedOptions, customInput, note, navigation, cancelled, timedOut } = await askSingleQuestion(
|
|
824
978
|
ui,
|
|
825
979
|
q.question,
|
|
826
980
|
questionOptions,
|
|
@@ -833,7 +987,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
833
987
|
navigation: options?.navigation,
|
|
834
988
|
},
|
|
835
989
|
);
|
|
836
|
-
return { optionLabels, selectedOptions, customInput, navigation, cancelled, timedOut };
|
|
990
|
+
return { optionLabels, selectedOptions, customInput, note, navigation, cancelled, timedOut };
|
|
837
991
|
} catch (error) {
|
|
838
992
|
if (error instanceof Error && error.name === "AbortError") {
|
|
839
993
|
throw new ToolAbortError("Ask input was cancelled");
|
|
@@ -844,7 +998,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
844
998
|
|
|
845
999
|
if (params.questions.length === 1) {
|
|
846
1000
|
const [q] = params.questions;
|
|
847
|
-
const { optionLabels, selectedOptions, customInput, cancelled, timedOut } = await askQuestion(q);
|
|
1001
|
+
const { optionLabels, selectedOptions, customInput, note, cancelled, timedOut } = await askQuestion(q);
|
|
848
1002
|
|
|
849
1003
|
if (!timedOut && (cancelled || (selectedOptions.length === 0 && customInput === undefined))) {
|
|
850
1004
|
context.abort();
|
|
@@ -856,27 +1010,17 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
856
1010
|
multi: q.multi ?? false,
|
|
857
1011
|
selectedOptions,
|
|
858
1012
|
customInput,
|
|
1013
|
+
note,
|
|
859
1014
|
timedOut: timedOut || undefined,
|
|
860
1015
|
};
|
|
861
1016
|
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
869
|
-
if (customInput !== undefined) {
|
|
870
|
-
responseParts.push(
|
|
871
|
-
customInput.includes("\n")
|
|
872
|
-
? `User provided custom input:\n${customInput
|
|
873
|
-
.split("\n")
|
|
874
|
-
.map(line => ` ${line}`)
|
|
875
|
-
.join("\n")}`
|
|
876
|
-
: `User provided custom input: ${customInput}`,
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
const responseText = responseParts.length > 0 ? responseParts.join("\n") : "User cancelled the selection";
|
|
1017
|
+
const responseText = formatSingleQuestionResponse({
|
|
1018
|
+
selectedOptions,
|
|
1019
|
+
customInput,
|
|
1020
|
+
note,
|
|
1021
|
+
timedOut: timedOut || undefined,
|
|
1022
|
+
multi: q.multi ?? false,
|
|
1023
|
+
});
|
|
880
1024
|
|
|
881
1025
|
return { content: [{ type: "text" as const, text: responseText }], details };
|
|
882
1026
|
}
|
|
@@ -884,7 +1028,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
884
1028
|
const resultsByIndex: Array<QuestionResult | undefined> = Array.from({ length: params.questions.length });
|
|
885
1029
|
let questionIndex = 0;
|
|
886
1030
|
while (questionIndex < params.questions.length) {
|
|
887
|
-
const q = params.questions[questionIndex]
|
|
1031
|
+
const q = params.questions[questionIndex];
|
|
1032
|
+
if (!q) throw new Error("Ask question index exceeded the requested question list");
|
|
888
1033
|
const previous = resultsByIndex[questionIndex];
|
|
889
1034
|
const navigation: NavigationControls = {
|
|
890
1035
|
allowBack: questionIndex > 0,
|
|
@@ -895,6 +1040,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
895
1040
|
optionLabels,
|
|
896
1041
|
selectedOptions,
|
|
897
1042
|
customInput,
|
|
1043
|
+
note,
|
|
898
1044
|
navigation: navAction,
|
|
899
1045
|
cancelled,
|
|
900
1046
|
timedOut,
|
|
@@ -912,6 +1058,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
912
1058
|
multi: q.multi ?? false,
|
|
913
1059
|
selectedOptions,
|
|
914
1060
|
customInput,
|
|
1061
|
+
note,
|
|
915
1062
|
timedOut: timedOut || undefined,
|
|
916
1063
|
};
|
|
917
1064
|
|
|
@@ -923,9 +1070,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
923
1070
|
questionIndex += 1;
|
|
924
1071
|
}
|
|
925
1072
|
|
|
926
|
-
const results =
|
|
1073
|
+
const results = params.questions.map((q, index) => {
|
|
1074
|
+
const result = resultsByIndex[index];
|
|
927
1075
|
if (result) return result;
|
|
928
|
-
const q = params.questions[index]!;
|
|
929
1076
|
return {
|
|
930
1077
|
id: q.id,
|
|
931
1078
|
question: q.question,
|
|
@@ -1024,6 +1171,21 @@ function renderCustomInputLines(uiTheme: Theme, customInput: string): string[] {
|
|
|
1024
1171
|
return out;
|
|
1025
1172
|
}
|
|
1026
1173
|
|
|
1174
|
+
/** Render an answer note with tab replacement and line-width clamping. */
|
|
1175
|
+
function renderNoteLines(uiTheme: Theme, note: string, width: number): string[] {
|
|
1176
|
+
const prefix = " Note: ";
|
|
1177
|
+
const continuationPrefix = " ";
|
|
1178
|
+
const firstLineWidth = Math.max(1, width - visibleWidth(prefix));
|
|
1179
|
+
const continuationWidth = Math.max(1, width - visibleWidth(continuationPrefix));
|
|
1180
|
+
return replaceTabs(note)
|
|
1181
|
+
.split("\n")
|
|
1182
|
+
.map((line, index) => {
|
|
1183
|
+
const linePrefix = index === 0 ? `${uiTheme.fg("dim", " Note:")} ` : continuationPrefix;
|
|
1184
|
+
const maxWidth = index === 0 ? firstLineWidth : continuationWidth;
|
|
1185
|
+
return `${linePrefix}${uiTheme.fg("toolOutput", truncateToWidth(line, maxWidth))}`;
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1027
1189
|
/**
|
|
1028
1190
|
* Marker glyph for a question option. Single-choice questions render circular radio
|
|
1029
1191
|
* buttons (pick one); multi-select questions render rectangular checkboxes (pick many).
|
|
@@ -1064,6 +1226,8 @@ function renderAnswerOptionLines(
|
|
|
1064
1226
|
selectedOptions: string[] | undefined,
|
|
1065
1227
|
multi: boolean | undefined,
|
|
1066
1228
|
customInput: string | undefined,
|
|
1229
|
+
note: string | undefined,
|
|
1230
|
+
width: number,
|
|
1067
1231
|
): string[] {
|
|
1068
1232
|
const selected = new Set(selectedOptions ?? []);
|
|
1069
1233
|
// Prefer the full recorded option set; fall back to the selected labels when
|
|
@@ -1071,7 +1235,7 @@ function renderAnswerOptionLines(
|
|
|
1071
1235
|
const list = options && options.length > 0 ? options : (selectedOptions ?? []);
|
|
1072
1236
|
|
|
1073
1237
|
// Nothing was chosen (and no custom answer) → a lone cancelled marker.
|
|
1074
|
-
if (selected.size === 0 && customInput === undefined) {
|
|
1238
|
+
if (selected.size === 0 && customInput === undefined && note === undefined) {
|
|
1075
1239
|
return [` ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`];
|
|
1076
1240
|
}
|
|
1077
1241
|
|
|
@@ -1086,6 +1250,7 @@ function renderAnswerOptionLines(
|
|
|
1086
1250
|
out.push(` ${markerStyled} ${labelStyled}`);
|
|
1087
1251
|
}
|
|
1088
1252
|
if (customInput !== undefined) out.push(...renderCustomInputLines(uiTheme, customInput));
|
|
1253
|
+
if (note !== undefined) out.push(...renderNoteLines(uiTheme, note, width));
|
|
1089
1254
|
return out;
|
|
1090
1255
|
}
|
|
1091
1256
|
|
|
@@ -1175,11 +1340,27 @@ export const askToolRenderer = {
|
|
|
1175
1340
|
return new Text(`${header}${body}`, 0, 0);
|
|
1176
1341
|
}
|
|
1177
1342
|
|
|
1343
|
+
// Chat redirect: user chose "Chat about this" instead of answering.
|
|
1344
|
+
if (details.chatRedirect) {
|
|
1345
|
+
const header = renderStatusLine({ icon: "info", title: "Ask", meta: ["chat redirect"] }, uiTheme);
|
|
1346
|
+
const questions = details.questions ?? [];
|
|
1347
|
+
return framedBlock(uiTheme, width => ({
|
|
1348
|
+
header,
|
|
1349
|
+
sections: questions.length > 0 ? [{ lines: questions.flatMap(q => md(q, width)) }] : [],
|
|
1350
|
+
state: "warning",
|
|
1351
|
+
borderColor: "borderMuted",
|
|
1352
|
+
width,
|
|
1353
|
+
}));
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1178
1356
|
// Multi-part results: one divider-labelled section per question.
|
|
1179
1357
|
if (details.results && details.results.length > 0) {
|
|
1180
1358
|
const results = details.results;
|
|
1181
1359
|
const hasAnySelection = results.some(
|
|
1182
|
-
r =>
|
|
1360
|
+
r =>
|
|
1361
|
+
r.customInput !== undefined ||
|
|
1362
|
+
r.note !== undefined ||
|
|
1363
|
+
(r.selectedOptions && r.selectedOptions.length > 0),
|
|
1183
1364
|
);
|
|
1184
1365
|
const header = renderStatusLine(
|
|
1185
1366
|
{
|
|
@@ -1194,7 +1375,16 @@ export const askToolRenderer = {
|
|
|
1194
1375
|
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
1195
1376
|
const lines = [
|
|
1196
1377
|
...md(r.question, width),
|
|
1197
|
-
...renderAnswerOptionLines(
|
|
1378
|
+
...renderAnswerOptionLines(
|
|
1379
|
+
uiTheme,
|
|
1380
|
+
mdTheme,
|
|
1381
|
+
r.options,
|
|
1382
|
+
r.selectedOptions,
|
|
1383
|
+
r.multi,
|
|
1384
|
+
r.customInput,
|
|
1385
|
+
r.note,
|
|
1386
|
+
width,
|
|
1387
|
+
),
|
|
1198
1388
|
];
|
|
1199
1389
|
return { label: uiTheme.fg("dim", `[${r.id}]`), lines };
|
|
1200
1390
|
});
|
|
@@ -1217,7 +1407,9 @@ export const askToolRenderer = {
|
|
|
1217
1407
|
|
|
1218
1408
|
const question = details.question;
|
|
1219
1409
|
const hasSelection =
|
|
1220
|
-
details.customInput !== undefined ||
|
|
1410
|
+
details.customInput !== undefined ||
|
|
1411
|
+
details.note !== undefined ||
|
|
1412
|
+
(details.selectedOptions && details.selectedOptions.length > 0);
|
|
1221
1413
|
const header = renderStatusLine(
|
|
1222
1414
|
hasSelection
|
|
1223
1415
|
? { iconOverride: uiTheme.styledSymbol("tool.ask", "accent"), title: "Ask" }
|
|
@@ -1228,12 +1420,13 @@ export const askToolRenderer = {
|
|
|
1228
1420
|
const dSelected = details.selectedOptions;
|
|
1229
1421
|
const dMulti = details.multi;
|
|
1230
1422
|
const dCustom = details.customInput;
|
|
1423
|
+
const dNote = details.note;
|
|
1231
1424
|
const dTimedOut = details.timedOut;
|
|
1232
1425
|
return framedBlock(uiTheme, width => {
|
|
1233
1426
|
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
1234
1427
|
const bodyLines = [
|
|
1235
1428
|
...md(question, width),
|
|
1236
|
-
...renderAnswerOptionLines(uiTheme, mdTheme, dOptions, dSelected, dMulti, dCustom),
|
|
1429
|
+
...renderAnswerOptionLines(uiTheme, mdTheme, dOptions, dSelected, dMulti, dCustom, dNote, width),
|
|
1237
1430
|
];
|
|
1238
1431
|
if (dTimedOut) {
|
|
1239
1432
|
// Distinguish auto-selection from a real user choice in the transcript.
|
|
@@ -319,6 +319,15 @@ export function parseConflictUri(raw: string): ParsedConflictUri | null {
|
|
|
319
319
|
return recoveredPrefix !== undefined ? { id, scope, recoveredPrefix } : { id, scope };
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
/** Result of {@link spliceConflict}: the new file text plus any boundary-echo repair applied. */
|
|
323
|
+
export interface ConflictSplice {
|
|
324
|
+
text: string;
|
|
325
|
+
/** Replacement lines dropped because they duplicated the context directly above the region. */
|
|
326
|
+
trimmedLeading: number;
|
|
327
|
+
/** Replacement lines dropped because they duplicated the context directly below the region. */
|
|
328
|
+
trimmedTrailing: number;
|
|
329
|
+
}
|
|
330
|
+
|
|
322
331
|
/**
|
|
323
332
|
* Splice the conflict region recorded in `entry` out of `originalText`
|
|
324
333
|
* and replace it with `replacement` (markers and all sides included).
|
|
@@ -328,8 +337,16 @@ export function parseConflictUri(raw: string): ParsedConflictUri | null {
|
|
|
328
337
|
* match), so out-of-band edits earlier in the file that shift line
|
|
329
338
|
* numbers don't break resolution. Throws clearly when the marker block
|
|
330
339
|
* has actually been altered or removed.
|
|
340
|
+
*
|
|
341
|
+
* Boundary-echo repair (same philosophy as the edit tool's hashline
|
|
342
|
+
* keeper repair): models frequently paste the "whole resolved function"
|
|
343
|
+
* including the lines that live directly before/after the marker block,
|
|
344
|
+
* which the verbatim splice would duplicate. Replacement lines that
|
|
345
|
+
* exactly echo the adjacent context are dropped when the echo is
|
|
346
|
+
* unambiguous — two or more consecutive lines, or a single line whose
|
|
347
|
+
* removal fixes a delimiter-balance mismatch against the recorded sides.
|
|
331
348
|
*/
|
|
332
|
-
export function spliceConflict(originalText: string, entry: ConflictEntry, replacement: string):
|
|
349
|
+
export function spliceConflict(originalText: string, entry: ConflictEntry, replacement: string): ConflictSplice {
|
|
333
350
|
const lines = originalText.split("\n");
|
|
334
351
|
const expected = buildRecordedRegion(entry);
|
|
335
352
|
const match = locateRegion(lines, expected, entry.startLine - 1);
|
|
@@ -341,6 +358,8 @@ export function spliceConflict(originalText: string, entry: ConflictEntry, repla
|
|
|
341
358
|
|
|
342
359
|
const trimmed = normalizeTrailingNewline(replacement);
|
|
343
360
|
let replacementLines = trimmed.split("\n").map(stripTrailingCr);
|
|
361
|
+
const echo = trimBoundaryEcho(replacementLines, lines, match, entry);
|
|
362
|
+
replacementLines = echo.lines;
|
|
344
363
|
// Round-trip fidelity for CRLF files: recorded sections are LF-normalized,
|
|
345
364
|
// so re-apply \r to spliced lines when the matched region used CRLF. The
|
|
346
365
|
// final replacement line only carries \r when another line follows it.
|
|
@@ -351,7 +370,79 @@ export function spliceConflict(originalText: string, entry: ConflictEntry, repla
|
|
|
351
370
|
);
|
|
352
371
|
}
|
|
353
372
|
const next = [...lines.slice(0, match.startIdx), ...replacementLines, ...lines.slice(match.endIdx + 1)];
|
|
354
|
-
return next.join("\n");
|
|
373
|
+
return { text: next.join("\n"), trimmedLeading: echo.leading, trimmedTrailing: echo.trailing };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const MAX_ECHO_LINES = 12;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Net `{}`/`()`/`[]` count over `lines`. Crude (string/comment-blind) —
|
|
380
|
+
* used only to corroborate single-line echo trims, never alone.
|
|
381
|
+
*/
|
|
382
|
+
function delimiterBalance(lines: readonly string[]): number {
|
|
383
|
+
let balance = 0;
|
|
384
|
+
for (const line of lines) {
|
|
385
|
+
for (let i = 0; i < line.length; i++) {
|
|
386
|
+
const ch = line.charCodeAt(i);
|
|
387
|
+
if (ch === 123 /* { */ || ch === 40 /* ( */ || ch === 91 /* [ */) balance++;
|
|
388
|
+
else if (ch === 125 /* } */ || ch === 41 /* ) */ || ch === 93 /* ] */) balance--;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return balance;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Drop replacement lines that exactly echo the file lines adjacent to the
|
|
396
|
+
* located region. A multi-line echo is trimmed unconditionally (a correct
|
|
397
|
+
* resolution ending with the exact lines that already follow the region
|
|
398
|
+
* would mean intentionally duplicated code — vanishingly unlikely, and the
|
|
399
|
+
* untrimmed splice produces exactly that duplication). A single-line echo
|
|
400
|
+
* is trimmed only when the recorded sides agree on the region's delimiter
|
|
401
|
+
* balance and dropping the echo is what restores it.
|
|
402
|
+
*/
|
|
403
|
+
function trimBoundaryEcho(
|
|
404
|
+
replacement: string[],
|
|
405
|
+
fileLines: readonly string[],
|
|
406
|
+
match: { startIdx: number; endIdx: number },
|
|
407
|
+
entry: ConflictBlock,
|
|
408
|
+
): { lines: string[]; leading: number; trailing: number } {
|
|
409
|
+
const oursBalance = delimiterBalance(entry.oursLines);
|
|
410
|
+
const expectedBalance = oursBalance === delimiterBalance(entry.theirsLines) ? oursBalance : null;
|
|
411
|
+
const singleEchoJustified = (lines: string[], without: string[]) =>
|
|
412
|
+
expectedBalance !== null &&
|
|
413
|
+
delimiterBalance(lines) !== expectedBalance &&
|
|
414
|
+
delimiterBalance(without) === expectedBalance;
|
|
415
|
+
|
|
416
|
+
let lines = replacement;
|
|
417
|
+
let trailing = 0;
|
|
418
|
+
const after: string[] = [];
|
|
419
|
+
for (let i = match.endIdx + 1; i < fileLines.length && after.length < MAX_ECHO_LINES; i++) {
|
|
420
|
+
after.push(stripTrailingCr(fileLines[i]!));
|
|
421
|
+
}
|
|
422
|
+
for (let k = Math.min(after.length, lines.length - 1); k >= 1; k--) {
|
|
423
|
+
if (!after.slice(0, k).every((line, i) => lines[lines.length - k + i] === line)) continue;
|
|
424
|
+
if (k >= 2 || singleEchoJustified(lines, lines.slice(0, -1))) {
|
|
425
|
+
trailing = k;
|
|
426
|
+
lines = lines.slice(0, lines.length - k);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let leading = 0;
|
|
432
|
+
const before: string[] = [];
|
|
433
|
+
for (let i = match.startIdx - 1; i >= 0 && before.length < MAX_ECHO_LINES; i--) {
|
|
434
|
+
before.unshift(stripTrailingCr(fileLines[i]!));
|
|
435
|
+
}
|
|
436
|
+
for (let k = Math.min(before.length, lines.length - 1); k >= 1; k--) {
|
|
437
|
+
if (!before.slice(before.length - k).every((line, i) => lines[i] === line)) continue;
|
|
438
|
+
if (k >= 2 || singleEchoJustified(lines, lines.slice(1))) {
|
|
439
|
+
leading = k;
|
|
440
|
+
lines = lines.slice(k);
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return { lines, leading, trailing };
|
|
355
446
|
}
|
|
356
447
|
|
|
357
448
|
/** Reconstruct the recorded marker block as it should appear in the file. */
|
|
@@ -608,10 +699,16 @@ export function formatConflictWarning(
|
|
|
608
699
|
if (theirsLabel) out.push(`- theirs = ${theirsLabel}`);
|
|
609
700
|
if (anyBase) out.push(`- base = ${baseLabel ?? "(no label)"}`);
|
|
610
701
|
out.push(
|
|
611
|
-
'NOTICE: Inspect a block by reading `conflict://<N>` (add `/ours` / `/theirs` / `/base` to render a single side). Resolve with `write({ path: "conflict://<N>", content })`, or bulk-resolve every registered conflict with `write({ path: "conflict://*", content })`. Writes replace the
|
|
702
|
+
'NOTICE: Inspect a block by reading `conflict://<N>` (add `/ours` / `/theirs` / `/base` to render a single side). Resolve with `write({ path: "conflict://<N>", content })`, or bulk-resolve every registered conflict with `write({ path: "conflict://*", content })`. Writes replace ONLY the marker block (markers + all sides) — never repeat the lines before/after it; they stay in place.',
|
|
703
|
+
);
|
|
704
|
+
out.push(
|
|
705
|
+
'`content` shorthand: a line that is exactly `@ours` / `@theirs` / `@base` / `@both` expands to that recorded section. `@both` is ours-then-theirs with no separator — only for additive conflicts where each side adds something different; NEVER for competing edits of the same lines (pick a side or write the combined text). Lines that are not a token pass through verbatim, so `"// keep both\\n@ours\\n@theirs"` literally writes the comment, then ours, then theirs.',
|
|
706
|
+
);
|
|
707
|
+
out.push(
|
|
708
|
+
'Per-id bulk: `write({ path: "conflict://*", content: "1: @ours\\n2: @theirs\\n…" })` resolves each listed id with that side in ONE call — the cheapest way through many pick-one conflicts; unlisted ids stay registered.',
|
|
612
709
|
);
|
|
613
710
|
out.push(
|
|
614
|
-
|
|
711
|
+
"Resolve each block faithfully: keep one side (`@ours`/`@theirs`), or combine them when both intents apply — never invent content beyond the recorded sides, and never stack both sides of competing edits. Resolve several conflicts in a single turn by issuing multiple `write` calls at once; ids stay valid as earlier blocks are resolved.",
|
|
615
712
|
);
|
|
616
713
|
|
|
617
714
|
for (const entry of entries) {
|
|
@@ -674,7 +771,7 @@ export function formatConflictSummary(
|
|
|
674
771
|
'NOTICE: Bulk-resolve with `write({ path: "conflict://*", content })`, or address a single block with `write({ path: "conflict://<N>", content })`. Inspect a block by reading `conflict://<N>` (add `/ours` / `/theirs` / `/base` for a single side).',
|
|
675
772
|
);
|
|
676
773
|
lines.push(
|
|
677
|
-
|
|
774
|
+
'`content` shorthand: `@ours` / `@theirs` / `@base` / `@both` lines expand to the recorded sections; `@both` = ours-then-theirs (additive conflicts only — never for competing edits of the same lines). Per-id bulk: content of `<id>: @side` lines (e.g. "1: @ours\\n2: @theirs") resolves each listed id in one call. Non-token lines pass through verbatim. Writes replace ONLY the marker block — never repeat the surrounding lines. Keep one side or combine faithfully; never invent content beyond the recorded sides.',
|
|
678
775
|
);
|
|
679
776
|
lines.push("");
|
|
680
777
|
const idWidth = String(entries[entries.length - 1]?.id ?? 1).length;
|
package/src/tools/index.ts
CHANGED
|
@@ -617,6 +617,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
617
617
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
618
618
|
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
|
|
619
619
|
if (name === "search_tool_bm25") return discoveryActive;
|
|
620
|
+
if (name === "ask") return session.settings.get("ask.enabled");
|
|
620
621
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
621
622
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
622
623
|
if (name === "irc") return isIrcEnabled(session.settings, session.taskDepth ?? 0);
|