@oh-my-pi/pi-coding-agent 15.9.3 → 15.9.5
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 +39 -1
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +13 -4
- package/dist/types/modes/components/assistant-message.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/config/model-registry.ts +54 -4
- package/src/config/settings-schema.ts +14 -4
- package/src/eval/__tests__/agent-bridge.test.ts +72 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/main.ts +7 -1
- package/src/modes/components/assistant-message.ts +22 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/tool-execution.ts +44 -0
- package/src/modes/components/transcript-container.ts +93 -32
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/event-controller.ts +42 -3
- package/src/modes/controllers/input-controller.ts +33 -1
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +48 -13
- package/src/modes/types.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/sdk.ts +17 -9
- package/src/session/agent-session.ts +37 -12
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/system-prompt.ts +4 -0
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/fetch.ts +29 -9
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +6 -8
- package/src/tools/read.ts +58 -12
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +60 -11
- package/src/tui/hyperlink.ts +42 -7
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +20 -52
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { type BlobPutResult, blobExtensionForImageMimeType } from "../session/blob-store";
|
|
4
|
+
import { fileHyperlink } from "../tui/hyperlink";
|
|
5
|
+
|
|
6
|
+
const IMAGE_REFERENCE_REGEX = /\[Image #([1-9]\d*)\]/g;
|
|
7
|
+
|
|
8
|
+
type ImageBlobWriter = (data: Buffer, options?: { extension?: string }) => Promise<BlobPutResult>;
|
|
9
|
+
type ImageBlobWriterSync = (data: Buffer, options?: { extension?: string }) => BlobPutResult;
|
|
10
|
+
|
|
11
|
+
export interface ImageReferenceRenderers {
|
|
12
|
+
renderText: (text: string) => string;
|
|
13
|
+
renderReference: (label: string, index: number) => string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderImageReferences(text: string, renderers: ImageReferenceRenderers): string {
|
|
17
|
+
IMAGE_REFERENCE_REGEX.lastIndex = 0;
|
|
18
|
+
let result = "";
|
|
19
|
+
let last = 0;
|
|
20
|
+
let matched = false;
|
|
21
|
+
|
|
22
|
+
for (;;) {
|
|
23
|
+
const match = IMAGE_REFERENCE_REGEX.exec(text);
|
|
24
|
+
if (match === null) break;
|
|
25
|
+
matched = true;
|
|
26
|
+
if (match.index > last) {
|
|
27
|
+
result += renderers.renderText(text.slice(last, match.index));
|
|
28
|
+
}
|
|
29
|
+
result += renderers.renderReference(match[0], Number(match[1]));
|
|
30
|
+
last = match.index + match[0].length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!matched) {
|
|
34
|
+
return renderers.renderText(text);
|
|
35
|
+
}
|
|
36
|
+
if (last < text.length) {
|
|
37
|
+
result += renderers.renderText(text.slice(last));
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function imageReferenceHyperlink(
|
|
43
|
+
label: string,
|
|
44
|
+
index: number,
|
|
45
|
+
imageLinks: readonly (string | undefined)[] | undefined,
|
|
46
|
+
renderLabel: (text: string) => string,
|
|
47
|
+
): string {
|
|
48
|
+
const rendered = renderLabel(label);
|
|
49
|
+
const target = imageLinks?.[index - 1];
|
|
50
|
+
return target ? fileHyperlink(target, rendered) : rendered;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function materializeImageReferenceLinkAsync(
|
|
54
|
+
image: ImageContent,
|
|
55
|
+
index: number,
|
|
56
|
+
putBlob: ImageBlobWriter,
|
|
57
|
+
): Promise<string | undefined> {
|
|
58
|
+
try {
|
|
59
|
+
const result = await putBlob(Buffer.from(image.data, "base64"), {
|
|
60
|
+
extension: blobExtensionForImageMimeType(image.mimeType),
|
|
61
|
+
});
|
|
62
|
+
return result.displayPath;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.warn("Failed to write image reference blob", {
|
|
65
|
+
index,
|
|
66
|
+
mimeType: image.mimeType,
|
|
67
|
+
error: error instanceof Error ? error.message : String(error),
|
|
68
|
+
});
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function materializeImageReferenceLink(
|
|
74
|
+
image: ImageContent,
|
|
75
|
+
index: number,
|
|
76
|
+
putBlob: ImageBlobWriterSync,
|
|
77
|
+
): string | undefined {
|
|
78
|
+
try {
|
|
79
|
+
const result = putBlob(Buffer.from(image.data, "base64"), {
|
|
80
|
+
extension: blobExtensionForImageMimeType(image.mimeType),
|
|
81
|
+
});
|
|
82
|
+
return result.displayPath;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
logger.warn("Failed to write image reference blob", {
|
|
85
|
+
index,
|
|
86
|
+
mimeType: image.mimeType,
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
});
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function materializeImageReferenceLinks(
|
|
94
|
+
images: readonly ImageContent[] | undefined,
|
|
95
|
+
putBlob: ImageBlobWriter,
|
|
96
|
+
): Promise<(string | undefined)[] | undefined> {
|
|
97
|
+
if (!images || images.length === 0) return undefined;
|
|
98
|
+
const links = await Promise.all(
|
|
99
|
+
images.map((image, index) => materializeImageReferenceLinkAsync(image, index + 1, putBlob)),
|
|
100
|
+
);
|
|
101
|
+
return links.some(link => link !== undefined) ? links : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function materializeImageReferenceLinksSync(
|
|
105
|
+
images: readonly ImageContent[] | undefined,
|
|
106
|
+
putBlob: ImageBlobWriterSync,
|
|
107
|
+
): (string | undefined)[] | undefined {
|
|
108
|
+
if (!images || images.length === 0) return undefined;
|
|
109
|
+
const links = images.map((image, index) => materializeImageReferenceLink(image, index + 1, putBlob));
|
|
110
|
+
return links.some(link => link !== undefined) ? links : undefined;
|
|
111
|
+
}
|
|
@@ -96,6 +96,7 @@ import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
|
96
96
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
97
97
|
import { CustomEditor } from "./components/custom-editor";
|
|
98
98
|
import { DynamicBorder } from "./components/dynamic-border";
|
|
99
|
+
import { ErrorBannerComponent } from "./components/error-banner";
|
|
99
100
|
import type { EvalExecutionComponent } from "./components/eval-execution";
|
|
100
101
|
import type { HookEditorComponent } from "./components/hook-editor";
|
|
101
102
|
import type { HookInputComponent } from "./components/hook-input";
|
|
@@ -264,6 +265,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
264
265
|
todoContainer: Container;
|
|
265
266
|
btwContainer: Container;
|
|
266
267
|
omfgContainer: Container;
|
|
268
|
+
errorBannerContainer: Container;
|
|
267
269
|
editor: CustomEditor;
|
|
268
270
|
editorContainer: Container;
|
|
269
271
|
hookWidgetContainerAbove: Container;
|
|
@@ -288,6 +290,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
288
290
|
todoPhases: TodoPhase[] = [];
|
|
289
291
|
hideThinkingBlock = false;
|
|
290
292
|
pendingImages: ImageContent[] = [];
|
|
293
|
+
pendingImageLinks: (string | undefined)[] = [];
|
|
291
294
|
compactionQueuedMessages: CompactionQueuedMessage[] = [];
|
|
292
295
|
pendingTools = new Map<string, ToolExecutionHandle>();
|
|
293
296
|
pendingBashComponents: BashExecutionComponent[] = [];
|
|
@@ -404,6 +407,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
404
407
|
this.todoContainer = new Container();
|
|
405
408
|
this.btwContainer = new Container();
|
|
406
409
|
this.omfgContainer = new Container();
|
|
410
|
+
this.errorBannerContainer = new Container();
|
|
407
411
|
this.editor = new CustomEditor(getEditorTheme());
|
|
408
412
|
this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
|
409
413
|
this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
|
|
@@ -565,6 +569,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
565
569
|
this.ui.addChild(this.todoContainer);
|
|
566
570
|
this.ui.addChild(this.btwContainer);
|
|
567
571
|
this.ui.addChild(this.omfgContainer);
|
|
572
|
+
this.ui.addChild(this.errorBannerContainer);
|
|
568
573
|
this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
|
|
569
574
|
this.ui.addChild(this.hookWidgetContainerAbove);
|
|
570
575
|
this.ui.addChild(this.editorContainer);
|
|
@@ -896,12 +901,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
896
901
|
startPendingSubmission(input: {
|
|
897
902
|
text: string;
|
|
898
903
|
images?: ImageContent[];
|
|
904
|
+
imageLinks?: (string | undefined)[];
|
|
899
905
|
customType?: string;
|
|
900
906
|
display?: boolean;
|
|
901
907
|
}): SubmittedUserInput {
|
|
902
908
|
const submission: SubmittedUserInput = {
|
|
903
909
|
text: input.text,
|
|
904
910
|
images: input.images,
|
|
911
|
+
imageLinks: input.imageLinks,
|
|
905
912
|
customType: input.customType,
|
|
906
913
|
display: input.display,
|
|
907
914
|
cancelled: false,
|
|
@@ -913,22 +920,28 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
913
920
|
const imageCount = submission.images?.length ?? 0;
|
|
914
921
|
this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
|
|
915
922
|
this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
|
|
916
|
-
this.addMessageToChat(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
923
|
+
this.addMessageToChat(
|
|
924
|
+
{
|
|
925
|
+
role: "user",
|
|
926
|
+
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
927
|
+
attribution: "user",
|
|
928
|
+
timestamp: Date.now(),
|
|
929
|
+
},
|
|
930
|
+
{ imageLinks: input.imageLinks },
|
|
931
|
+
);
|
|
922
932
|
} else {
|
|
923
933
|
this.optimisticUserMessageSignature = undefined;
|
|
924
934
|
this.#pendingSubmissionDispose = undefined;
|
|
925
935
|
}
|
|
926
936
|
this.editor.setText("");
|
|
927
|
-
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
937
|
+
this.editor.imageLinks = undefined;
|
|
938
|
+
// Reconciliation checkpoint: only retire frozen block snapshots after TUI
|
|
939
|
+
// proves the native viewport is at the tail and replays scrollback safely.
|
|
940
|
+
// Unknown host viewports stay frozen; thawing them would expose live rows
|
|
941
|
+
// over stale native history and can yank or duplicate when ED3 is unsafe.
|
|
942
|
+
if (this.ui.refreshNativeScrollbackIfDirty()) {
|
|
943
|
+
this.chatContainer.thaw();
|
|
944
|
+
}
|
|
932
945
|
this.ensureLoadingAnimation();
|
|
933
946
|
this.ui.requestRender();
|
|
934
947
|
return submission;
|
|
@@ -956,6 +969,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
956
969
|
}
|
|
957
970
|
if (!submission.customType) {
|
|
958
971
|
this.pendingImages = submission.images ? [...submission.images] : [];
|
|
972
|
+
this.pendingImageLinks = submission.imageLinks ? [...submission.imageLinks] : [];
|
|
973
|
+
this.editor.imageLinks = this.pendingImageLinks;
|
|
959
974
|
this.rebuildChatFromMessages();
|
|
960
975
|
this.editor.setText(submission.text);
|
|
961
976
|
}
|
|
@@ -2492,6 +2507,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2492
2507
|
this.#uiHelpers.showError(message);
|
|
2493
2508
|
}
|
|
2494
2509
|
|
|
2510
|
+
showPinnedError(message: string): void {
|
|
2511
|
+
if (this.isBackgrounded) return;
|
|
2512
|
+
this.errorBannerContainer.clear();
|
|
2513
|
+
this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
|
|
2514
|
+
this.ui.requestRender();
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
clearPinnedError(): void {
|
|
2518
|
+
if (this.errorBannerContainer.children.length === 0) return;
|
|
2519
|
+
this.errorBannerContainer.clear();
|
|
2520
|
+
this.ui.requestRender();
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2495
2523
|
showWarning(message: string): void {
|
|
2496
2524
|
this.#uiHelpers.showWarning(message);
|
|
2497
2525
|
}
|
|
@@ -2622,7 +2650,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2622
2650
|
return this.#uiHelpers.isKnownSlashCommand(text);
|
|
2623
2651
|
}
|
|
2624
2652
|
|
|
2625
|
-
addMessageToChat(
|
|
2653
|
+
addMessageToChat(
|
|
2654
|
+
message: AgentMessage,
|
|
2655
|
+
options?: { populateHistory?: boolean; imageLinks?: readonly (string | undefined)[] },
|
|
2656
|
+
): Component[] {
|
|
2626
2657
|
return this.#uiHelpers.addMessageToChat(message, options);
|
|
2627
2658
|
}
|
|
2628
2659
|
|
|
@@ -2633,7 +2664,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2633
2664
|
this.#uiHelpers.renderSessionContext(sessionContext, options);
|
|
2634
2665
|
}
|
|
2635
2666
|
|
|
2636
|
-
renderInitialMessages(
|
|
2667
|
+
renderInitialMessages(
|
|
2668
|
+
prebuiltContext?: SessionContext,
|
|
2669
|
+
options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean },
|
|
2670
|
+
): void {
|
|
2637
2671
|
this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
|
|
2638
2672
|
}
|
|
2639
2673
|
|
|
@@ -2706,6 +2740,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2706
2740
|
this.#btwController.dispose();
|
|
2707
2741
|
this.#omfgController.dispose();
|
|
2708
2742
|
this.#extensionUiController.clearExtensionTerminalInputListeners();
|
|
2743
|
+
this.clearPinnedError();
|
|
2709
2744
|
this.#planReviewContainer = undefined;
|
|
2710
2745
|
}
|
|
2711
2746
|
|
package/src/modes/types.ts
CHANGED
|
@@ -40,6 +40,7 @@ export type CompactionQueuedMessage = {
|
|
|
40
40
|
export type SubmittedUserInput = {
|
|
41
41
|
text: string;
|
|
42
42
|
images?: ImageContent[];
|
|
43
|
+
imageLinks?: (string | undefined)[];
|
|
43
44
|
customType?: string;
|
|
44
45
|
display?: boolean;
|
|
45
46
|
cancelled: boolean;
|
|
@@ -75,6 +76,7 @@ export interface InteractiveModeContext {
|
|
|
75
76
|
todoContainer: Container;
|
|
76
77
|
btwContainer: Container;
|
|
77
78
|
omfgContainer: Container;
|
|
79
|
+
errorBannerContainer: Container;
|
|
78
80
|
editor: CustomEditor;
|
|
79
81
|
editorContainer: Container;
|
|
80
82
|
hookWidgetContainerAbove: Container;
|
|
@@ -106,6 +108,7 @@ export interface InteractiveModeContext {
|
|
|
106
108
|
planModePlanFilePath?: string;
|
|
107
109
|
hideThinkingBlock: boolean;
|
|
108
110
|
pendingImages: ImageContent[];
|
|
111
|
+
pendingImageLinks: (string | undefined)[];
|
|
109
112
|
compactionQueuedMessages: CompactionQueuedMessage[];
|
|
110
113
|
pendingTools: Map<string, ToolExecutionHandle>;
|
|
111
114
|
pendingBashComponents: BashExecutionComponent[];
|
|
@@ -157,6 +160,8 @@ export interface InteractiveModeContext {
|
|
|
157
160
|
// UI helpers
|
|
158
161
|
showStatus(message: string, options?: { dim?: boolean }): void;
|
|
159
162
|
showError(message: string): void;
|
|
163
|
+
showPinnedError(message: string): void;
|
|
164
|
+
clearPinnedError(): void;
|
|
160
165
|
showWarning(message: string): void;
|
|
161
166
|
showNewVersionNotification(newVersion: string): void;
|
|
162
167
|
clearEditor(): void;
|
|
@@ -171,6 +176,7 @@ export interface InteractiveModeContext {
|
|
|
171
176
|
startPendingSubmission(input: {
|
|
172
177
|
text: string;
|
|
173
178
|
images?: ImageContent[];
|
|
179
|
+
imageLinks?: (string | undefined)[];
|
|
174
180
|
customType?: string;
|
|
175
181
|
display?: boolean;
|
|
176
182
|
}): SubmittedUserInput;
|
|
@@ -191,7 +197,10 @@ export interface InteractiveModeContext {
|
|
|
191
197
|
*/
|
|
192
198
|
withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T>;
|
|
193
199
|
isKnownSlashCommand(text: string): boolean;
|
|
194
|
-
addMessageToChat(
|
|
200
|
+
addMessageToChat(
|
|
201
|
+
message: AgentMessage,
|
|
202
|
+
options?: { populateHistory?: boolean; imageLinks?: readonly (string | undefined)[] },
|
|
203
|
+
): Component[];
|
|
195
204
|
renderSessionContext(
|
|
196
205
|
sessionContext: SessionContext,
|
|
197
206
|
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { SkillMessageComponent } from "../../modes/components/skill-message";
|
|
19
19
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
20
20
|
import { UserMessageComponent } from "../../modes/components/user-message";
|
|
21
|
+
import { materializeImageReferenceLinksSync } from "../../modes/image-references";
|
|
21
22
|
import { theme } from "../../modes/theme/theme";
|
|
22
23
|
import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
|
|
23
24
|
import {
|
|
@@ -40,6 +41,18 @@ type QueuedMessages = {
|
|
|
40
41
|
followUp: string[];
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
function imageLinksForMessage(
|
|
45
|
+
message: Extract<AgentMessage, { role: "developer" | "user" }>,
|
|
46
|
+
putBlobSync: InteractiveModeContext["sessionManager"]["putBlobSync"],
|
|
47
|
+
): (string | undefined)[] | undefined {
|
|
48
|
+
if (typeof message.content === "string") return undefined;
|
|
49
|
+
const images = message.content.filter(
|
|
50
|
+
(content): content is ImageContent =>
|
|
51
|
+
content.type === "image" && typeof content.data === "string" && typeof content.mimeType === "string",
|
|
52
|
+
);
|
|
53
|
+
return materializeImageReferenceLinksSync(images, putBlobSync);
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
export class UiHelpers {
|
|
44
57
|
constructor(private ctx: InteractiveModeContext) {}
|
|
45
58
|
|
|
@@ -84,7 +97,10 @@ export class UiHelpers {
|
|
|
84
97
|
this.ctx.ui.requestRender();
|
|
85
98
|
}
|
|
86
99
|
|
|
87
|
-
addMessageToChat(
|
|
100
|
+
addMessageToChat(
|
|
101
|
+
message: AgentMessage,
|
|
102
|
+
options?: { populateHistory?: boolean; imageLinks?: readonly (string | undefined)[] },
|
|
103
|
+
): Component[] {
|
|
88
104
|
switch (message.role) {
|
|
89
105
|
case "bashExecution": {
|
|
90
106
|
const component = new BashExecutionComponent(message.command, this.ctx.ui, message.excludeFromContext);
|
|
@@ -253,7 +269,10 @@ export class UiHelpers {
|
|
|
253
269
|
const textContent = this.ctx.getUserMessageText(message);
|
|
254
270
|
if (textContent) {
|
|
255
271
|
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
256
|
-
const
|
|
272
|
+
const imageLinks =
|
|
273
|
+
options?.imageLinks ??
|
|
274
|
+
imageLinksForMessage(message, this.ctx.sessionManager.putBlobSync.bind(this.ctx.sessionManager));
|
|
275
|
+
const userComponent = new UserMessageComponent(textContent, isSynthetic, imageLinks);
|
|
257
276
|
this.ctx.chatContainer.addChild(userComponent);
|
|
258
277
|
if (options?.populateHistory && message.role === "user" && !isSynthetic) {
|
|
259
278
|
this.ctx.editor.addToHistory(textContent);
|
|
@@ -513,6 +532,8 @@ export class UiHelpers {
|
|
|
513
532
|
}
|
|
514
533
|
this.ctx.editor.setText("");
|
|
515
534
|
this.ctx.pendingImages = [];
|
|
535
|
+
this.ctx.pendingImageLinks = [];
|
|
536
|
+
this.ctx.editor.imageLinks = undefined;
|
|
516
537
|
this.ctx.ui.requestRender();
|
|
517
538
|
}
|
|
518
539
|
|
|
@@ -14,7 +14,7 @@ Do not stop after a single fix attempt.
|
|
|
14
14
|
2. If any run fails, inspect failing job output and logs.
|
|
15
15
|
3. Identify root cause and make minimal correct fix.
|
|
16
16
|
4. Run local verification if it reduces chance of another failing push.
|
|
17
|
-
5. Push the branch.
|
|
17
|
+
{{#if headTag}}5. Push the branch and tag `{{headTag}}` atomically: `git push --atomic "{{remote}}" "{{branch}}" "+refs/tags/{{headTag}}"`.{{else}}5. Push the branch.{{/if}}
|
|
18
18
|
6. Watch workflow runs for new HEAD commit again.
|
|
19
19
|
7. Repeat until workflow runs for latest HEAD commit succeed.
|
|
20
20
|
</procedure>
|
|
@@ -26,11 +26,13 @@ Do not stop after a single fix attempt.
|
|
|
26
26
|
|
|
27
27
|
{{#if headTag}}
|
|
28
28
|
<instruction>
|
|
29
|
-
|
|
29
|
+
Always push the branch and tag together atomically so the tag never points at an un-pushed or non-green commit:
|
|
30
|
+
`git push --atomic "{{remote}}" "{{branch}}" "+refs/tags/{{headTag}}"`.
|
|
31
|
+
The `--atomic` flag makes the branch and tag update succeed or fail as one ref transaction; `+refs/tags/{{headTag}}` force-moves the tag to the new HEAD. Do not push the branch first and retag later.
|
|
30
32
|
</instruction>
|
|
31
33
|
{{/if}}
|
|
32
34
|
|
|
33
35
|
<critical>
|
|
34
36
|
The task is complete only when the workflow runs for the latest HEAD commit succeed.
|
|
35
|
-
{{#if headTag}}The
|
|
37
|
+
{{#if headTag}}The latest HEAD commit must carry tag `{{headTag}}`, pushed atomically with the branch via `git push --atomic`.{{/if}}
|
|
36
38
|
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -130,6 +130,7 @@ import {
|
|
|
130
130
|
resolveThinkingLevelForModel,
|
|
131
131
|
toReasoningEffort,
|
|
132
132
|
} from "./thinking";
|
|
133
|
+
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "./tool-discovery/mode";
|
|
133
134
|
import {
|
|
134
135
|
collectDiscoverableTools,
|
|
135
136
|
type DiscoverableTool,
|
|
@@ -157,6 +158,7 @@ import {
|
|
|
157
158
|
ResolveTool,
|
|
158
159
|
renderSearchToolBm25Description,
|
|
159
160
|
SearchTool,
|
|
161
|
+
SearchToolBm25Tool,
|
|
160
162
|
setPreferredImageProvider,
|
|
161
163
|
setPreferredSearchProvider,
|
|
162
164
|
type Tool,
|
|
@@ -1687,6 +1689,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1687
1689
|
}
|
|
1688
1690
|
}
|
|
1689
1691
|
|
|
1692
|
+
const effectiveDiscoveryMode = resolveEffectiveToolDiscoveryMode(
|
|
1693
|
+
settings,
|
|
1694
|
+
countToolsForAutoDiscovery(toolRegistry.keys()),
|
|
1695
|
+
);
|
|
1696
|
+
if (effectiveDiscoveryMode !== "off" && !toolRegistry.has("search_tool_bm25")) {
|
|
1697
|
+
const searchTool: Tool = new SearchToolBm25Tool(toolSession);
|
|
1698
|
+
toolRegistry.set(
|
|
1699
|
+
searchTool.name,
|
|
1700
|
+
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1704
|
+
|
|
1690
1705
|
const reloadSshTool = async (): Promise<AgentTool | null> => {
|
|
1691
1706
|
if (!requestedToolNameSet.has("ssh")) return null;
|
|
1692
1707
|
const sshTool = (await loadSshTool({
|
|
@@ -1773,6 +1788,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1773
1788
|
secretsEnabled,
|
|
1774
1789
|
workspaceTree: workspaceTreePromise,
|
|
1775
1790
|
memoryRootEnabled: memoryBackend.id === "local",
|
|
1791
|
+
model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined,
|
|
1776
1792
|
});
|
|
1777
1793
|
|
|
1778
1794
|
if (options.systemPrompt === undefined) {
|
|
@@ -1805,15 +1821,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1805
1821
|
const requestedToolNames = explicitlyRequestedToolNames ?? toolNamesFromRegistry;
|
|
1806
1822
|
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
1807
1823
|
const requestedToolNameSet = new Set(normalizedRequested);
|
|
1808
|
-
// Effective discovery mode
|
|
1809
|
-
const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
|
|
1810
|
-
const effectiveDiscoveryMode: "off" | "mcp-only" | "all" =
|
|
1811
|
-
toolsDiscoveryModeSetting !== "off"
|
|
1812
|
-
? (toolsDiscoveryModeSetting as "off" | "mcp-only" | "all")
|
|
1813
|
-
: settings.get("mcp.discoveryMode")
|
|
1814
|
-
? "mcp-only"
|
|
1815
|
-
: "off";
|
|
1816
|
-
const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1824
|
+
// Effective discovery mode is resolved after the full registry exists so auto mode can count MCP/extension tools.
|
|
1817
1825
|
const defaultInactiveToolNames = new Set(
|
|
1818
1826
|
registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
|
|
1819
1827
|
);
|
|
@@ -192,6 +192,7 @@ import {
|
|
|
192
192
|
toReasoningEffort,
|
|
193
193
|
} from "../thinking";
|
|
194
194
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
195
|
+
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
195
196
|
import {
|
|
196
197
|
buildDiscoverableToolSearchIndex,
|
|
197
198
|
collectDiscoverableTools,
|
|
@@ -960,6 +961,13 @@ export class AgentSession {
|
|
|
960
961
|
* the dominant cause of prompt-cache invalidation in long sessions.
|
|
961
962
|
*/
|
|
962
963
|
#lastAppliedToolSignature: string | undefined;
|
|
964
|
+
/**
|
|
965
|
+
* Model identifier (`provider/id`) currently rendered into `#baseSystemPrompt`.
|
|
966
|
+
* The prompt surfaces the active model to the agent, so a model switch must
|
|
967
|
+
* trigger a rebuild. Compared against the live model after every model change
|
|
968
|
+
* to decide whether the cached prompt is stale.
|
|
969
|
+
*/
|
|
970
|
+
#promptModelKey: string | undefined;
|
|
963
971
|
#mcpDiscoveryEnabled = false;
|
|
964
972
|
#discoverableMCPTools = new Map<string, DiscoverableTool>();
|
|
965
973
|
#selectedMCPToolNames = new Set<string>();
|
|
@@ -1173,6 +1181,7 @@ export class AgentSession {
|
|
|
1173
1181
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
1174
1182
|
this.#reloadSshTool = config.reloadSshTool;
|
|
1175
1183
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
1184
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
1176
1185
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
1177
1186
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
1178
1187
|
this.#selectedMCPToolNames = new Set(config.initialSelectedMCPToolNames ?? []);
|
|
@@ -3264,9 +3273,21 @@ export class AgentSession {
|
|
|
3264
3273
|
return resolveEditMode(this.#getEditModeSession());
|
|
3265
3274
|
}
|
|
3266
3275
|
|
|
3267
|
-
|
|
3276
|
+
/**
|
|
3277
|
+
* Model key (`provider/id`) currently surfaced in the system prompt, or
|
|
3278
|
+
* undefined when the model is unset or `includeModelInPrompt` is disabled.
|
|
3279
|
+
*/
|
|
3280
|
+
#currentPromptModelKey(): string | undefined {
|
|
3281
|
+
if (!this.settings.get("includeModelInPrompt")) return undefined;
|
|
3282
|
+
return this.model ? formatModelString(this.model) : undefined;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
async #syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
3268
3286
|
const currentEditMode = this.#resolveActiveEditMode();
|
|
3269
|
-
|
|
3287
|
+
const editModeChanged = previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit");
|
|
3288
|
+
// The system prompt may surface the active model; a switch makes the cached prompt stale.
|
|
3289
|
+
const modelChanged = this.#currentPromptModelKey() !== this.#promptModelKey;
|
|
3290
|
+
if (editModeChanged || modelChanged) {
|
|
3270
3291
|
await this.refreshBaseSystemPrompt();
|
|
3271
3292
|
}
|
|
3272
3293
|
}
|
|
@@ -3305,12 +3326,14 @@ export class AgentSession {
|
|
|
3305
3326
|
|
|
3306
3327
|
// ── Generic tool discovery (covers built-in + MCP + extension) ────────────
|
|
3307
3328
|
|
|
3308
|
-
/** Resolve effective discovery mode
|
|
3329
|
+
/** Resolve effective discovery mode from the current registry size. */
|
|
3309
3330
|
#resolveEffectiveDiscoveryMode(): "off" | "mcp-only" | "all" {
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3331
|
+
const mode = resolveEffectiveToolDiscoveryMode(
|
|
3332
|
+
this.settings,
|
|
3333
|
+
countToolsForAutoDiscovery(this.#toolRegistry.keys()),
|
|
3334
|
+
);
|
|
3335
|
+
if (mode !== "off") return mode;
|
|
3336
|
+
return this.#mcpDiscoveryEnabled ? "mcp-only" : "off";
|
|
3314
3337
|
}
|
|
3315
3338
|
|
|
3316
3339
|
isToolDiscoveryEnabled(): boolean {
|
|
@@ -3551,6 +3574,7 @@ export class AgentSession {
|
|
|
3551
3574
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
3552
3575
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
3553
3576
|
this.#lastAppliedToolSignature = signature;
|
|
3577
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
3554
3578
|
}
|
|
3555
3579
|
}
|
|
3556
3580
|
if (options?.persistMCPSelection !== false) {
|
|
@@ -3633,6 +3657,7 @@ export class AgentSession {
|
|
|
3633
3657
|
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
3634
3658
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
3635
3659
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
3660
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
3636
3661
|
// Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
|
|
3637
3662
|
// the same tool set does not re-rebuild on top of the explicit refresh we
|
|
3638
3663
|
// just performed (and conversely, a different set forces a fresh rebuild).
|
|
@@ -3692,7 +3717,7 @@ export class AgentSession {
|
|
|
3692
3717
|
* closure-captured ones cannot change at runtime regardless of skip behavior.
|
|
3693
3718
|
* For everything else, callers must explicitly call `refreshBaseSystemPrompt()`
|
|
3694
3719
|
* after side-effecting changes; see e.g. the memory hooks and
|
|
3695
|
-
* `#
|
|
3720
|
+
* `#syncAfterModelChange`.
|
|
3696
3721
|
*
|
|
3697
3722
|
* The current calendar date IS covered (appended as a segment) because
|
|
3698
3723
|
* `buildSystemPrompt` injects it into the prompt body (`Today is '{{date}}'`).
|
|
@@ -5284,7 +5309,7 @@ export class AgentSession {
|
|
|
5284
5309
|
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
5285
5310
|
// configured defaultLevel; otherwise preserve the current level (or auto).
|
|
5286
5311
|
this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
|
|
5287
|
-
await this.#
|
|
5312
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5288
5313
|
}
|
|
5289
5314
|
|
|
5290
5315
|
/**
|
|
@@ -5318,7 +5343,7 @@ export class AgentSession {
|
|
|
5318
5343
|
} else {
|
|
5319
5344
|
this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
|
|
5320
5345
|
}
|
|
5321
|
-
await this.#
|
|
5346
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5322
5347
|
}
|
|
5323
5348
|
|
|
5324
5349
|
/**
|
|
@@ -5462,7 +5487,7 @@ export class AgentSession {
|
|
|
5462
5487
|
|
|
5463
5488
|
// Apply the scoped model's configured thinking level, preserving auto.
|
|
5464
5489
|
this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : next.thinkingLevel);
|
|
5465
|
-
await this.#
|
|
5490
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5466
5491
|
|
|
5467
5492
|
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
|
5468
5493
|
}
|
|
@@ -5491,7 +5516,7 @@ export class AgentSession {
|
|
|
5491
5516
|
this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
5492
5517
|
// Re-apply the current thinking level (or auto) for the newly selected model
|
|
5493
5518
|
this.#reapplyThinkingLevel();
|
|
5494
|
-
await this.#
|
|
5519
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5495
5520
|
|
|
5496
5521
|
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
5497
5522
|
}
|