@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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MAX_CLIPBOARD_TEXT_BYTES } from "../../../contracts/owned-ui/index.js";
|
|
2
|
+
export const PASTE_READ_MS = 5_000;
|
|
3
|
+
export const PASTE_TOTAL_MS = 15_000;
|
|
4
|
+
export const PASTE_STOP_MS = 250;
|
|
5
|
+
export const PASTE_TEXT_BYTES = MAX_CLIPBOARD_TEXT_BYTES;
|
|
6
|
+
export const PASTE_CHUNK_UNITS = 16_384;
|
|
7
|
+
export const PASTE_REQUESTS = 8;
|
|
8
|
+
/** Split without breaking UTF-16 surrogate pairs, so per-fragment UTF-8 accounting is exact. */
|
|
9
|
+
export function* pasteFragments(text) {
|
|
10
|
+
for (let offset = 0; offset < text.length;) {
|
|
11
|
+
let end = Math.min(text.length, offset + PASTE_CHUNK_UNITS);
|
|
12
|
+
const last = text.charCodeAt(end - 1);
|
|
13
|
+
if (end < text.length && last >= 0xd800 && last <= 0xdbff)
|
|
14
|
+
end--;
|
|
15
|
+
yield text.slice(offset, end);
|
|
16
|
+
offset = end;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ClipboardPath {
|
|
2
|
+
readonly fullPath: string;
|
|
3
|
+
readonly kind: "folder" | "file";
|
|
4
|
+
}
|
|
5
|
+
export type PreparedPasteText = {
|
|
6
|
+
readonly kind: "text";
|
|
7
|
+
readonly text: string;
|
|
8
|
+
readonly label?: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "url";
|
|
11
|
+
readonly url: string;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "paths";
|
|
15
|
+
readonly paths: readonly ClipboardPath[];
|
|
16
|
+
};
|
|
17
|
+
/** Small non-path text has a fixed CPU bound and cannot enter filesystem probing. */
|
|
18
|
+
export declare function canPreparePasteInline(text: string): boolean;
|
|
19
|
+
/** The shared chip classifier; interactive callers run path/filesystem probing in an isolated executor. */
|
|
20
|
+
export declare function preparePasteText(text: string, skipPaths?: boolean, compactPaths?: boolean): PreparedPasteText;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { prepareTextPaste } from "./text-paste.js";
|
|
5
|
+
import { preparePathPresentation } from "./path-chip-presentation.js";
|
|
6
|
+
const URL_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f]+$/iu;
|
|
7
|
+
/** Small non-path text has a fixed CPU bound and cannot enter filesystem probing. */
|
|
8
|
+
export function canPreparePasteInline(text) {
|
|
9
|
+
return text.length <= 1000 && !/^(?:[A-Za-z]:\\|\/|file:\/\/|["'&])/iu.test(text.trimStart());
|
|
10
|
+
}
|
|
11
|
+
/** The shared chip classifier; interactive callers run path/filesystem probing in an isolated executor. */
|
|
12
|
+
export function preparePasteText(text, skipPaths = false, compactPaths = false) {
|
|
13
|
+
const url = text.trim();
|
|
14
|
+
if (URL_PATTERN.test(url))
|
|
15
|
+
return { kind: "url", url, label: url.length <= 40 ? url : `${url.slice(0, 40)}…` };
|
|
16
|
+
const paths = skipPaths ? [] : pathsFromClipboard(text);
|
|
17
|
+
if (paths.length > 0)
|
|
18
|
+
return compactPaths ? preparePathPresentation(paths) : { kind: "paths", paths };
|
|
19
|
+
return { kind: "text", ...prepareTextPaste(text) };
|
|
20
|
+
}
|
|
21
|
+
function pathsFromClipboard(text) {
|
|
22
|
+
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
23
|
+
if (normalized.length === 0)
|
|
24
|
+
return [];
|
|
25
|
+
const paths = [];
|
|
26
|
+
for (const line of normalized.split("\n").filter(value => value.trim().length > 0)) {
|
|
27
|
+
const wholeLine = existingClipboardPath(line);
|
|
28
|
+
if (wholeLine !== null) {
|
|
29
|
+
paths.push(wholeLine);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const tokens = tokenizePathLine(line)?.filter(token => token.quoted || token.text !== "&");
|
|
33
|
+
if (tokens === undefined || tokens.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
for (let index = 0; index < tokens.length;) {
|
|
36
|
+
const token = tokens[index];
|
|
37
|
+
if (token === undefined)
|
|
38
|
+
return [];
|
|
39
|
+
if (token.quoted) {
|
|
40
|
+
const quoted = existingClipboardPath(token.text);
|
|
41
|
+
if (quoted === null)
|
|
42
|
+
return [];
|
|
43
|
+
paths.push(quoted);
|
|
44
|
+
index++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (normalizePath(token.text) === null)
|
|
48
|
+
return [];
|
|
49
|
+
let runEnd = index;
|
|
50
|
+
while (runEnd < tokens.length && tokens[runEnd]?.quoted !== true)
|
|
51
|
+
runEnd++;
|
|
52
|
+
let matched = null;
|
|
53
|
+
let matchedEnd = index;
|
|
54
|
+
for (let end = runEnd; end > index; end--) {
|
|
55
|
+
matched = existingClipboardPath(tokens.slice(index, end).map(candidate => candidate.text).join(" "));
|
|
56
|
+
if (matched !== null) {
|
|
57
|
+
matchedEnd = end;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (matched === null)
|
|
62
|
+
return [];
|
|
63
|
+
paths.push(matched);
|
|
64
|
+
index = matchedEnd;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return paths;
|
|
68
|
+
}
|
|
69
|
+
function existingClipboardPath(value) {
|
|
70
|
+
const fullPath = normalizePath(unquote(value.trim()));
|
|
71
|
+
if (fullPath === null || !existsSync(fullPath))
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
const stat = statSync(fullPath);
|
|
75
|
+
if (stat.isDirectory())
|
|
76
|
+
return { fullPath, kind: "folder" };
|
|
77
|
+
if (stat.isFile())
|
|
78
|
+
return { fullPath, kind: "file" };
|
|
79
|
+
}
|
|
80
|
+
catch { /* Compatibility: unavailable paths remain ordinary text. */ }
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function tokenizePathLine(line) {
|
|
84
|
+
const tokens = [];
|
|
85
|
+
let index = 0;
|
|
86
|
+
while (index < line.length) {
|
|
87
|
+
while (index < line.length && /\s/u.test(line[index] ?? ""))
|
|
88
|
+
index++;
|
|
89
|
+
if (index >= line.length)
|
|
90
|
+
break;
|
|
91
|
+
const quote = line[index] === '"' || line[index] === "'" ? line[index] : undefined;
|
|
92
|
+
if (quote !== undefined) {
|
|
93
|
+
index++;
|
|
94
|
+
const start = index;
|
|
95
|
+
while (index < line.length && line[index] !== quote)
|
|
96
|
+
index++;
|
|
97
|
+
if (index >= line.length)
|
|
98
|
+
return undefined;
|
|
99
|
+
tokens.push({ text: line.slice(start, index), quoted: true });
|
|
100
|
+
index++;
|
|
101
|
+
if (index < line.length && !/\s/u.test(line[index] ?? ""))
|
|
102
|
+
return undefined;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const start = index;
|
|
106
|
+
while (index < line.length && !/\s/u.test(line[index] ?? ""))
|
|
107
|
+
index++;
|
|
108
|
+
tokens.push({ text: line.slice(start, index), quoted: false });
|
|
109
|
+
}
|
|
110
|
+
return tokens;
|
|
111
|
+
}
|
|
112
|
+
function normalizePath(value) {
|
|
113
|
+
let candidate = value;
|
|
114
|
+
if (/^file:\/\//iu.test(candidate)) {
|
|
115
|
+
try {
|
|
116
|
+
candidate = fileURLToPath(candidate);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (process.platform === "win32") {
|
|
123
|
+
const msys = /^\/([A-Za-z])\/(.*)$/u.exec(candidate);
|
|
124
|
+
if (msys !== null)
|
|
125
|
+
candidate = `${msys[1]}:\\${msys[2]?.replaceAll("/", "\\") ?? ""}`;
|
|
126
|
+
}
|
|
127
|
+
return /^(?:[A-Za-z]:\\|\/)/u.test(candidate) ? path.normalize(candidate) : null;
|
|
128
|
+
}
|
|
129
|
+
function unquote(value) {
|
|
130
|
+
let result = value.startsWith("& ") ? value.slice(2).trim() : value;
|
|
131
|
+
if ((result.startsWith('"') && result.endsWith('"')) || (result.startsWith("'") && result.endsWith("'")))
|
|
132
|
+
result = result.slice(1, -1).trim();
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { preparePasteText } from "./paste-text-preparation.js";
|
|
3
|
+
// Concurrency: the containing paste process can exit even if a filesystem probe blocks this worker.
|
|
4
|
+
try {
|
|
5
|
+
parentPort?.postMessage({ ok: true, value: preparePasteText(workerData, false, true) });
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
parentPort?.postMessage({ ok: false });
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
parentPort?.close();
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ClipboardPath, PreparedPasteText } from "./paste-text-preparation.js";
|
|
2
|
+
export declare const PATH_CHIP_PRESENTATION_UNITS = 4096;
|
|
3
|
+
export declare const PATH_CHIP_SUFFIX_UNITS = 20;
|
|
4
|
+
/** Shared base tag for presentation budgeting and actual chip adoption, including image-file spacing. */
|
|
5
|
+
export declare function pathChipTag(item: ClipboardPath): string;
|
|
6
|
+
/** Run in the isolated preparer: compact before path arrays become an oversized editor presentation. */
|
|
7
|
+
export declare function preparePathPresentation(paths: readonly ClipboardPath[]): PreparedPasteText;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export const PATH_CHIP_PRESENTATION_UNITS = 4096;
|
|
3
|
+
// Invariant: a collision suffix containing any safe-integer index fits this per-occurrence reserve.
|
|
4
|
+
export const PATH_CHIP_SUFFIX_UNITS = 20;
|
|
5
|
+
const IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|bmp|tiff?)$/iu;
|
|
6
|
+
/** Shared base tag for presentation budgeting and actual chip adoption, including image-file spacing. */
|
|
7
|
+
export function pathChipTag(item) {
|
|
8
|
+
const label = path.basename(item.fullPath) || path.parse(item.fullPath).root.replace(/[\\/]+$/u, "") || item.fullPath;
|
|
9
|
+
const icon = item.kind === "folder" ? "📁" : IMAGE_EXTENSION.test(item.fullPath) ? "🖼 " : "📄";
|
|
10
|
+
return `[${icon} ${label}]`;
|
|
11
|
+
}
|
|
12
|
+
/** Run in the isolated preparer: compact before path arrays become an oversized editor presentation. */
|
|
13
|
+
export function preparePathPresentation(paths) {
|
|
14
|
+
let units = 0;
|
|
15
|
+
for (const item of paths) {
|
|
16
|
+
units += pathChipTag(item).length + PATH_CHIP_SUFFIX_UNITS;
|
|
17
|
+
if (units > PATH_CHIP_PRESENTATION_UNITS) {
|
|
18
|
+
// Compatibility: individual adjacent chips expand without inserted separators or text re-normalization.
|
|
19
|
+
const text = paths.map(value => value.fullPath).join("");
|
|
20
|
+
return { kind: "text", text, label: `${text.length} chars` };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { kind: "paths", paths };
|
|
24
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PiShellClipboardContent, PiShellEditorTextRange } from "../components/index.js";
|
|
2
|
+
import type { PasteEvent, PasteSource } from "./paste-protocol.js";
|
|
2
3
|
export interface PromptImageAttachment {
|
|
3
4
|
readonly type: "image";
|
|
4
5
|
readonly data: string;
|
|
@@ -11,9 +12,15 @@ export interface PreparedPrompt {
|
|
|
11
12
|
/** Owns semantic chips and bounded pending paste references; cancels background work on reset or disposal. */
|
|
12
13
|
export declare class PromptChipStore {
|
|
13
14
|
#private;
|
|
14
|
-
|
|
15
|
+
constructor(options?: {
|
|
16
|
+
readonly isolated?: boolean;
|
|
17
|
+
readonly onEvent?: (event: PasteEvent) => void;
|
|
18
|
+
});
|
|
19
|
+
beginPaste(currentText: string, read: ((signal: AbortSignal) => Promise<PiShellClipboardContent | null>) | PasteSource, onError: (error: unknown) => void, onImage?: () => void): {
|
|
15
20
|
marker: string;
|
|
16
21
|
result: Promise<string>;
|
|
22
|
+
complete?(): void;
|
|
23
|
+
isCurrent?(): boolean;
|
|
17
24
|
};
|
|
18
25
|
hasPending(text: string): boolean;
|
|
19
26
|
waitForPastes(text: string, signal: AbortSignal, readDraft?: () => string): Promise<PreparedPrompt>;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync, statSync } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
3
|
import { canonicalizeClipboardImage } from "./clipboard-image.js";
|
|
6
4
|
import { assertImageEncodedSize, assertPromptImages, ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
|
|
7
5
|
import { ImagePreparationClient } from "./image-preparation-client.js";
|
|
8
|
-
import {
|
|
6
|
+
import { preparePasteText } from "./paste-text-preparation.js";
|
|
7
|
+
import { pathChipTag } from "./path-chip-presentation.js";
|
|
8
|
+
import { PastePreparationClient } from "./paste-preparation-client.js";
|
|
9
9
|
const CHIP_PATTERN = /\[(?:paste #\d+ (?:\+\d+ lines|\d+ chars)|📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
|
|
10
|
-
const IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|bmp|tiff?)$/iu;
|
|
11
|
-
const URL_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f]+$/iu;
|
|
12
10
|
const URL_SUBSTRING_PATTERN = /https?:\/\/[^\s\u0000-\u001f\u007f\]]+/giu;
|
|
13
11
|
const IMAGE_CHIP_IDENTIFIER_PATTERN = /^\[📷 screenshot-([a-f0-9]+)(?:-resized)?\]$/u;
|
|
14
12
|
const URL_DISPLAY_LENGTH = 40;
|
|
@@ -24,7 +22,17 @@ export class PromptChipStore {
|
|
|
24
22
|
#pending = new Map();
|
|
25
23
|
#preparation = new ImagePreparationClient();
|
|
26
24
|
#stopping = new Set();
|
|
25
|
+
#isolated;
|
|
26
|
+
#provisionalOwners = new Map();
|
|
27
|
+
#ownedChipTags = new Map();
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.#isolated = options.isolated ? new PastePreparationClient(options.onEvent) : undefined;
|
|
30
|
+
}
|
|
27
31
|
beginPaste(currentText, read, onError, onImage = () => { }) {
|
|
32
|
+
if (this.#isolated)
|
|
33
|
+
return this.#beginIsolatedPaste(currentText, typeof read === "function" ? { kind: "provided", read } : read, onError, onImage);
|
|
34
|
+
if (typeof read !== "function")
|
|
35
|
+
throw new Error("Clipboard sources require isolated paste preparation");
|
|
28
36
|
const id = randomBytes(5).toString("hex");
|
|
29
37
|
// Invariant: reserve paste identity immediately, but only display a screenshot after identifying an image.
|
|
30
38
|
const marker = `[📷 screenshot-${id}]`;
|
|
@@ -57,6 +65,56 @@ export class PromptChipStore {
|
|
|
57
65
|
this.#pending.set(marker, entry);
|
|
58
66
|
return { marker, result: entry.completion };
|
|
59
67
|
}
|
|
68
|
+
#beginIsolatedPaste(currentText, source, onError, onImage) {
|
|
69
|
+
const id = randomBytes(5).toString("hex");
|
|
70
|
+
const marker = `[📷 screenshot-${id}]`;
|
|
71
|
+
let entry;
|
|
72
|
+
const owner = Symbol("paste");
|
|
73
|
+
this.#ownedChipTags.set(owner, new Set());
|
|
74
|
+
const job = this.#isolated.start(source, async (value, signal) => {
|
|
75
|
+
if (signal.aborted)
|
|
76
|
+
throw new ImageAttachmentError("image-canceled");
|
|
77
|
+
if (value === null)
|
|
78
|
+
return "";
|
|
79
|
+
if (value.kind === "image")
|
|
80
|
+
return this.#addPreparedImage(value, id, owner);
|
|
81
|
+
entry.kind = "text";
|
|
82
|
+
return this.#adoptPreparedText(value, signal, owner);
|
|
83
|
+
}, () => {
|
|
84
|
+
entry.kind = "image";
|
|
85
|
+
onImage();
|
|
86
|
+
if (this.#imageCount(currentText) >= 8)
|
|
87
|
+
throw new ImageAttachmentError("image-count");
|
|
88
|
+
}, () => { entry.kind = "text"; });
|
|
89
|
+
entry = { marker, job, references: 0, kind: "unknown", completion: job.result.catch(error => {
|
|
90
|
+
this.#finishChipOwnership(owner, false);
|
|
91
|
+
entry.error = error instanceof ImageAttachmentError ? error : new ImageAttachmentError("paste-unavailable");
|
|
92
|
+
// Compatibility: typed image validation can fail before transfer identifies the payload to this store.
|
|
93
|
+
if (entry.kind === "unknown" && entry.error.code.startsWith("image-") && entry.error.code !== "image-canceled")
|
|
94
|
+
entry.kind = "image";
|
|
95
|
+
if (entry.error.code !== "image-canceled" && entry.references === 0)
|
|
96
|
+
onError(entry.error);
|
|
97
|
+
return entry.kind === "image" ? marker.replace("screenshot-", "failed-") : "";
|
|
98
|
+
}).then(replacement => { entry.replacement = replacement; return replacement; }) };
|
|
99
|
+
entry.onComplete = () => {
|
|
100
|
+
this.#finishChipOwnership(owner, job.isCurrent() && entry.error === undefined);
|
|
101
|
+
job.complete();
|
|
102
|
+
};
|
|
103
|
+
this.#pending.set(marker, entry);
|
|
104
|
+
return { marker, result: entry.completion, complete: entry.onComplete, isCurrent: () => job.isCurrent() };
|
|
105
|
+
}
|
|
106
|
+
async #adoptPreparedText(value, signal, owner) {
|
|
107
|
+
if (value.kind !== "paths")
|
|
108
|
+
return this.#recordPreparedText(value, owner);
|
|
109
|
+
let text = "";
|
|
110
|
+
for (let start = 0; start < value.paths.length; start += 32) {
|
|
111
|
+
if (signal.aborted)
|
|
112
|
+
throw new ImageAttachmentError("image-canceled");
|
|
113
|
+
text += this.#recordPreparedText({ kind: "paths", paths: value.paths.slice(start, start + 32) }, owner);
|
|
114
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
115
|
+
}
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
60
118
|
hasPending(text) {
|
|
61
119
|
return [...this.#pending.values()].some(item => text.includes(item.marker) && item.replacement === undefined);
|
|
62
120
|
}
|
|
@@ -82,6 +140,8 @@ export class PromptChipStore {
|
|
|
82
140
|
finally {
|
|
83
141
|
for (const entry of entries) {
|
|
84
142
|
entry.references--;
|
|
143
|
+
if (entry.replacement !== undefined)
|
|
144
|
+
entry.onComplete?.();
|
|
85
145
|
if (signal.aborted && entry.references === 0 && !readDraft().includes(entry.marker))
|
|
86
146
|
entry.job.cancel();
|
|
87
147
|
}
|
|
@@ -89,8 +149,13 @@ export class PromptChipStore {
|
|
|
89
149
|
}
|
|
90
150
|
reconcileDraft(text) {
|
|
91
151
|
for (const entry of this.#pending.values()) {
|
|
92
|
-
if (entry.references === 0 && entry.
|
|
152
|
+
if (entry.references === 0 && !text.includes(entry.marker) && !text.includes(entry.marker.replace("screenshot-", "failed-"))) {
|
|
93
153
|
entry.job.cancel();
|
|
154
|
+
if (entry.replacement !== undefined) {
|
|
155
|
+
entry.onComplete?.();
|
|
156
|
+
this.#pending.delete(entry.marker);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
94
159
|
}
|
|
95
160
|
}
|
|
96
161
|
resetPastes(text) {
|
|
@@ -98,6 +163,7 @@ export class PromptChipStore {
|
|
|
98
163
|
for (const entry of this.#pending.values())
|
|
99
164
|
if (entry.replacement === undefined)
|
|
100
165
|
remaining = remaining.replaceAll(entry.marker, "");
|
|
166
|
+
this.#isolated?.reset();
|
|
101
167
|
const stopped = this.#preparation.dispose();
|
|
102
168
|
this.#stopping.add(stopped);
|
|
103
169
|
void stopped.finally(() => this.#stopping.delete(stopped));
|
|
@@ -108,9 +174,11 @@ export class PromptChipStore {
|
|
|
108
174
|
return remaining;
|
|
109
175
|
}
|
|
110
176
|
async dispose() {
|
|
111
|
-
await Promise.all([this.#preparation.dispose(), ...this.#stopping]);
|
|
177
|
+
await Promise.all([this.#preparation.dispose(), this.#isolated?.dispose(), ...this.#stopping]);
|
|
112
178
|
this.#chips.clear();
|
|
113
179
|
this.#pending.clear();
|
|
180
|
+
this.#provisionalOwners.clear();
|
|
181
|
+
this.#ownedChipTags.clear();
|
|
114
182
|
}
|
|
115
183
|
#imageCount(text) {
|
|
116
184
|
const tags = new Set([...this.#chips.values()].filter(chip => chip.kind === "image" && text.includes(chip.tag)).map(chip => chip.tag));
|
|
@@ -122,11 +190,12 @@ export class PromptChipStore {
|
|
|
122
190
|
}
|
|
123
191
|
return tags.size;
|
|
124
192
|
}
|
|
125
|
-
#addPreparedImage(image, id) {
|
|
193
|
+
#addPreparedImage(image, id, owner) {
|
|
126
194
|
const tag = `[📷 screenshot-${id}${image.transformed ? "-resized" : ""}]`;
|
|
127
195
|
const attachment = Object.freeze({ type: "image", data: image.data, mimeType: image.mimeType });
|
|
128
196
|
assertPromptImages([attachment]);
|
|
129
197
|
this.#chips.set(tag, { kind: "image", tag, image: attachment });
|
|
198
|
+
this.#claimChip(tag, owner, true);
|
|
130
199
|
return tag;
|
|
131
200
|
}
|
|
132
201
|
transformPastedContent(content, currentText = "") {
|
|
@@ -146,30 +215,30 @@ export class PromptChipStore {
|
|
|
146
215
|
return tag;
|
|
147
216
|
}
|
|
148
217
|
const text = content.text;
|
|
149
|
-
if (this.#chips.has(text.trim()))
|
|
218
|
+
if (this.#chips.has(text.trim())) {
|
|
219
|
+
this.#claimChip(text.trim(), undefined, false);
|
|
150
220
|
return text.trim();
|
|
151
|
-
const url = text.trim();
|
|
152
|
-
if (URL_PATTERN.test(url)) {
|
|
153
|
-
const label = url.length <= URL_DISPLAY_LENGTH ? url : `${url.slice(0, URL_DISPLAY_LENGTH)}…`;
|
|
154
|
-
return this.#recordUnique({ kind: "url", tag: `[🔗 ${label}]`, label, url });
|
|
155
221
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
222
|
+
return this.#recordPreparedText(preparePasteText(text));
|
|
223
|
+
}
|
|
224
|
+
#recordPreparedText(paste, owner) {
|
|
225
|
+
if (paste.kind === "url")
|
|
226
|
+
return this.#recordUnique({ kind: "url", tag: `[🔗 ${paste.label}]`, label: paste.label, url: paste.url }, owner);
|
|
227
|
+
if (paste.kind === "text") {
|
|
228
|
+
if (paste.label === undefined) {
|
|
229
|
+
const tag = paste.text.trim();
|
|
230
|
+
if (this.#chips.has(tag)) {
|
|
231
|
+
this.#claimChip(tag, owner, false);
|
|
232
|
+
return tag;
|
|
233
|
+
}
|
|
160
234
|
return paste.text;
|
|
235
|
+
}
|
|
161
236
|
const tag = `[paste #${++this.#textCounter} ${paste.label}]`;
|
|
162
237
|
this.#chips.set(tag, Object.freeze({ kind: "text", tag, text: paste.text }));
|
|
238
|
+
this.#claimChip(tag, owner, true);
|
|
163
239
|
return tag;
|
|
164
240
|
}
|
|
165
|
-
return paths.map(item => {
|
|
166
|
-
const label = pathLabel(item.fullPath);
|
|
167
|
-
if (item.kind === "folder") {
|
|
168
|
-
return this.#recordUnique({ kind: "folder", tag: `[📁 ${label}]`, path: item.fullPath });
|
|
169
|
-
}
|
|
170
|
-
const icon = IMAGE_EXTENSION.test(item.fullPath) ? "🖼 " : "📄";
|
|
171
|
-
return this.#recordUnique({ kind: "file", tag: `[${icon} ${label}]`, path: item.fullPath });
|
|
172
|
-
}).join("");
|
|
241
|
+
return paste.paths.map(item => this.#recordUnique({ kind: item.kind, tag: pathChipTag(item), path: item.fullPath }, owner)).join("");
|
|
173
242
|
}
|
|
174
243
|
/** Hide provisional clipboard identities until the read identifies an actual image. */
|
|
175
244
|
hiddenRanges(line) {
|
|
@@ -373,142 +442,53 @@ export class PromptChipStore {
|
|
|
373
442
|
});
|
|
374
443
|
return { text: expanded, images };
|
|
375
444
|
}
|
|
376
|
-
#
|
|
445
|
+
#claimChip(tag, owner, created) {
|
|
446
|
+
if (owner === undefined) {
|
|
447
|
+
this.#provisionalOwners.delete(tag);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
let owners = this.#provisionalOwners.get(tag);
|
|
451
|
+
if (!owners && !created)
|
|
452
|
+
return; // Invariant: existing committed chips must survive canceled later pastes.
|
|
453
|
+
if (!owners) {
|
|
454
|
+
owners = new Set();
|
|
455
|
+
this.#provisionalOwners.set(tag, owners);
|
|
456
|
+
}
|
|
457
|
+
owners.add(owner);
|
|
458
|
+
this.#ownedChipTags.get(owner)?.add(tag);
|
|
459
|
+
}
|
|
460
|
+
#finishChipOwnership(owner, commit) {
|
|
461
|
+
for (const tag of this.#ownedChipTags.get(owner) ?? []) {
|
|
462
|
+
const owners = this.#provisionalOwners.get(tag);
|
|
463
|
+
if (!owners?.has(owner))
|
|
464
|
+
continue;
|
|
465
|
+
if (commit)
|
|
466
|
+
this.#provisionalOwners.delete(tag);
|
|
467
|
+
else {
|
|
468
|
+
owners.delete(owner);
|
|
469
|
+
if (owners.size === 0) {
|
|
470
|
+
this.#provisionalOwners.delete(tag);
|
|
471
|
+
this.#chips.delete(tag);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
this.#ownedChipTags.delete(owner);
|
|
476
|
+
}
|
|
477
|
+
#recordUnique(chip, owner) {
|
|
377
478
|
const existing = this.#chips.get(chip.tag);
|
|
378
479
|
if (existing === undefined || sameChipValue(existing, chip)) {
|
|
379
480
|
this.#chips.set(chip.tag, chip);
|
|
481
|
+
this.#claimChip(chip.tag, owner, existing === undefined);
|
|
380
482
|
return chip.tag;
|
|
381
483
|
}
|
|
382
484
|
const suffix = randomBytes(2).toString("hex");
|
|
383
485
|
const uniqueTag = `${chip.tag.slice(0, -1)} #${suffix}]`;
|
|
384
486
|
const unique = { ...chip, tag: uniqueTag };
|
|
385
487
|
this.#chips.set(uniqueTag, unique);
|
|
488
|
+
this.#claimChip(uniqueTag, owner, true);
|
|
386
489
|
return uniqueTag;
|
|
387
490
|
}
|
|
388
491
|
}
|
|
389
|
-
function pathsFromClipboard(text) {
|
|
390
|
-
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
391
|
-
if (normalized.length === 0)
|
|
392
|
-
return [];
|
|
393
|
-
const paths = [];
|
|
394
|
-
for (const line of normalized.split("\n").filter(value => value.trim().length > 0)) {
|
|
395
|
-
const wholeLine = existingClipboardPath(line);
|
|
396
|
-
if (wholeLine !== null) {
|
|
397
|
-
paths.push(wholeLine);
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
const tokens = tokenizePathLine(line)?.filter(token => token.quoted || token.text !== "&");
|
|
401
|
-
if (tokens === undefined || tokens.length === 0)
|
|
402
|
-
return [];
|
|
403
|
-
for (let index = 0; index < tokens.length;) {
|
|
404
|
-
const token = tokens[index];
|
|
405
|
-
if (token === undefined)
|
|
406
|
-
return [];
|
|
407
|
-
if (token.quoted) {
|
|
408
|
-
const quoted = existingClipboardPath(token.text);
|
|
409
|
-
if (quoted === null)
|
|
410
|
-
return [];
|
|
411
|
-
paths.push(quoted);
|
|
412
|
-
index += 1;
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
if (normalizePath(token.text) === null)
|
|
416
|
-
return [];
|
|
417
|
-
let runEnd = index;
|
|
418
|
-
while (runEnd < tokens.length && tokens[runEnd]?.quoted !== true)
|
|
419
|
-
runEnd += 1;
|
|
420
|
-
let matched = null;
|
|
421
|
-
let matchedEnd = index;
|
|
422
|
-
for (let end = runEnd; end > index; end -= 1) {
|
|
423
|
-
matched = existingClipboardPath(tokens.slice(index, end).map(candidate => candidate.text).join(" "));
|
|
424
|
-
if (matched !== null) {
|
|
425
|
-
matchedEnd = end;
|
|
426
|
-
break;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (matched === null)
|
|
430
|
-
return [];
|
|
431
|
-
paths.push(matched);
|
|
432
|
-
index = matchedEnd;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return paths;
|
|
436
|
-
}
|
|
437
|
-
function existingClipboardPath(value) {
|
|
438
|
-
const fullPath = normalizePath(unquote(value.trim()));
|
|
439
|
-
if (fullPath === null || !existsSync(fullPath))
|
|
440
|
-
return null;
|
|
441
|
-
try {
|
|
442
|
-
const stat = statSync(fullPath);
|
|
443
|
-
if (stat.isDirectory())
|
|
444
|
-
return { fullPath, kind: "folder" };
|
|
445
|
-
if (stat.isFile())
|
|
446
|
-
return { fullPath, kind: "file" };
|
|
447
|
-
}
|
|
448
|
-
catch {
|
|
449
|
-
// Compatibility: clipboard path probing is best-effort.
|
|
450
|
-
}
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
function tokenizePathLine(line) {
|
|
454
|
-
const tokens = [];
|
|
455
|
-
let index = 0;
|
|
456
|
-
while (index < line.length) {
|
|
457
|
-
while (index < line.length && /\s/u.test(line[index] ?? ""))
|
|
458
|
-
index += 1;
|
|
459
|
-
if (index >= line.length)
|
|
460
|
-
break;
|
|
461
|
-
const quote = line[index] === '"' || line[index] === "'" ? line[index] : undefined;
|
|
462
|
-
if (quote !== undefined) {
|
|
463
|
-
index += 1;
|
|
464
|
-
const start = index;
|
|
465
|
-
while (index < line.length && line[index] !== quote)
|
|
466
|
-
index += 1;
|
|
467
|
-
if (index >= line.length)
|
|
468
|
-
return undefined;
|
|
469
|
-
tokens.push({ text: line.slice(start, index), quoted: true });
|
|
470
|
-
index += 1;
|
|
471
|
-
if (index < line.length && !/\s/u.test(line[index] ?? ""))
|
|
472
|
-
return undefined;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
const start = index;
|
|
476
|
-
while (index < line.length && !/\s/u.test(line[index] ?? ""))
|
|
477
|
-
index += 1;
|
|
478
|
-
tokens.push({ text: line.slice(start, index), quoted: false });
|
|
479
|
-
}
|
|
480
|
-
return tokens;
|
|
481
|
-
}
|
|
482
|
-
function normalizePath(value) {
|
|
483
|
-
let candidate = value;
|
|
484
|
-
if (/^file:\/\//iu.test(candidate)) {
|
|
485
|
-
try {
|
|
486
|
-
candidate = fileURLToPath(candidate);
|
|
487
|
-
}
|
|
488
|
-
catch {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (process.platform === "win32") {
|
|
493
|
-
const msys = /^\/([A-Za-z])\/(.*)$/u.exec(candidate);
|
|
494
|
-
if (msys !== null)
|
|
495
|
-
candidate = `${msys[1]}:\\${msys[2]?.replaceAll("/", "\\") ?? ""}`;
|
|
496
|
-
}
|
|
497
|
-
return /^(?:[A-Za-z]:\\|\/)/u.test(candidate) ? path.normalize(candidate) : null;
|
|
498
|
-
}
|
|
499
|
-
function unquote(value) {
|
|
500
|
-
let result = value.startsWith("& ") ? value.slice(2).trim() : value;
|
|
501
|
-
if ((result.startsWith('"') && result.endsWith('"')) || (result.startsWith("'") && result.endsWith("'"))) {
|
|
502
|
-
result = result.slice(1, -1).trim();
|
|
503
|
-
}
|
|
504
|
-
return result;
|
|
505
|
-
}
|
|
506
|
-
function pathLabel(fullPath) {
|
|
507
|
-
const basename = path.basename(fullPath);
|
|
508
|
-
if (basename.length > 0)
|
|
509
|
-
return basename;
|
|
510
|
-
return path.parse(fullPath).root.replace(/[\\/]+$/u, "") || fullPath;
|
|
511
|
-
}
|
|
512
492
|
function sameChipValue(left, right) {
|
|
513
493
|
if (left.kind !== right.kind)
|
|
514
494
|
return false;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type SelectionCopySnapshot } from "../../../ui/components/index.js";
|
|
2
|
+
import { type CopyResult, type ResponseCopyEvent } from "./response-copy-protocol.js";
|
|
3
|
+
import type { ResponseCopyExecutor } from "./response-copy-transport.js";
|
|
4
|
+
/** Owns one delivery and one newest pending copy, including expiry and side-effect fencing. */
|
|
5
|
+
export declare class ResponseCopyCoordinator {
|
|
6
|
+
#private;
|
|
7
|
+
private readonly options;
|
|
8
|
+
constructor(options: {
|
|
9
|
+
readonly execute: ResponseCopyExecutor;
|
|
10
|
+
readonly onEvent?: (event: ResponseCopyEvent) => void;
|
|
11
|
+
readonly onFailure?: (result: CopyResult) => void;
|
|
12
|
+
readonly now?: () => number;
|
|
13
|
+
});
|
|
14
|
+
get busy(): boolean;
|
|
15
|
+
get latest(): Promise<CopyResult>;
|
|
16
|
+
/** Exact prompt copy/cut shares the write lifetime without transcript text transformations. */
|
|
17
|
+
submitText(text: string): Promise<CopyResult>;
|
|
18
|
+
/** Admission is synchronous, but preparation/startup never runs in the input handler's stack. */
|
|
19
|
+
submit(snapshot: SelectionCopySnapshot, before?: Promise<void>): Promise<CopyResult>;
|
|
20
|
+
/** Capture the current copy barrier, without making later copies retroactively delay this paste. */
|
|
21
|
+
canPaste(signal: AbortSignal): Promise<boolean>;
|
|
22
|
+
/** Only a pending write is a prerequisite; a completed failure cannot poison independent future reads. */
|
|
23
|
+
capturePasteBarrier(): (signal: AbortSignal) => Promise<boolean>;
|
|
24
|
+
/** Session replacement fences the old executor rather than discarding its live side effects. */
|
|
25
|
+
reset(): void;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|