@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.
Files changed (87) hide show
  1. package/dist/composition/owned-ui.d.ts +2 -0
  2. package/dist/composition/owned-ui.js +28 -11
  3. package/dist/contracts/owned-ui/image-attachments.d.ts +7 -0
  4. package/dist/contracts/owned-ui/image-attachments.js +7 -0
  5. package/dist/foundation/release/dependency-certification.js +65 -7
  6. package/dist/integrations/pi/components/owned-editor-ux.d.ts +8 -6
  7. package/dist/integrations/pi/components/owned-editor-ux.js +115 -26
  8. package/dist/integrations/pi/components/shell-editor-autocomplete.js +4 -0
  9. package/dist/integrations/pi/components/shell-presenters-transcript.js +15 -5
  10. package/dist/integrations/pi/components/shell-shared-facade.d.ts +14 -5
  11. package/dist/integrations/pi/components/tool-image-presentation.d.ts +21 -0
  12. package/dist/integrations/pi/components/tool-image-presentation.js +86 -0
  13. package/dist/integrations/pi/components/tool-result-adapter.d.ts +2 -2
  14. package/dist/integrations/pi/components/transcript-image-resolver.d.ts +7 -0
  15. package/dist/integrations/pi/components/transcript-image-resolver.js +19 -0
  16. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  17. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -1
  18. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +61 -0
  19. package/dist/integrations/pi/components/upstream/components/tool-execution.js +344 -0
  20. package/dist/integrations/pi/engine/adapter.d.ts +4 -1
  21. package/dist/integrations/pi/engine/adapter.js +66 -28
  22. package/dist/integrations/pi/engine/pending-delivery.d.ts +4 -0
  23. package/dist/integrations/pi/engine/pending-delivery.js +15 -6
  24. package/dist/integrations/pi/engine/tool-rendering.js +26 -3
  25. package/dist/integrations/pi/engine/transcript-image-assets.d.ts +11 -0
  26. package/dist/integrations/pi/engine/transcript-image-assets.js +86 -0
  27. package/dist/integrations/pi/session-ui/clipboard-diagnostics.d.ts +16 -0
  28. package/dist/integrations/pi/session-ui/clipboard-diagnostics.js +65 -0
  29. package/dist/integrations/pi/session-ui/editor-hyperlink-budget.d.ts +9 -0
  30. package/dist/integrations/pi/session-ui/editor-hyperlink-budget.js +23 -0
  31. package/dist/integrations/pi/session-ui/index.d.ts +1 -0
  32. package/dist/integrations/pi/session-ui/index.js +1 -0
  33. package/dist/integrations/pi/session-ui/paste-executor.d.ts +10 -0
  34. package/dist/integrations/pi/session-ui/paste-executor.js +232 -0
  35. package/dist/integrations/pi/session-ui/paste-helper.d.ts +1 -0
  36. package/dist/integrations/pi/session-ui/paste-helper.js +173 -0
  37. package/dist/integrations/pi/session-ui/paste-preparation-client.d.ts +16 -0
  38. package/dist/integrations/pi/session-ui/paste-preparation-client.js +159 -0
  39. package/dist/integrations/pi/session-ui/paste-protocol.d.ts +78 -0
  40. package/dist/integrations/pi/session-ui/paste-protocol.js +18 -0
  41. package/dist/integrations/pi/session-ui/paste-text-preparation.d.ts +20 -0
  42. package/dist/integrations/pi/session-ui/paste-text-preparation.js +134 -0
  43. package/dist/integrations/pi/session-ui/paste-text-worker.d.ts +1 -0
  44. package/dist/integrations/pi/session-ui/paste-text-worker.js +12 -0
  45. package/dist/integrations/pi/session-ui/path-chip-presentation.d.ts +7 -0
  46. package/dist/integrations/pi/session-ui/path-chip-presentation.js +24 -0
  47. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +8 -1
  48. package/dist/integrations/pi/session-ui/prompt-chips.js +129 -149
  49. package/dist/integrations/pi/session-ui/response-copy-coordinator.d.ts +27 -0
  50. package/dist/integrations/pi/session-ui/response-copy-coordinator.js +189 -0
  51. package/dist/integrations/pi/session-ui/response-copy-helper.d.ts +1 -0
  52. package/dist/integrations/pi/session-ui/response-copy-helper.js +122 -0
  53. package/dist/integrations/pi/session-ui/response-copy-protocol.d.ts +56 -0
  54. package/dist/integrations/pi/session-ui/response-copy-protocol.js +5 -0
  55. package/dist/integrations/pi/session-ui/response-copy-transport.d.ts +31 -0
  56. package/dist/integrations/pi/session-ui/response-copy-transport.js +187 -0
  57. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +15 -6
  58. package/dist/integrations/pi/session-ui/session-shell-root.js +25 -6
  59. package/dist/integrations/pi/session-ui/session-shell.js +100 -28
  60. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
  61. package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -3
  62. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +2 -2
  63. package/dist/integrations/pi/session-ui/system-clipboard.js +26 -11
  64. package/dist/integrations/pi/tui-runtime/adapter.js +3 -1
  65. package/dist/native/darwin-arm64/manifest.json +1 -1
  66. package/dist/native/linux-x64/manifest.json +1 -1
  67. package/dist/native/win32-x64/manifest.json +2 -2
  68. package/dist/native/win32-x64/process-guardian.exe +0 -0
  69. package/dist/product-identity.d.ts +1 -1
  70. package/dist/product-identity.js +1 -1
  71. package/dist/product-identity.json +1 -0
  72. package/dist/ui/components/index.d.ts +1 -0
  73. package/dist/ui/components/index.js +1 -0
  74. package/dist/ui/components/selection-copy.d.ts +24 -0
  75. package/dist/ui/components/selection-copy.js +39 -0
  76. package/dist/ui/components/spans.d.ts +5 -2
  77. package/dist/ui/components/spans.js +12 -3
  78. package/dist/ui/components/text-selection.js +2 -0
  79. package/dist/ui/components/text.js +1 -1
  80. package/dist/ui/components/transcript-viewport.d.ts +5 -0
  81. package/dist/ui/components/transcript-viewport.js +100 -23
  82. package/dist/ui/components/visible-hyperlinks.js +40 -8
  83. package/docs/architecture/response-copy-delivery.md +83 -0
  84. package/docs/architecture/tool-image-presentation.md +62 -0
  85. package/docs/ci-release-runbook.md +19 -7
  86. package/docs/openspec-archive-automation.md +124 -0
  87. 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,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
- beginPaste(currentText: string, read: (signal: AbortSignal) => Promise<PiShellClipboardContent | null>, onError: (error: unknown) => void, onImage?: () => void): {
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 { prepareTextPaste } from "./text-paste.js";
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.replacement === undefined && !text.includes(entry.marker))
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
- const paths = pathsFromClipboard(text);
157
- if (paths.length === 0) {
158
- const paste = prepareTextPaste(text);
159
- if (paste.label === undefined)
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
- #recordUnique(chip) {
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
+ }