@oh-my-pi/pi-coding-agent 15.9.1 → 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 +68 -2
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +13 -4
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/manager.d.ts +1 -1
- 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 +4 -2
- 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/agent-session.d.ts +9 -0
- package/dist/types/session/auth-storage.d.ts +2 -2
- 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/task/types.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/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- 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/dry-balance-cli.ts +823 -0
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/cli-commands.ts +1 -0
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/model-registry.ts +60 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +14 -4
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- 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/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +7 -1
- package/src/mcp/manager.ts +40 -21
- 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 +102 -30
- package/src/modes/components/tree-selector.ts +29 -2
- 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 +41 -3
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +48 -13
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/types.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/sdk.ts +99 -18
- package/src/session/agent-session.ts +103 -19
- package/src/session/auth-storage.ts +4 -0
- 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/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -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/bash.ts +3 -4
- package/src/tools/fetch.ts +29 -9
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +22 -8
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-reflect.ts +2 -2
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/read.ts +58 -12
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +78 -12
- package/src/tui/hyperlink.ts +42 -7
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +58 -37
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +20 -52
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { $env, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { getRoleInfo } from "../../config/model-registry";
|
|
5
5
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
6
6
|
import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
7
7
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
8
8
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
9
|
+
import { materializeImageReferenceLinks } from "../../modes/image-references";
|
|
9
10
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
10
11
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
11
12
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
@@ -253,6 +254,8 @@ export class InputController {
|
|
|
253
254
|
if (this.ctx.onInputCallback) {
|
|
254
255
|
this.ctx.editor.setText("");
|
|
255
256
|
this.ctx.pendingImages = [];
|
|
257
|
+
this.ctx.pendingImageLinks = [];
|
|
258
|
+
this.ctx.editor.imageLinks = undefined;
|
|
256
259
|
this.ctx.onInputCallback({ text: "", cancelled: false, started: true });
|
|
257
260
|
}
|
|
258
261
|
return;
|
|
@@ -260,12 +263,15 @@ export class InputController {
|
|
|
260
263
|
|
|
261
264
|
const runner = this.ctx.session.extensionRunner;
|
|
262
265
|
let inputImages = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
|
|
266
|
+
let inputImageLinks = this.ctx.pendingImageLinks.length > 0 ? [...this.ctx.pendingImageLinks] : undefined;
|
|
263
267
|
|
|
264
268
|
if (runner?.hasHandlers("input")) {
|
|
265
269
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
266
270
|
if (result?.handled) {
|
|
267
271
|
this.ctx.editor.setText("");
|
|
268
272
|
this.ctx.pendingImages = [];
|
|
273
|
+
this.ctx.pendingImageLinks = [];
|
|
274
|
+
this.ctx.editor.imageLinks = undefined;
|
|
269
275
|
return;
|
|
270
276
|
}
|
|
271
277
|
if (result?.text !== undefined) {
|
|
@@ -273,6 +279,10 @@ export class InputController {
|
|
|
273
279
|
}
|
|
274
280
|
if (result?.images !== undefined) {
|
|
275
281
|
inputImages = result.images;
|
|
282
|
+
inputImageLinks = await materializeImageReferenceLinks(
|
|
283
|
+
inputImages,
|
|
284
|
+
this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
|
|
285
|
+
);
|
|
276
286
|
}
|
|
277
287
|
}
|
|
278
288
|
|
|
@@ -356,8 +366,10 @@ export class InputController {
|
|
|
356
366
|
if (this.ctx.session.isStreaming) {
|
|
357
367
|
this.ctx.editor.addToHistory(text);
|
|
358
368
|
this.ctx.editor.setText("");
|
|
369
|
+
this.ctx.editor.imageLinks = undefined;
|
|
359
370
|
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
360
371
|
this.ctx.pendingImages = [];
|
|
372
|
+
this.ctx.pendingImageLinks = [];
|
|
361
373
|
// Record the signature so the queued message's eventual delivery
|
|
362
374
|
// (a user-role `message_start` event) leaves any draft the user has
|
|
363
375
|
// typed since queuing intact. Same protection as #783, applied to
|
|
@@ -406,16 +418,28 @@ export class InputController {
|
|
|
406
418
|
}
|
|
407
419
|
}
|
|
408
420
|
})
|
|
409
|
-
.catch(
|
|
421
|
+
.catch(err => {
|
|
422
|
+
logger.warn("title-generator: uncaught auto-title error", {
|
|
423
|
+
sessionId: this.ctx.session.sessionId,
|
|
424
|
+
reason: "uncaught-auto-title-error",
|
|
425
|
+
error: err instanceof Error ? err.message : String(err),
|
|
426
|
+
});
|
|
427
|
+
});
|
|
410
428
|
}
|
|
411
429
|
|
|
412
430
|
if (this.ctx.onInputCallback) {
|
|
413
431
|
// Include any pending images from clipboard paste
|
|
432
|
+
this.ctx.editor.imageLinks = undefined;
|
|
414
433
|
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
415
434
|
this.ctx.pendingImages = [];
|
|
435
|
+
this.ctx.pendingImageLinks = [];
|
|
416
436
|
|
|
417
437
|
// Render user message immediately, then let session events catch up
|
|
418
|
-
const submission = this.ctx.startPendingSubmission({
|
|
438
|
+
const submission = this.ctx.startPendingSubmission({
|
|
439
|
+
text,
|
|
440
|
+
images,
|
|
441
|
+
imageLinks: inputImageLinks,
|
|
442
|
+
});
|
|
419
443
|
|
|
420
444
|
this.ctx.onInputCallback(submission);
|
|
421
445
|
}
|
|
@@ -679,11 +703,25 @@ export class InputController {
|
|
|
679
703
|
}
|
|
680
704
|
}
|
|
681
705
|
|
|
706
|
+
const imageLink = (
|
|
707
|
+
await materializeImageReferenceLinks(
|
|
708
|
+
[
|
|
709
|
+
{
|
|
710
|
+
type: "image",
|
|
711
|
+
data: imageData.data,
|
|
712
|
+
mimeType: imageData.mimeType,
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
|
|
716
|
+
)
|
|
717
|
+
)?.[0];
|
|
682
718
|
this.ctx.pendingImages.push({
|
|
683
719
|
type: "image",
|
|
684
720
|
data: imageData.data,
|
|
685
721
|
mimeType: imageData.mimeType,
|
|
686
722
|
});
|
|
723
|
+
this.ctx.pendingImageLinks.push(imageLink);
|
|
724
|
+
this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
|
|
687
725
|
// Insert placeholder at cursor like Claude does
|
|
688
726
|
const imageNum = this.ctx.pendingImages.length;
|
|
689
727
|
const placeholder = `[Image #${imageNum}]`;
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
3
|
-
import { Input, matchesKey,
|
|
3
|
+
import { Input, matchesKey, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { OAuthSelectorComponent } from "../../components/oauth-selector";
|
|
6
6
|
import { theme } from "../../theme/theme";
|
|
@@ -15,6 +15,10 @@ const CALLBACK_SERVER_PROVIDERS: Partial<Record<OAuthProvider, true>> = {
|
|
|
15
15
|
"google-antigravity": true,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
function loginUrlLink(url: string): string {
|
|
19
|
+
return `\x1b]8;;${url}\x07Open login URL\x1b]8;;\x07`;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
interface PromptState {
|
|
19
23
|
message: string;
|
|
20
24
|
placeholder?: string;
|
|
@@ -33,6 +37,7 @@ export class SignInTab implements SetupTab {
|
|
|
33
37
|
#authStorage: AuthStorage;
|
|
34
38
|
#selector: OAuthSelectorComponent;
|
|
35
39
|
#statusLines: string[] = [];
|
|
40
|
+
#authUrl: string | undefined;
|
|
36
41
|
#prompt: PromptState | undefined;
|
|
37
42
|
#promptResolve: ((value: string) => void) | undefined;
|
|
38
43
|
#loginAbort: AbortController | undefined;
|
|
@@ -72,22 +77,31 @@ export class SignInTab implements SetupTab {
|
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
render(width: number): string[] {
|
|
75
|
-
const lines = [
|
|
80
|
+
const lines: string[] = [];
|
|
76
81
|
if (this.#loggingInProvider) {
|
|
77
|
-
lines.push(theme.bold(`Signing in to ${this.#loggingInProvider}`)
|
|
82
|
+
lines.push(theme.bold(`Signing in to ${this.#loggingInProvider}`));
|
|
78
83
|
} else {
|
|
84
|
+
lines.push(theme.fg("muted", "Pick a provider to sign in — you can connect more than one."), "");
|
|
79
85
|
lines.push(...this.#selector.render(width));
|
|
80
86
|
}
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
|
|
88
|
+
const urlLines = this.#authUrl ? wrapTextWithAnsi(theme.fg("dim", this.#authUrl), width) : [];
|
|
89
|
+
if (this.#authUrl) {
|
|
90
|
+
lines.push(theme.fg("accent", `Browser login: ${loginUrlLink(this.#authUrl)}`), ...urlLines.slice(0, 2));
|
|
83
91
|
}
|
|
84
92
|
if (this.#prompt) {
|
|
85
|
-
lines.push(
|
|
93
|
+
lines.push(theme.fg("warning", this.#prompt.message));
|
|
86
94
|
if (this.#prompt.placeholder) {
|
|
87
95
|
lines.push(theme.fg("dim", this.#prompt.placeholder));
|
|
88
96
|
}
|
|
89
97
|
lines.push(this.#prompt.input.render(width)[0] ?? "");
|
|
90
98
|
}
|
|
99
|
+
if (urlLines.length > 2) {
|
|
100
|
+
lines.push(...urlLines);
|
|
101
|
+
}
|
|
102
|
+
if (this.#statusLines.length > 0) {
|
|
103
|
+
lines.push(...this.#statusLines.flatMap(line => wrapTextWithAnsi(line, width)));
|
|
104
|
+
}
|
|
91
105
|
return lines;
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -109,6 +123,7 @@ export class SignInTab implements SetupTab {
|
|
|
109
123
|
this.#selector.stopValidation();
|
|
110
124
|
this.#loggingInProvider = providerId;
|
|
111
125
|
this.#statusLines = [theme.fg("dim", "Starting OAuth flow…")];
|
|
126
|
+
this.#authUrl = undefined;
|
|
112
127
|
this.#loginAbort = new AbortController();
|
|
113
128
|
this.host.restoreFocus();
|
|
114
129
|
this.host.requestRender();
|
|
@@ -116,7 +131,8 @@ export class SignInTab implements SetupTab {
|
|
|
116
131
|
await this.#authStorage.login(providerId as OAuthProvider, {
|
|
117
132
|
signal: this.#loginAbort.signal,
|
|
118
133
|
onAuth: info => {
|
|
119
|
-
this.#
|
|
134
|
+
this.#authUrl = info.url;
|
|
135
|
+
this.#statusLines = [];
|
|
120
136
|
if (info.instructions) {
|
|
121
137
|
this.#statusLines.push(theme.fg("warning", info.instructions));
|
|
122
138
|
}
|
|
@@ -140,6 +156,7 @@ export class SignInTab implements SetupTab {
|
|
|
140
156
|
theme.fg("success", `${theme.status.success} Signed in to ${providerId}`),
|
|
141
157
|
theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`),
|
|
142
158
|
];
|
|
159
|
+
this.#authUrl = undefined;
|
|
143
160
|
this.#loggingInProvider = undefined;
|
|
144
161
|
this.#loginAbort = undefined;
|
|
145
162
|
this.#selector.stopValidation();
|
|
@@ -150,12 +167,14 @@ export class SignInTab implements SetupTab {
|
|
|
150
167
|
if (this.#disposed) return;
|
|
151
168
|
if (this.#loginAbort?.signal.aborted) {
|
|
152
169
|
this.#statusLines = [theme.fg("dim", "Login cancelled.")];
|
|
170
|
+
this.#authUrl = undefined;
|
|
153
171
|
} else {
|
|
154
172
|
const message = error instanceof Error ? error.message : String(error);
|
|
155
173
|
this.#statusLines = [
|
|
156
174
|
theme.fg("error", `Login failed: ${message}`),
|
|
157
175
|
theme.fg("dim", "Choose another provider or press Esc to continue."),
|
|
158
176
|
];
|
|
177
|
+
this.#authUrl = undefined;
|
|
159
178
|
}
|
|
160
179
|
this.#loggingInProvider = undefined;
|
|
161
180
|
this.#loginAbort = undefined;
|
|
@@ -174,6 +193,7 @@ export class SignInTab implements SetupTab {
|
|
|
174
193
|
this.#resolvePrompt(value);
|
|
175
194
|
};
|
|
176
195
|
input.onEscape = () => {
|
|
196
|
+
this.#loginAbort?.abort();
|
|
177
197
|
this.#resolvePrompt("");
|
|
178
198
|
};
|
|
179
199
|
this.host.setFocus(input);
|
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>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Write a 20-line poem about balancing OAuth accounts across many providers.
|
|
2
|
+
|
|
3
|
+
Form:
|
|
4
|
+
- Exactly 20 lines, no title, no stanza breaks.
|
|
5
|
+
- Each line is terse and image-driven, in the spirit of haiku: 7 words or fewer, no end punctuation.
|
|
6
|
+
- Let the imagery carry the theme — tokens, scopes, refresh cycles, expiry, consent, revocation — rather than naming them literally.
|
|
7
|
+
|
|
8
|
+
Output only the 20 lines. No preamble, no commentary, no code fences.
|