@timurproko/a1 0.1.8-dev.368 → 0.1.8-dev.402
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/dist/composition/owned-ui.d.ts +2 -0
- package/dist/composition/owned-ui.js +28 -11
- package/dist/contracts/owned-ui/image-attachments.d.ts +7 -0
- package/dist/contracts/owned-ui/image-attachments.js +7 -0
- package/dist/foundation/release/dependency-certification.js +65 -7
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +8 -6
- package/dist/integrations/pi/components/owned-editor-ux.js +115 -26
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +4 -0
- package/dist/integrations/pi/components/shell-presenters-transcript.js +15 -5
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +14 -5
- package/dist/integrations/pi/components/tool-image-presentation.d.ts +21 -0
- package/dist/integrations/pi/components/tool-image-presentation.js +86 -0
- package/dist/integrations/pi/components/tool-result-adapter.d.ts +2 -2
- package/dist/integrations/pi/components/transcript-image-resolver.d.ts +7 -0
- package/dist/integrations/pi/components/transcript-image-resolver.js +19 -0
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -1
- package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +61 -0
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +344 -0
- package/dist/integrations/pi/engine/adapter.d.ts +4 -1
- package/dist/integrations/pi/engine/adapter.js +66 -28
- package/dist/integrations/pi/engine/pending-delivery.d.ts +4 -0
- package/dist/integrations/pi/engine/pending-delivery.js +15 -6
- package/dist/integrations/pi/engine/tool-rendering.js +26 -3
- package/dist/integrations/pi/engine/transcript-image-assets.d.ts +11 -0
- package/dist/integrations/pi/engine/transcript-image-assets.js +86 -0
- package/dist/integrations/pi/session-ui/clipboard-diagnostics.d.ts +16 -0
- package/dist/integrations/pi/session-ui/clipboard-diagnostics.js +65 -0
- package/dist/integrations/pi/session-ui/editor-hyperlink-budget.d.ts +9 -0
- package/dist/integrations/pi/session-ui/editor-hyperlink-budget.js +23 -0
- package/dist/integrations/pi/session-ui/index.d.ts +1 -0
- package/dist/integrations/pi/session-ui/index.js +1 -0
- package/dist/integrations/pi/session-ui/paste-executor.d.ts +10 -0
- package/dist/integrations/pi/session-ui/paste-executor.js +232 -0
- package/dist/integrations/pi/session-ui/paste-helper.d.ts +1 -0
- package/dist/integrations/pi/session-ui/paste-helper.js +173 -0
- package/dist/integrations/pi/session-ui/paste-preparation-client.d.ts +16 -0
- package/dist/integrations/pi/session-ui/paste-preparation-client.js +159 -0
- package/dist/integrations/pi/session-ui/paste-protocol.d.ts +78 -0
- package/dist/integrations/pi/session-ui/paste-protocol.js +18 -0
- package/dist/integrations/pi/session-ui/paste-text-preparation.d.ts +20 -0
- package/dist/integrations/pi/session-ui/paste-text-preparation.js +134 -0
- package/dist/integrations/pi/session-ui/paste-text-worker.d.ts +1 -0
- package/dist/integrations/pi/session-ui/paste-text-worker.js +12 -0
- package/dist/integrations/pi/session-ui/path-chip-presentation.d.ts +7 -0
- package/dist/integrations/pi/session-ui/path-chip-presentation.js +24 -0
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +8 -1
- package/dist/integrations/pi/session-ui/prompt-chips.js +129 -149
- package/dist/integrations/pi/session-ui/response-copy-coordinator.d.ts +27 -0
- package/dist/integrations/pi/session-ui/response-copy-coordinator.js +189 -0
- package/dist/integrations/pi/session-ui/response-copy-helper.d.ts +1 -0
- package/dist/integrations/pi/session-ui/response-copy-helper.js +122 -0
- package/dist/integrations/pi/session-ui/response-copy-protocol.d.ts +56 -0
- package/dist/integrations/pi/session-ui/response-copy-protocol.js +5 -0
- package/dist/integrations/pi/session-ui/response-copy-transport.d.ts +31 -0
- package/dist/integrations/pi/session-ui/response-copy-transport.js +187 -0
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +15 -6
- package/dist/integrations/pi/session-ui/session-shell-root.js +25 -6
- package/dist/integrations/pi/session-ui/session-shell.js +100 -28
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -3
- package/dist/integrations/pi/session-ui/system-clipboard.d.ts +2 -2
- package/dist/integrations/pi/session-ui/system-clipboard.js +26 -11
- package/dist/integrations/pi/tui-runtime/adapter.js +3 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +1 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/selection-copy.d.ts +24 -0
- package/dist/ui/components/selection-copy.js +39 -0
- package/dist/ui/components/spans.d.ts +5 -2
- package/dist/ui/components/spans.js +12 -3
- package/dist/ui/components/text-selection.js +2 -0
- package/dist/ui/components/text.js +1 -1
- package/dist/ui/components/transcript-viewport.d.ts +5 -0
- package/dist/ui/components/transcript-viewport.js +100 -23
- package/dist/ui/components/visible-hyperlinks.js +40 -8
- package/docs/architecture/response-copy-delivery.md +83 -0
- package/docs/architecture/tool-image-presentation.md +62 -0
- package/docs/ci-release-runbook.md +19 -7
- package/docs/openspec-archive-automation.md +124 -0
- package/package.json +2 -1
|
@@ -23,6 +23,8 @@ export interface OwnedUiCompositionOptions {
|
|
|
23
23
|
readonly projectTrustPrompt?: PiProjectTrustPreflightPrompt;
|
|
24
24
|
/** Explicit local diagnostic destination; ignored by comparison/settings-free compositions. */
|
|
25
25
|
readonly suggestionDiagnosticsPath?: string;
|
|
26
|
+
/** Optional local metadata-only clipboard diagnostics; never enabled in comparison profiles. */
|
|
27
|
+
readonly clipboardDiagnosticsPath?: string;
|
|
26
28
|
}
|
|
27
29
|
export interface OwnedUiComposition {
|
|
28
30
|
readonly application: OwnedUiApplicationPort;
|
|
@@ -5,7 +5,7 @@ import { resolvePromptHistoryDataDir } from "../features/launch/index.js";
|
|
|
5
5
|
import { resolveProductPaths } from "../foundation/lifecycle/index.js";
|
|
6
6
|
import { applyConfiguredPiTheme, getAvailablePiThemes, loadHistoryEditor } from "../integrations/pi/components/index.js";
|
|
7
7
|
import { createPiEngineAdapter, } from "../integrations/pi/engine/index.js";
|
|
8
|
-
import { OwnedUiSessionShell } from "../integrations/pi/session-ui/index.js";
|
|
8
|
+
import { ClipboardDiagnosticCapture, OwnedUiSessionShell } from "../integrations/pi/session-ui/index.js";
|
|
9
9
|
import { OwnedUiSettingsSession, OwnedUiSettingsStore } from "../ui/settings/index.js";
|
|
10
10
|
import { createPiTerminalBridge } from "../integrations/pi/tui-runtime/index.js";
|
|
11
11
|
import { createOwnedRouteHost } from "./settings-route-host.js";
|
|
@@ -63,16 +63,32 @@ export async function composeOwnedUi(options = {}) {
|
|
|
63
63
|
}),
|
|
64
64
|
imageSidecar: new PromptImageSidecar(historyProfileLocation.imagesDir),
|
|
65
65
|
};
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
const clipboardDestination = options.clipboardDiagnosticsPath ?? process.env[PRODUCT_IDENTITY.environment.clipboardDiagnostics];
|
|
67
|
+
const clipboardDiagnostics = settings !== null && ownedSurfaces && clipboardDestination?.trim()
|
|
68
|
+
? new ClipboardDiagnosticCapture(clipboardDestination) : null;
|
|
69
|
+
let shell;
|
|
70
|
+
try {
|
|
71
|
+
shell = new OwnedUiSessionShell({
|
|
72
|
+
backend: adapter,
|
|
73
|
+
cwd: adapter.cwd,
|
|
74
|
+
...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
|
|
75
|
+
...(routeHost === null ? {} : { routeHost }),
|
|
76
|
+
...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
|
|
77
|
+
...(clipboardDiagnostics === null ? {} : {
|
|
78
|
+
responseCopy: { onEvent: event => clipboardDiagnostics.copy(event) },
|
|
79
|
+
pasteDiagnostics: event => clipboardDiagnostics.paste(event),
|
|
80
|
+
inputPresentation: { onEvent: event => clipboardDiagnostics.runtime(event) },
|
|
81
|
+
}),
|
|
82
|
+
...(viewportSettings === null ? {} : { viewportSettings }),
|
|
83
|
+
...(promptSuggestions === null ? {} : { promptSuggestions }),
|
|
84
|
+
...(promptHistory === null ? {} : { promptHistory: { ...promptHistory, editor: await loadHistoryEditor() } }),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
clipboardDiagnostics?.dispose();
|
|
89
|
+
suggestionDiagnostics?.dispose();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
76
92
|
const application = {
|
|
77
93
|
get disposed() { return adapter.disposed; },
|
|
78
94
|
start: () => shell.start(),
|
|
@@ -84,6 +100,7 @@ export async function composeOwnedUi(options = {}) {
|
|
|
84
100
|
}
|
|
85
101
|
finally {
|
|
86
102
|
suggestionDiagnostics?.dispose();
|
|
103
|
+
clipboardDiagnostics?.dispose();
|
|
87
104
|
}
|
|
88
105
|
},
|
|
89
106
|
};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { OwnedUiImageAttachment } from "./model.js";
|
|
2
2
|
export declare const MAX_PROMPT_IMAGES = 8;
|
|
3
|
+
/** Owned clipboard text is bounded before transfer, normalization, or editor insertion. */
|
|
4
|
+
export declare const MAX_CLIPBOARD_TEXT_BYTES: number;
|
|
3
5
|
/** Limit applies to canonical ASCII base64, not decoded image bytes. */
|
|
4
6
|
export declare const MAX_IMAGE_DATA_BYTES: number;
|
|
5
7
|
declare const MESSAGES: {
|
|
8
|
+
readonly "paste-timeout": "Clipboard paste timed out. The editor is still available.";
|
|
9
|
+
readonly "paste-unavailable": "Could not paste; the clipboard is unavailable.";
|
|
10
|
+
readonly "paste-size": "Pasted text exceeds the 16 MiB limit.";
|
|
11
|
+
readonly "paste-busy": "Clipboard preparation is full (8 pending pastes).";
|
|
12
|
+
readonly "paste-write-failed": "Paste skipped because the preceding copy failed.";
|
|
6
13
|
readonly "image-size": "Image exceeds the 8 MiB encoded base64 limit. Reduce the image size and paste it again.";
|
|
7
14
|
readonly "image-count": "A prompt supports at most 8 images. Remove an attachment before adding another.";
|
|
8
15
|
readonly "image-data": "Image data is invalid. Paste a valid image again.";
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export const MAX_PROMPT_IMAGES = 8;
|
|
2
|
+
/** Owned clipboard text is bounded before transfer, normalization, or editor insertion. */
|
|
3
|
+
export const MAX_CLIPBOARD_TEXT_BYTES = 16 * 1024 * 1024;
|
|
2
4
|
/** Limit applies to canonical ASCII base64, not decoded image bytes. */
|
|
3
5
|
export const MAX_IMAGE_DATA_BYTES = 8 * 1024 * 1024;
|
|
4
6
|
const MESSAGES = {
|
|
7
|
+
"paste-timeout": "Clipboard paste timed out. The editor is still available.",
|
|
8
|
+
"paste-unavailable": "Could not paste; the clipboard is unavailable.",
|
|
9
|
+
"paste-size": "Pasted text exceeds the 16 MiB limit.",
|
|
10
|
+
"paste-busy": "Clipboard preparation is full (8 pending pastes).",
|
|
11
|
+
"paste-write-failed": "Paste skipped because the preceding copy failed.",
|
|
5
12
|
"image-size": "Image exceeds the 8 MiB encoded base64 limit. Reduce the image size and paste it again.",
|
|
6
13
|
"image-count": "A prompt supports at most 8 images. Remove an attachment before adding another.",
|
|
7
14
|
"image-data": "Image data is invalid. Paste a valid image again.",
|
|
@@ -82,9 +82,16 @@ async function publishCertification(dataDir, identity, document, replaceInvalid)
|
|
|
82
82
|
try {
|
|
83
83
|
await publishLockedCertification(dataDir, identity, document, replaceInvalid, path);
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
catch (publicationError) {
|
|
86
|
+
try {
|
|
87
|
+
await releaseLock();
|
|
88
|
+
}
|
|
89
|
+
catch (releaseError) {
|
|
90
|
+
throw new AggregateError([publicationError, releaseError], "dependency certification publication and lease release failed");
|
|
91
|
+
}
|
|
92
|
+
throw publicationError;
|
|
87
93
|
}
|
|
94
|
+
await releaseLock();
|
|
88
95
|
}
|
|
89
96
|
async function publishLockedCertification(dataDir, identity, document, replaceInvalid, path) {
|
|
90
97
|
const existing = await managedDependencyCertificationPath(dataDir, identity.layerId);
|
|
@@ -130,11 +137,7 @@ async function acquirePublicationLock(path) {
|
|
|
130
137
|
// Concurrency: a nonempty directory cannot replace another nonempty directory on either
|
|
131
138
|
// platform. Publishing the complete owner atomically also avoids partially written leases.
|
|
132
139
|
await rename(candidate, path);
|
|
133
|
-
return
|
|
134
|
-
const released = `${path}.released-${token}`;
|
|
135
|
-
await rename(path, released);
|
|
136
|
-
await rm(released, { recursive: true, force: true });
|
|
137
|
-
};
|
|
140
|
+
return () => releasePublicationLock(path, token);
|
|
138
141
|
}
|
|
139
142
|
catch (error) {
|
|
140
143
|
if (!["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].some(code => hasCode(error, code)))
|
|
@@ -182,6 +185,61 @@ async function acquirePublicationLock(path) {
|
|
|
182
185
|
await rm(candidate, { recursive: true, force: true });
|
|
183
186
|
}
|
|
184
187
|
}
|
|
188
|
+
/** Retire this generation once; retries after retirement can only touch its private cleanup path. */
|
|
189
|
+
async function releasePublicationLock(path, token) {
|
|
190
|
+
const released = `${path}.released-${token}`;
|
|
191
|
+
const deadline = Date.now() + 1_000;
|
|
192
|
+
let retired = false;
|
|
193
|
+
let delay = 20;
|
|
194
|
+
while (true) {
|
|
195
|
+
try {
|
|
196
|
+
if (!retired) {
|
|
197
|
+
await assertPublicationLockOwner(path, token);
|
|
198
|
+
await rename(path, released);
|
|
199
|
+
retired = true;
|
|
200
|
+
}
|
|
201
|
+
// Concurrency: a successor can own `path` now. Never inspect or delete it during cleanup.
|
|
202
|
+
await rm(released, { recursive: true, force: true });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const phase = retired ? "cleanup" : "retirement";
|
|
207
|
+
if (!isReleaseContention(error)) {
|
|
208
|
+
throw new Error(`dependency certification lease release ${phase} failed: ${error instanceof Error ? error.message : "unknown error"}`, { cause: error });
|
|
209
|
+
}
|
|
210
|
+
const remaining = deadline - Date.now();
|
|
211
|
+
if (remaining <= 0) {
|
|
212
|
+
throw new Error(`dependency certification lease release ${phase} deadline exceeded (${error.code})`, { cause: error });
|
|
213
|
+
}
|
|
214
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, Math.min(delay, remaining)));
|
|
215
|
+
delay = Math.min(delay * 2, 100);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** Ownership uncertainty is never authority to retire a missing, linked, or replaced generation. */
|
|
220
|
+
async function assertPublicationLockOwner(path, token) {
|
|
221
|
+
try {
|
|
222
|
+
const metadata = await lstat(path);
|
|
223
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpath(path) !== path)
|
|
224
|
+
throw new Error("invalid lock directory");
|
|
225
|
+
const ownerPath = resolve(path, "owner.json");
|
|
226
|
+
const ownerMetadata = await lstat(ownerPath);
|
|
227
|
+
if (!ownerMetadata.isFile() || ownerMetadata.isSymbolicLink() || await realpath(ownerPath) !== ownerPath)
|
|
228
|
+
throw new Error("invalid owner file");
|
|
229
|
+
const owner = JSON.parse(await readFile(ownerPath, "utf8"));
|
|
230
|
+
if (!owner || typeof owner !== "object" || !("pid" in owner) || !("token" in owner)
|
|
231
|
+
|| owner.pid !== process.pid || owner.token !== token)
|
|
232
|
+
throw new Error("owner generation differs");
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (isReleaseContention(error))
|
|
236
|
+
throw error;
|
|
237
|
+
throw new Error("publication lease release ownership could not be verified", { cause: error });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function isReleaseContention(error) {
|
|
241
|
+
return ["EPERM", "EACCES", "EBUSY"].some(code => hasCode(error, code));
|
|
242
|
+
}
|
|
185
243
|
function validateCertification(value, identity, allowLegacyPlatform = false) {
|
|
186
244
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
187
245
|
throw new Error(`dependency layer certification differs from manifest: ${identity.layerId}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SelectionEditor as Editor } from "./editor-interaction.js";
|
|
2
2
|
import type { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
|
|
3
|
-
import type { PiShellClipboardContent, PiShellEditorTextRange } from "./shell-shared-facade.js";
|
|
3
|
+
import type { PiShellClipboardContent, PiShellEditorTextRange, PiShellPasteReservation } from "./shell-shared-facade.js";
|
|
4
4
|
/**
|
|
5
5
|
* Middleware boundary around Pi's editor. Interceptors may claim an input or
|
|
6
6
|
* delegate to the next interceptor/vanilla editor, and may decorate the rows
|
|
@@ -46,16 +46,18 @@ export declare class OwnedEditorUxInterception {
|
|
|
46
46
|
export interface PromptSelectionUxOptions {
|
|
47
47
|
readonly copyText: (text: string) => void;
|
|
48
48
|
readonly readClipboardContent: () => Promise<PiShellClipboardContent | null>;
|
|
49
|
-
readonly beginClipboardPaste?: () =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
readonly beginClipboardPaste?: () => PiShellPasteReservation;
|
|
50
|
+
readonly beginTextPaste?: (text: string) => PiShellPasteReservation;
|
|
51
|
+
readonly deferTextPaste?: (text: string) => boolean;
|
|
52
|
+
readonly onPasteRejected?: (error: unknown) => void;
|
|
53
|
+
readonly onPasteInput?: (bytes: number) => void;
|
|
53
54
|
readonly transformPastedContent: (content: PiShellClipboardContent) => string;
|
|
54
55
|
readonly atomicRanges: (line: string) => readonly PiShellEditorTextRange[];
|
|
55
56
|
readonly hiddenRanges?: (line: string) => readonly PiShellEditorTextRange[];
|
|
56
57
|
readonly expandCopiedText: (text: string) => string;
|
|
57
58
|
readonly paintSelection: (line: string, from: number, to: number, atomic: boolean) => string;
|
|
58
|
-
|
|
59
|
+
/** Row indices restart at zero for each complete decoration pass. */
|
|
60
|
+
readonly decorateRow: (row: string, width: number, rowIndex: number) => string;
|
|
59
61
|
readonly requestRender: () => void;
|
|
60
62
|
readonly getRows: () => number;
|
|
61
63
|
/** Presentation-only columns reserved before semantic editor text. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES } from "../../../contracts/owned-ui/index.js";
|
|
1
2
|
import { CURSOR_MARKER, decodeKittyPrintable, visibleWidth, } from "#pi-tui";
|
|
2
3
|
import { promptPathWordRanges } from "./path-word-ranges.js";
|
|
3
4
|
export class OwnedEditorUxInterception {
|
|
@@ -267,7 +268,7 @@ class PromptSelectionInterceptor {
|
|
|
267
268
|
}
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
|
-
return rows.map(row => this.options.decorateRow(row, width));
|
|
271
|
+
return rows.map((row, index) => this.options.decorateRow(row, width, index));
|
|
271
272
|
}
|
|
272
273
|
reset() {
|
|
273
274
|
this.#selection = undefined;
|
|
@@ -447,20 +448,57 @@ class PromptSelectionInterceptor {
|
|
|
447
448
|
const before = data.slice(0, start);
|
|
448
449
|
this.handleInput(before, (replacement = before) => next(replacement));
|
|
449
450
|
}
|
|
450
|
-
this.#terminalPaste = "";
|
|
451
|
+
this.#terminalPaste = { parts: [], piece: "", tail: "", bytes: 0, oversized: false };
|
|
451
452
|
data = data.slice(start + 6);
|
|
452
453
|
}
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const end =
|
|
457
|
-
|
|
454
|
+
// Protocol: inspect only the new fragment plus a delimiter-sized suffix, not the whole growing body.
|
|
455
|
+
const pending = this.#terminalPaste;
|
|
456
|
+
const fragment = pending.tail + data;
|
|
457
|
+
const end = fragment.indexOf("\x1b[201~");
|
|
458
|
+
const append = (part) => {
|
|
459
|
+
if (pending.oversized || part.length === 0)
|
|
460
|
+
return;
|
|
461
|
+
if (this.options.beginTextPaste !== undefined) {
|
|
462
|
+
pending.bytes += Buffer.byteLength(part);
|
|
463
|
+
if (pending.bytes > MAX_CLIPBOARD_TEXT_BYTES) {
|
|
464
|
+
pending.oversized = true;
|
|
465
|
+
pending.parts = [];
|
|
466
|
+
pending.piece = "";
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Performance: bound fragment metadata even if a terminal supplies one character per input chunk.
|
|
471
|
+
pending.piece += part;
|
|
472
|
+
if (pending.piece.length >= 16_384) {
|
|
473
|
+
pending.parts.push(pending.piece);
|
|
474
|
+
pending.piece = "";
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
if (end < 0) {
|
|
478
|
+
let cut = Math.max(0, fragment.length - 5);
|
|
479
|
+
if (cut > 0 && /[\uD800-\uDBFF]/u.test(fragment[cut - 1]) && /[\uDC00-\uDFFF]/u.test(fragment[cut]))
|
|
480
|
+
cut--;
|
|
481
|
+
append(fragment.slice(0, cut));
|
|
482
|
+
pending.tail = fragment.slice(cut);
|
|
458
483
|
return true;
|
|
459
|
-
|
|
460
|
-
|
|
484
|
+
}
|
|
485
|
+
append(fragment.slice(0, end));
|
|
486
|
+
const remaining = fragment.slice(end + 6);
|
|
461
487
|
this.#terminalPaste = undefined;
|
|
488
|
+
this.options.onPasteInput?.(pending.bytes);
|
|
489
|
+
if (pending.oversized) {
|
|
490
|
+
this.options.onPasteRejected?.(new ImageAttachmentError("paste-size"));
|
|
491
|
+
if (remaining.length > 0)
|
|
492
|
+
this.handleInput(remaining, (replacement = remaining) => next(replacement));
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
const text = pending.parts.join("") + pending.piece;
|
|
496
|
+
if (text.length > 0)
|
|
497
|
+
this.#redoStack = [];
|
|
462
498
|
if (text.length === 0)
|
|
463
499
|
this.pasteClipboard();
|
|
500
|
+
else if (this.options.beginTextPaste !== undefined && (this.options.deferTextPaste?.(text) ?? true))
|
|
501
|
+
this.#pasteFromClipboard(text);
|
|
464
502
|
else {
|
|
465
503
|
const transformed = this.options.transformPastedContent({ kind: "text", text });
|
|
466
504
|
if (transformed === text && this.#orderedSelection() === undefined)
|
|
@@ -478,18 +516,24 @@ class PromptSelectionInterceptor {
|
|
|
478
516
|
this.handleInput(remaining, (replacement = remaining) => next(replacement));
|
|
479
517
|
return true;
|
|
480
518
|
}
|
|
481
|
-
#pasteFromClipboard() {
|
|
482
|
-
|
|
519
|
+
#pasteFromClipboard(source) {
|
|
520
|
+
const begin = source === undefined ? this.options.beginClipboardPaste
|
|
521
|
+
: this.options.beginTextPaste === undefined ? undefined : () => this.options.beginTextPaste(source);
|
|
522
|
+
if (begin !== undefined) {
|
|
483
523
|
const generation = this.#pasteGeneration;
|
|
484
524
|
const restore = this.#orderedSelection() === undefined ? "" : this.#selectedText();
|
|
485
|
-
const paste =
|
|
525
|
+
const paste = begin();
|
|
526
|
+
this.#redoStack = [];
|
|
486
527
|
if (this.#orderedSelection() !== undefined)
|
|
487
528
|
this.#replaceSelection(paste.marker);
|
|
488
529
|
else
|
|
489
530
|
this.editor.insertTextAtCursor(paste.marker);
|
|
490
531
|
this.#requestRender();
|
|
491
|
-
const replace = (text) => {
|
|
492
|
-
|
|
532
|
+
const replace = (text) => {
|
|
533
|
+
if (generation === this.#pasteGeneration)
|
|
534
|
+
this.#replacePasteMarker(paste.marker, paste.isCurrent?.() === false ? restore : text || restore);
|
|
535
|
+
paste.complete?.();
|
|
536
|
+
};
|
|
493
537
|
void paste.result.then(replace).catch(() => replace(restore));
|
|
494
538
|
return;
|
|
495
539
|
}
|
|
@@ -520,10 +564,12 @@ class PromptSelectionInterceptor {
|
|
|
520
564
|
#replacePasteMarker(marker, replacement) {
|
|
521
565
|
const current = this.editor.getText();
|
|
522
566
|
const from = current.indexOf(marker);
|
|
523
|
-
|
|
567
|
+
const text = normalizeInsertedText(replacement);
|
|
568
|
+
if (from < 0) {
|
|
569
|
+
this.#rewritePasteSnapshots(marker, text);
|
|
524
570
|
return;
|
|
571
|
+
}
|
|
525
572
|
const to = from + marker.length;
|
|
526
|
-
const text = normalizeInsertedText(replacement);
|
|
527
573
|
const lines = editorState(this.editor).lines;
|
|
528
574
|
const remap = (position) => {
|
|
529
575
|
const offset = positionOffset(lines, position);
|
|
@@ -533,12 +579,23 @@ class PromptSelectionInterceptor {
|
|
|
533
579
|
const selection = this.#selection === undefined ? undefined : { anchor: remap(this.#selection.anchor), head: remap(this.#selection.head) };
|
|
534
580
|
const next = current.slice(0, from) + text + current.slice(to);
|
|
535
581
|
// Compatibility: completing a paste updates its provisional undo snapshots, not the user's undo history.
|
|
536
|
-
if (this.editor.interaction !== undefined)
|
|
582
|
+
if (this.editor.interaction !== undefined)
|
|
537
583
|
this.editor.interaction.replaceLines(next.split("\n"));
|
|
584
|
+
else
|
|
585
|
+
editorState(this.editor).lines = next.split("\n");
|
|
586
|
+
this.#rewritePasteSnapshots(marker, text);
|
|
587
|
+
this.#setCursor(positionAtOffset(next, cursor));
|
|
588
|
+
this.editor.onChange?.(this.editor.getText());
|
|
589
|
+
this.editor.invalidate();
|
|
590
|
+
if (selection !== undefined)
|
|
591
|
+
this.#selection = { anchor: positionAtOffset(next, selection.anchor), head: positionAtOffset(next, selection.head) };
|
|
592
|
+
this.#requestRender();
|
|
593
|
+
}
|
|
594
|
+
#rewritePasteSnapshots(marker, text) {
|
|
595
|
+
// Concurrency: even a removed reservation must be retired from redo/undo snapshots, or a canceled paste can reappear.
|
|
596
|
+
if (this.editor.interaction !== undefined)
|
|
538
597
|
this.editor.interaction.updateUndoStates(state => replaceSnapshotMarker(state, marker, text));
|
|
539
|
-
}
|
|
540
598
|
else {
|
|
541
|
-
editorState(this.editor).lines = next.split("\n");
|
|
542
599
|
const undo = Reflect.get(this.editor, "undoStack");
|
|
543
600
|
const snapshots = typeof undo === "object" && undo !== null ? Reflect.get(undo, "stack") : undefined;
|
|
544
601
|
if (Array.isArray(snapshots))
|
|
@@ -554,12 +611,6 @@ class PromptSelectionInterceptor {
|
|
|
554
611
|
snapshot.text = state.lines.join("\n");
|
|
555
612
|
snapshot.cursor = { line: state.cursorLine, col: state.cursorCol };
|
|
556
613
|
}
|
|
557
|
-
this.#setCursor(positionAtOffset(next, cursor));
|
|
558
|
-
this.editor.onChange?.(this.editor.getText());
|
|
559
|
-
this.editor.invalidate();
|
|
560
|
-
if (selection !== undefined)
|
|
561
|
-
this.#selection = { anchor: positionAtOffset(next, selection.anchor), head: positionAtOffset(next, selection.head) };
|
|
562
|
-
this.#requestRender();
|
|
563
614
|
}
|
|
564
615
|
#replaceSelection(text) {
|
|
565
616
|
const selection = this.#orderedSelection();
|
|
@@ -757,7 +808,6 @@ class PromptSelectionInterceptor {
|
|
|
757
808
|
}
|
|
758
809
|
function installAtomicSegmentation(editor, rangesForText, wordDirection) {
|
|
759
810
|
const transform = (text, mode, values) => {
|
|
760
|
-
const segments = [...values].filter(isEditorSegment);
|
|
761
811
|
const ranges = rangesForText(text).map(range => ({ ...range, wordLike: false }));
|
|
762
812
|
if (mode === "word") {
|
|
763
813
|
for (const range of contextualPathRanges(editor, text, wordDirection())) {
|
|
@@ -766,6 +816,12 @@ function installAtomicSegmentation(editor, rangesForText, wordDirection) {
|
|
|
766
816
|
}
|
|
767
817
|
}
|
|
768
818
|
ranges.sort((left, right) => left.start - right.start);
|
|
819
|
+
if (mode === "grapheme" && ranges.length > 0 && hasSegmentLookup(values)) {
|
|
820
|
+
const indexed = atomicSegmentsByBoundary(text, ranges, offset => values.containing(offset));
|
|
821
|
+
if (indexed !== undefined)
|
|
822
|
+
return indexed;
|
|
823
|
+
}
|
|
824
|
+
const segments = [...values].filter(isEditorSegment);
|
|
769
825
|
if (ranges.length === 0)
|
|
770
826
|
return segments;
|
|
771
827
|
const merged = [];
|
|
@@ -803,6 +859,39 @@ function installAtomicSegmentation(editor, rangesForText, wordDirection) {
|
|
|
803
859
|
Reflect.set(editor, "segment", (text, mode) => transform(text, mode, original(text, mode)));
|
|
804
860
|
Reflect.set(editor, ATOMIC_SEGMENTATION, true);
|
|
805
861
|
}
|
|
862
|
+
/** Narrow the optional indexed segment capability without a dynamic callback invocation. */
|
|
863
|
+
function hasSegmentLookup(values) {
|
|
864
|
+
return typeof values === "object" && values !== null && "containing" in values && typeof values.containing === "function";
|
|
865
|
+
}
|
|
866
|
+
/** Skip atomic interiors using the supplied segmenter's boundaries; preserve the iterable fallback exactly. */
|
|
867
|
+
function atomicSegmentsByBoundary(text, ranges, containing) {
|
|
868
|
+
const merged = [];
|
|
869
|
+
let offset = 0, rangeIndex = 0;
|
|
870
|
+
while (offset < text.length) {
|
|
871
|
+
const segment = containing(offset);
|
|
872
|
+
if (!isEditorSegment(segment) || segment.index !== offset || segment.segment.length === 0)
|
|
873
|
+
return undefined;
|
|
874
|
+
while ((ranges[rangeIndex]?.end ?? Number.POSITIVE_INFINITY) <= offset)
|
|
875
|
+
rangeIndex++;
|
|
876
|
+
const range = ranges[rangeIndex];
|
|
877
|
+
if (range !== undefined && offset >= range.start && offset < range.end) {
|
|
878
|
+
if (range.end > text.length)
|
|
879
|
+
return undefined;
|
|
880
|
+
const last = containing(range.end - 1);
|
|
881
|
+
if (!isEditorSegment(last) || last.index + last.segment.length < range.end)
|
|
882
|
+
return undefined;
|
|
883
|
+
if (offset === range.start)
|
|
884
|
+
merged.push({ segment: text.slice(range.start, range.end).replaceAll(" ", ATOMIC_SPACE_SENTINEL), index: range.start, input: text });
|
|
885
|
+
// Compatibility: the old iterable consumes the entire final grapheme, even if it crosses the chip end.
|
|
886
|
+
offset = last.index + last.segment.length;
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
merged.push(segment);
|
|
890
|
+
offset += segment.segment.length;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return merged;
|
|
894
|
+
}
|
|
806
895
|
function contextualPathRanges(editor, text, direction) {
|
|
807
896
|
if (direction === undefined)
|
|
808
897
|
return promptPathWordRanges(text);
|
|
@@ -76,6 +76,10 @@ export function createPiShellEditor(options) {
|
|
|
76
76
|
copyText: options.onCopyText ?? (() => { }),
|
|
77
77
|
readClipboardContent: options.readClipboardContent ?? (async () => null),
|
|
78
78
|
...(options.beginClipboardPaste === undefined ? {} : { beginClipboardPaste: options.beginClipboardPaste }),
|
|
79
|
+
...(options.beginTextPaste === undefined ? {} : { beginTextPaste: options.beginTextPaste }),
|
|
80
|
+
...(options.deferTextPaste === undefined ? {} : { deferTextPaste: options.deferTextPaste }),
|
|
81
|
+
...(options.onPasteRejected === undefined ? {} : { onPasteRejected: options.onPasteRejected }),
|
|
82
|
+
...(options.onPasteInput === undefined ? {} : { onPasteInput: options.onPasteInput }),
|
|
79
83
|
transformPastedContent: options.transformPastedContent ?? (content => content.kind === "text" ? content.text : ""),
|
|
80
84
|
atomicRanges: options.editorAtomicRanges ?? (() => []),
|
|
81
85
|
hiddenRanges: options.editorHiddenRanges ?? (() => []),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { AssistantMessageComponent, BashExecutionComponent, CompactionSummaryMessageComponent, CustomMessageComponent, DynamicBorder, getMarkdownTheme, parseSkillBlock,
|
|
1
|
+
import { AssistantMessageComponent, BashExecutionComponent, CompactionSummaryMessageComponent, CustomMessageComponent, DynamicBorder, getMarkdownTheme, parseSkillBlock, UserMessageComponent, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { ToolExecutionComponent } from "./upstream/components/tool-execution.js";
|
|
2
3
|
import { SkillInvocationMessageComponent } from "./upstream/components/skill-invocation-message.js";
|
|
3
4
|
import { piToolArguments as toolArguments, updatePiToolResult as updateToolResult } from "./tool-result-adapter.js";
|
|
5
|
+
import { createTranscriptImageResolver } from "./transcript-image-resolver.js";
|
|
4
6
|
import { createMermaidMarkdownTransformer } from "./upstream/components/mermaid.js";
|
|
5
7
|
import { Container, Image, Spacer, Text, } from "#pi-tui";
|
|
6
8
|
import { PRODUCT_TEXT } from "../../../product-identity.js";
|
|
@@ -11,6 +13,7 @@ import { componentPort, createTuiFacade, ensureTheme, isRecord, } from "./shell-
|
|
|
11
13
|
export function createPiShellTranscriptComponent(initial, cwd, extensions, submittedPrompt, initialOutputPad = PINNED_PI_LAYOUT.outputPad, initialHideThinkingBlock = false, initialMermaidRenderingMode = "off", initialShowImages = true, initialImageWidthCells = 80, imageAssets, presentation) {
|
|
12
14
|
ensureTheme();
|
|
13
15
|
let block = initial;
|
|
16
|
+
const retainedImages = createTranscriptImageResolver(imageAssets, initial);
|
|
14
17
|
let expanded = false;
|
|
15
18
|
let outputPad = initialOutputPad;
|
|
16
19
|
let hideThinkingBlock = initialHideThinkingBlock;
|
|
@@ -22,6 +25,7 @@ export function createPiShellTranscriptComponent(initial, cwd, extensions, submi
|
|
|
22
25
|
let updating = false;
|
|
23
26
|
let dirty = false;
|
|
24
27
|
let presentationRevision = 0;
|
|
28
|
+
let mountedTool;
|
|
25
29
|
const mutate = (action) => {
|
|
26
30
|
const previous = updating;
|
|
27
31
|
updating = true;
|
|
@@ -33,6 +37,7 @@ export function createPiShellTranscriptComponent(initial, cwd, extensions, submi
|
|
|
33
37
|
}
|
|
34
38
|
};
|
|
35
39
|
const rebuild = () => {
|
|
40
|
+
mountedTool?.dispose();
|
|
36
41
|
const token = ++mount;
|
|
37
42
|
const requestRender = () => {
|
|
38
43
|
if (disposed || updating || token !== mount || dirty)
|
|
@@ -41,8 +46,12 @@ export function createPiShellTranscriptComponent(initial, cwd, extensions, submi
|
|
|
41
46
|
presentationRevision++;
|
|
42
47
|
presentation?.changed();
|
|
43
48
|
};
|
|
44
|
-
return mutate(() =>
|
|
45
|
-
|
|
49
|
+
return mutate(() => {
|
|
50
|
+
const next = transcriptComponent(block, cwd, expanded, extensions, submittedPrompt, outputPad, hideThinkingBlock, mermaidRenderingMode, showImages, imageWidthCells, retainedImages, createTuiFacade({ getColumns: presentation?.getColumns ?? (() => 80),
|
|
51
|
+
getRows: presentation?.getRows ?? (() => 24), requestRender }));
|
|
52
|
+
mountedTool = next instanceof ToolExecutionComponent ? next : undefined;
|
|
53
|
+
return withTranscriptImages(next, block, retainedImages, showImages, imageWidthCells);
|
|
54
|
+
});
|
|
46
55
|
};
|
|
47
56
|
let component = rebuild();
|
|
48
57
|
return {
|
|
@@ -56,13 +65,14 @@ export function createPiShellTranscriptComponent(initial, cwd, extensions, submi
|
|
|
56
65
|
return warning === undefined ? rows : [...rows, ...new Text(piTheme().fg("warning", warning), outputPad, 0).render(width)];
|
|
57
66
|
},
|
|
58
67
|
invalidate: () => mutate(() => component.invalidate()),
|
|
59
|
-
dispose: () => { disposed = true; mount++; },
|
|
68
|
+
dispose: () => { disposed = true; mount++; mountedTool?.dispose(); retainedImages.dispose(); },
|
|
60
69
|
update(next) {
|
|
61
70
|
if (next.id !== block.id)
|
|
62
71
|
throw new TypeError("Pi transcript component identity cannot change");
|
|
63
72
|
const previous = block;
|
|
64
73
|
block = next;
|
|
65
|
-
|
|
74
|
+
retainedImages.update(next);
|
|
75
|
+
if (!mutate(() => updateTranscriptComponent(component, previous, next, expanded, retainedImages)))
|
|
66
76
|
component = rebuild();
|
|
67
77
|
},
|
|
68
78
|
setExpanded(next) {
|
|
@@ -134,6 +134,13 @@ export interface PiShellHeaderOptions {
|
|
|
134
134
|
readonly notices?: readonly PiShellStartupNotice[];
|
|
135
135
|
readonly resources?: readonly PiShellResourceEntry[];
|
|
136
136
|
}
|
|
137
|
+
/** An owned paste reserves its edit immediately and acknowledges completion only after insertion. */
|
|
138
|
+
export interface PiShellPasteReservation {
|
|
139
|
+
readonly marker: string;
|
|
140
|
+
readonly result: Promise<string>;
|
|
141
|
+
complete?(): void;
|
|
142
|
+
isCurrent?(): boolean;
|
|
143
|
+
}
|
|
137
144
|
export interface PiShellEditorOptions {
|
|
138
145
|
readonly persistentHistory?: boolean;
|
|
139
146
|
readonly historyEditor?: HistoryEditorConstructor;
|
|
@@ -162,16 +169,18 @@ export interface PiShellEditorOptions {
|
|
|
162
169
|
readonly onPromptSuggestionAccepted?: (text: string) => void;
|
|
163
170
|
readonly onCopyText?: (text: string) => void;
|
|
164
171
|
readonly readClipboardContent?: (signal?: AbortSignal) => Promise<PiShellClipboardContent | null>;
|
|
165
|
-
readonly beginClipboardPaste?: () =>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
readonly beginClipboardPaste?: () => PiShellPasteReservation;
|
|
173
|
+
readonly beginTextPaste?: (text: string) => PiShellPasteReservation;
|
|
174
|
+
readonly deferTextPaste?: (text: string) => boolean;
|
|
175
|
+
readonly onPasteRejected?: (error: unknown) => void;
|
|
176
|
+
readonly onPasteInput?: (bytes: number) => void;
|
|
169
177
|
readonly editorHiddenRanges?: (line: string) => readonly PiShellEditorTextRange[];
|
|
170
178
|
readonly transformPastedContent?: (content: PiShellClipboardContent) => string;
|
|
171
179
|
readonly editorAtomicRanges?: (line: string) => readonly PiShellEditorTextRange[];
|
|
172
180
|
readonly expandCopiedEditorText?: (text: string) => string;
|
|
173
181
|
readonly paintEditorSelection?: (line: string, from: number, to: number, atomic: boolean) => string;
|
|
174
|
-
|
|
182
|
+
/** Row indices restart at zero for each complete owned editor decoration pass. */
|
|
183
|
+
readonly decorateEditorRow?: (row: string, width: number, rowIndex: number) => string;
|
|
175
184
|
readonly cwd?: string;
|
|
176
185
|
readonly agentDir?: string;
|
|
177
186
|
readonly autocompleteCommands?: readonly PiShellAutocompleteCommand[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { convertToPng } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component } from "#pi-tui";
|
|
3
|
+
/**
|
|
4
|
+
* Own only current attachment presentation; tool renderers still receive original result content.
|
|
5
|
+
* Unlike pinned 0.84.2's index cache, entries follow immutable source bytes and stale completions
|
|
6
|
+
* cannot republish. At most one conversion runs per mount; queued work is only the current set.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ToolImagePresentation {
|
|
9
|
+
#private;
|
|
10
|
+
private readonly changed;
|
|
11
|
+
private readonly convert;
|
|
12
|
+
constructor(changed: () => void, convert?: typeof convertToPng);
|
|
13
|
+
update(content: readonly {
|
|
14
|
+
type: string;
|
|
15
|
+
data?: string;
|
|
16
|
+
mimeType?: string;
|
|
17
|
+
}[], show: boolean): void;
|
|
18
|
+
/** Public Image remains responsible for Kitty/iTerm protocol and dimension-aware fallback. */
|
|
19
|
+
components(width: number): Component[];
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|