@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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
4
3
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -9,6 +8,7 @@ import { PRODUCT_IDENTITY } from "../../../product-identity.js";
|
|
|
9
8
|
import { configureOwnedHttpDispatcher } from "./http-dispatcher.js";
|
|
10
9
|
import { readPinnedCommandChangelog } from "./changelog.js";
|
|
11
10
|
import { toolRenderingInput } from "./tool-rendering.js";
|
|
11
|
+
import { TranscriptImageAssets } from "./transcript-image-assets.js";
|
|
12
12
|
import { copyToClipboard, CredentialSynchronizationError, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, acceptsTranscriptUpdate, transcriptToolState, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiPromptSuggestionRequest, assertOwnedUiPromptSuggestionResult, assertOwnedUiSnapshot, normalizePromptSuggestionCandidate, } from "../../../contracts/owned-ui/index.js";
|
|
14
14
|
import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
|
|
@@ -91,6 +91,7 @@ export class PiEngineAdapter {
|
|
|
91
91
|
#sessionSelection;
|
|
92
92
|
#sessionForkPrompt;
|
|
93
93
|
#workflowHost;
|
|
94
|
+
#clipboardWriter;
|
|
94
95
|
#workflowInteraction;
|
|
95
96
|
#listeners = new Map();
|
|
96
97
|
#runtime;
|
|
@@ -126,11 +127,11 @@ export class PiEngineAdapter {
|
|
|
126
127
|
#messageBlockIds = new WeakMap();
|
|
127
128
|
#messageFallbackIds = new Map();
|
|
128
129
|
#toolBlockIds = new Map();
|
|
129
|
-
#transcriptImageAssets = new
|
|
130
|
+
#transcriptImageAssets = new TranscriptImageAssets();
|
|
130
131
|
#usageCache;
|
|
131
132
|
#nextBlockSequence = 0;
|
|
132
133
|
#diagnostics = [];
|
|
133
|
-
#eventQueue = new PendingEngineDelivery();
|
|
134
|
+
#eventQueue = new PendingEngineDelivery(event => this.#transcriptImageAssets.retainEvent(event));
|
|
134
135
|
#eventQueueProcessing;
|
|
135
136
|
#overload;
|
|
136
137
|
#overloads = 0;
|
|
@@ -192,7 +193,7 @@ export class PiEngineAdapter {
|
|
|
192
193
|
return this.#agentDir;
|
|
193
194
|
}
|
|
194
195
|
resolveTranscriptImage(assetId) {
|
|
195
|
-
return this.#transcriptImageAssets.
|
|
196
|
+
return this.#transcriptImageAssets.resolve(assetId);
|
|
196
197
|
}
|
|
197
198
|
currentSessionFile() {
|
|
198
199
|
const value = this.#session?.sessionManager?.getSessionFile?.();
|
|
@@ -784,17 +785,25 @@ export class PiEngineAdapter {
|
|
|
784
785
|
if (!runtime)
|
|
785
786
|
throw new Error("engine runtime is unavailable");
|
|
786
787
|
const cwd = resolve(this.#cwd);
|
|
787
|
-
|
|
788
|
+
let trustPath = cwd;
|
|
789
|
+
try {
|
|
790
|
+
trustPath = realpathSync(cwd);
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
// Compatibility: pinned Pi retains the resolved path when real-path lookup fails.
|
|
794
|
+
}
|
|
795
|
+
// Security: trust the target's parent, not the parent containing a directory alias.
|
|
796
|
+
const parent = dirname(trustPath);
|
|
788
797
|
const trustStore = new ProjectTrustStore(this.#agentDir);
|
|
789
798
|
const trustOptions = [
|
|
790
|
-
{ label: "Trust", trusted: true, updates: [{ path:
|
|
791
|
-
...(parent ===
|
|
799
|
+
{ label: "Trust", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath },
|
|
800
|
+
...(parent === trustPath ? [] : [{
|
|
792
801
|
label: `Trust parent folder (${parent})`,
|
|
793
802
|
trusted: true,
|
|
794
|
-
updates: [{ path: parent, decision: true }, { path:
|
|
803
|
+
updates: [{ path: parent, decision: true }, { path: trustPath, decision: null }],
|
|
795
804
|
savedPath: parent,
|
|
796
805
|
}]),
|
|
797
|
-
{ label: "Do not trust", trusted: false, updates: [{ path:
|
|
806
|
+
{ label: "Do not trust", trusted: false, updates: [{ path: trustPath, decision: false }], savedPath: trustPath },
|
|
798
807
|
];
|
|
799
808
|
return {
|
|
800
809
|
cwd,
|
|
@@ -1019,8 +1028,19 @@ export class PiEngineAdapter {
|
|
|
1019
1028
|
pinnedToolDefinition(toolName) {
|
|
1020
1029
|
return this.#requireWorkflowSession().extensionRunner?.getToolDefinition?.(toolName);
|
|
1021
1030
|
}
|
|
1031
|
+
/** Bind the owned UI's clipboard lifecycle without changing the comparison host. True means acknowledged delivery. */
|
|
1032
|
+
bindClipboardWriter(writer) {
|
|
1033
|
+
const previous = this.#clipboardWriter;
|
|
1034
|
+
this.#clipboardWriter = writer;
|
|
1035
|
+
return () => { if (this.#clipboardWriter === writer)
|
|
1036
|
+
this.#clipboardWriter = previous; };
|
|
1037
|
+
}
|
|
1038
|
+
/** Workflow and tree copying share the active owner's write fence and delivery acknowledgment. */
|
|
1022
1039
|
async copyWorkflowText(text) {
|
|
1040
|
+
if (this.#clipboardWriter !== undefined)
|
|
1041
|
+
return this.#clipboardWriter(text);
|
|
1023
1042
|
await this.#workflowHost.copyText(text);
|
|
1043
|
+
return true;
|
|
1024
1044
|
}
|
|
1025
1045
|
clearQueuedWorkflows() {
|
|
1026
1046
|
const session = this.#requireWorkflowSession();
|
|
@@ -1422,8 +1442,8 @@ export class PiEngineAdapter {
|
|
|
1422
1442
|
const text = requireCapability(session.getLastAssistantText, "getLastAssistantText").call(session);
|
|
1423
1443
|
if (typeof text !== "string" || text.length === 0)
|
|
1424
1444
|
return workflowResult(request.command, "failed", "No agent messages to copy yet.");
|
|
1425
|
-
await this
|
|
1426
|
-
return workflowResult(request.command, "completed", "Copied last agent message to clipboard");
|
|
1445
|
+
const acknowledged = await this.copyWorkflowText(text);
|
|
1446
|
+
return workflowResult(request.command, "completed", acknowledged ? "Copied last agent message to clipboard" : "Submitted last agent message to clipboard");
|
|
1427
1447
|
}
|
|
1428
1448
|
case "name": {
|
|
1429
1449
|
const manager = session.sessionManager;
|
|
@@ -2056,15 +2076,20 @@ export class PiEngineAdapter {
|
|
|
2056
2076
|
}
|
|
2057
2077
|
// Performance: replacing the transcript rebuilds its index so block lookup stays constant-time.
|
|
2058
2078
|
#setTranscript(blocks) {
|
|
2079
|
+
// Invariant: lazy delivery snapshots must freeze before their authoritative source can be removed.
|
|
2080
|
+
if (!this.#eventQueue.seal()) {
|
|
2081
|
+
this.#beginOverload();
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
for (const block of blocks)
|
|
2085
|
+
this.#transcriptImageAssets.retain(block);
|
|
2086
|
+
for (const block of this.#transcript)
|
|
2087
|
+
this.#transcriptImageAssets.release(block);
|
|
2059
2088
|
this.#transcript = blocks;
|
|
2060
2089
|
this.#transcriptIndex.clear();
|
|
2061
2090
|
for (const [index, block] of blocks.entries())
|
|
2062
2091
|
this.#transcriptIndex.set(block.id, index);
|
|
2063
|
-
|
|
2064
|
-
for (const assetId of this.#transcriptImageAssets.keys()) {
|
|
2065
|
-
if (!retainedAssets.has(assetId))
|
|
2066
|
-
this.#transcriptImageAssets.delete(assetId);
|
|
2067
|
-
}
|
|
2092
|
+
this.#transcriptImageAssets.discardUnowned();
|
|
2068
2093
|
this.#transcriptSnapshot = undefined;
|
|
2069
2094
|
}
|
|
2070
2095
|
#transcriptBlock(id) {
|
|
@@ -2099,6 +2124,14 @@ export class PiEngineAdapter {
|
|
|
2099
2124
|
this.#emitEvent({ type: "status", status: this.#status });
|
|
2100
2125
|
}
|
|
2101
2126
|
#handlePiEvent(event) {
|
|
2127
|
+
try {
|
|
2128
|
+
this.#applyPiEvent(event);
|
|
2129
|
+
}
|
|
2130
|
+
finally {
|
|
2131
|
+
this.#transcriptImageAssets.discardUnowned();
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
#applyPiEvent(event) {
|
|
2102
2135
|
if (!isRecord(event) || typeof event.type !== "string")
|
|
2103
2136
|
return;
|
|
2104
2137
|
// Invariant: usage moves at message and lifecycle boundaries, not with stream chunks, so the
|
|
@@ -2558,16 +2591,11 @@ export class PiEngineAdapter {
|
|
|
2558
2591
|
return [];
|
|
2559
2592
|
const references = [];
|
|
2560
2593
|
for (const item of content) {
|
|
2561
|
-
if (references.length >= 16
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
if (byteLength < 1 || byteLength > 20 * 1024 * 1024)
|
|
2567
|
-
continue;
|
|
2568
|
-
const assetId = `image-${createHash("sha256").update(item.mimeType).update("\0").update(item.data).digest("hex").slice(0, 24)}`;
|
|
2569
|
-
this.#transcriptImageAssets.set(assetId, { type: "image", data: item.data, mimeType: item.mimeType });
|
|
2570
|
-
references.push({ assetId, mimeType: item.mimeType, byteLength, source });
|
|
2594
|
+
if (references.length >= 16)
|
|
2595
|
+
break;
|
|
2596
|
+
const reference = this.#transcriptImageAssets.reference(item, source);
|
|
2597
|
+
if (reference !== undefined)
|
|
2598
|
+
references.push(reference);
|
|
2571
2599
|
}
|
|
2572
2600
|
return references;
|
|
2573
2601
|
}
|
|
@@ -2613,9 +2641,13 @@ export class PiEngineAdapter {
|
|
|
2613
2641
|
// revision keeps the rows it already rendered.
|
|
2614
2642
|
if (existing !== undefined && sameBlockContent(existing, block))
|
|
2615
2643
|
return;
|
|
2644
|
+
this.#transcriptImageAssets.retain(block);
|
|
2645
|
+
if (existing !== undefined)
|
|
2646
|
+
this.#transcriptImageAssets.release(existing);
|
|
2616
2647
|
this.#transcript[index] = block;
|
|
2617
2648
|
}
|
|
2618
2649
|
else {
|
|
2650
|
+
this.#transcriptImageAssets.retain(block);
|
|
2619
2651
|
this.#transcriptIndex.set(block.id, this.#transcript.length);
|
|
2620
2652
|
this.#transcript.push(block);
|
|
2621
2653
|
}
|
|
@@ -2789,9 +2821,15 @@ export class PiEngineAdapter {
|
|
|
2789
2821
|
continue;
|
|
2790
2822
|
if (pending.generation !== this.#sessionGeneration && pending.event.type !== "command-outcome") {
|
|
2791
2823
|
this.#invalidatedEvents = Math.min(Number.MAX_SAFE_INTEGER, this.#invalidatedEvents + 1);
|
|
2824
|
+
pending.release?.();
|
|
2792
2825
|
continue;
|
|
2793
2826
|
}
|
|
2794
|
-
|
|
2827
|
+
try {
|
|
2828
|
+
this.#deliver(pending.event);
|
|
2829
|
+
}
|
|
2830
|
+
finally {
|
|
2831
|
+
pending.release?.();
|
|
2832
|
+
}
|
|
2795
2833
|
deliveredSinceYield += 1;
|
|
2796
2834
|
// Concurrency: a microtask chain runs to exhaustion before the loop turns, so a streaming
|
|
2797
2835
|
// burst would hold typed input, pointer reports, and timed indicators until it
|
|
@@ -6,6 +6,8 @@ export declare function replaceableEventKey(event: OwnedUiEvent): string | null;
|
|
|
6
6
|
/** Intrusive FIFO + segment index: replacement unlinks in O(1), never changes source ordering. */
|
|
7
7
|
export declare class PendingEngineDelivery {
|
|
8
8
|
#private;
|
|
9
|
+
private readonly retain?;
|
|
10
|
+
constructor(retain?: ((event: OwnedUiEvent) => () => void) | undefined);
|
|
9
11
|
get size(): number;
|
|
10
12
|
diagnostics(): {
|
|
11
13
|
pending: number;
|
|
@@ -22,5 +24,7 @@ export declare class PendingEngineDelivery {
|
|
|
22
24
|
shift(): {
|
|
23
25
|
event: OwnedUiEvent;
|
|
24
26
|
generation: number;
|
|
27
|
+
release?: (() => void) | undefined;
|
|
25
28
|
} | undefined;
|
|
29
|
+
seal(): boolean;
|
|
26
30
|
}
|
|
@@ -25,6 +25,7 @@ export function replaceableEventKey(event) {
|
|
|
25
25
|
}
|
|
26
26
|
/** Intrusive FIFO + segment index: replacement unlinks in O(1), never changes source ordering. */
|
|
27
27
|
export class PendingEngineDelivery {
|
|
28
|
+
retain;
|
|
28
29
|
#replaceable = new Map();
|
|
29
30
|
#head;
|
|
30
31
|
#tail;
|
|
@@ -35,6 +36,9 @@ export class PendingEngineDelivery {
|
|
|
35
36
|
#superseded = 0;
|
|
36
37
|
#peakNodes = 0;
|
|
37
38
|
#peakBytes = 0;
|
|
39
|
+
constructor(retain) {
|
|
40
|
+
this.retain = retain;
|
|
41
|
+
}
|
|
38
42
|
get size() { return this.#size; }
|
|
39
43
|
diagnostics() {
|
|
40
44
|
return { pending: this.#size, bytes: this.#bytes, protected: this.#protected,
|
|
@@ -44,7 +48,7 @@ export class PendingEngineDelivery {
|
|
|
44
48
|
push(event, generation, reconcile) {
|
|
45
49
|
const key = replaceableEventKey(event);
|
|
46
50
|
if (generation !== this.#generation || key === null) {
|
|
47
|
-
if (!this
|
|
51
|
+
if (!this.seal())
|
|
48
52
|
return false;
|
|
49
53
|
this.#generation = generation;
|
|
50
54
|
}
|
|
@@ -56,11 +60,11 @@ export class PendingEngineDelivery {
|
|
|
56
60
|
if (this.#size - (prior === undefined ? 0 : 1) >= MAX_PENDING_EVENTS
|
|
57
61
|
|| this.#bytes - (prior?.bytes ?? 0) + bytes > MAX_PENDING_EVENT_BYTES)
|
|
58
62
|
return false;
|
|
63
|
+
const node = { key, generation, bytes, ...(marker ? { resolve: reconcile } : { event, release: this.retain?.(event) }) };
|
|
59
64
|
if (prior !== undefined) {
|
|
60
65
|
this.#unlink(prior);
|
|
61
66
|
this.#superseded = Math.min(Number.MAX_SAFE_INTEGER, this.#superseded + 1);
|
|
62
67
|
}
|
|
63
|
-
const node = { key, generation, bytes, ...(marker ? { resolve: reconcile } : { event }) };
|
|
64
68
|
node.previous = this.#tail;
|
|
65
69
|
if (this.#tail !== undefined)
|
|
66
70
|
this.#tail.next = node;
|
|
@@ -98,11 +102,13 @@ export class PendingEngineDelivery {
|
|
|
98
102
|
if (node === undefined)
|
|
99
103
|
return undefined;
|
|
100
104
|
const event = node.event ?? node.resolve();
|
|
101
|
-
this
|
|
102
|
-
|
|
105
|
+
const release = node.release ?? this.retain?.(event);
|
|
106
|
+
this.#unlink(node, false);
|
|
107
|
+
// Invariant: a consumed snapshot retains its assets through synchronous listener delivery, including reentrant production.
|
|
108
|
+
return { event, generation: node.generation, ...(release === undefined ? {} : { release }) };
|
|
103
109
|
}
|
|
104
110
|
// Invariant: a barrier freezes lazy authoritative markers before a later segment can change them.
|
|
105
|
-
|
|
111
|
+
seal() {
|
|
106
112
|
for (const node of this.#replaceable.values()) {
|
|
107
113
|
if (node.resolve === undefined)
|
|
108
114
|
continue;
|
|
@@ -114,12 +120,15 @@ export class PendingEngineDelivery {
|
|
|
114
120
|
node.bytes = bytes;
|
|
115
121
|
node.event = event;
|
|
116
122
|
node.resolve = undefined;
|
|
123
|
+
node.release = this.retain?.(event);
|
|
117
124
|
this.#peakBytes = Math.max(this.#peakBytes, this.#bytes);
|
|
118
125
|
}
|
|
119
126
|
this.#replaceable.clear();
|
|
120
127
|
return true;
|
|
121
128
|
}
|
|
122
|
-
#unlink(node) {
|
|
129
|
+
#unlink(node, release = true) {
|
|
130
|
+
if (release)
|
|
131
|
+
node.release?.();
|
|
123
132
|
if (node.previous !== undefined)
|
|
124
133
|
node.previous.next = node.next;
|
|
125
134
|
else
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const MAX_METADATA_BYTES = 64 * 1024;
|
|
2
|
+
const MAX_TEXT_BYTES = 256 * 1024;
|
|
2
3
|
/** Copy JSON metadata without diagnostic depth/array truncation or traversing result text/images. */
|
|
3
4
|
function renderingJson(value) {
|
|
4
5
|
let remaining = MAX_METADATA_BYTES;
|
|
@@ -48,27 +49,49 @@ export function toolRenderingInput(options) {
|
|
|
48
49
|
}
|
|
49
50
|
const imageReferences = [];
|
|
50
51
|
const content = [];
|
|
52
|
+
let contentBytes = 2;
|
|
53
|
+
let excessBoundaries = false;
|
|
54
|
+
const addPart = (part) => {
|
|
55
|
+
if (excessBoundaries)
|
|
56
|
+
return;
|
|
57
|
+
// Performance: these ASCII-only offsets/indexes are budgeted before retaining or encoding an oversized boundary array.
|
|
58
|
+
contentBytes += JSON.stringify(part).length + (content.length > 0 ? 1 : 0);
|
|
59
|
+
if (contentBytes > MAX_METADATA_BYTES)
|
|
60
|
+
excessBoundaries = true;
|
|
61
|
+
else
|
|
62
|
+
content.push(part);
|
|
63
|
+
};
|
|
51
64
|
const texts = [];
|
|
52
65
|
let length = 0;
|
|
66
|
+
let textBytes = 0;
|
|
53
67
|
const source = isRecord(options.result) ? options.result : undefined;
|
|
54
68
|
const rawContent = source === undefined ? options.result : source.content;
|
|
55
69
|
const parts = typeof rawContent === "string" ? [{ type: "text", text: rawContent }] : rawContent;
|
|
56
70
|
if (Array.isArray(parts)) {
|
|
57
71
|
for (const part of parts) {
|
|
58
72
|
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
73
|
+
const separatorBytes = texts.length > 0 ? 1 : 0;
|
|
74
|
+
// Performance: length rejects huge strings before the bounded UTF-8 count; never serialize accumulated text.
|
|
75
|
+
const partBytes = part.text.length + textBytes + separatorBytes > MAX_TEXT_BYTES
|
|
76
|
+
? MAX_TEXT_BYTES + 1 : Buffer.byteLength(part.text);
|
|
77
|
+
if (partBytes + textBytes + separatorBytes > MAX_TEXT_BYTES) {
|
|
78
|
+
warnings.add("Tool text unavailable: result exceeds the supported 256 KiB text limit.");
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
textBytes += partBytes + separatorBytes;
|
|
59
82
|
if (texts.length > 0)
|
|
60
83
|
length++;
|
|
61
84
|
const start = length;
|
|
62
85
|
texts.push(part.text);
|
|
63
86
|
length += part.text.length;
|
|
64
|
-
|
|
87
|
+
addPart({ type: "text", start, end: length });
|
|
65
88
|
}
|
|
66
89
|
else if (isRecord(part) && part.type === "image") {
|
|
67
90
|
const reference = imageReferences.length < 16 ? options.image(part) : undefined;
|
|
68
91
|
if (reference === undefined)
|
|
69
92
|
warnings.add("Image unavailable: unsupported data or attachment limit exceeded.");
|
|
70
93
|
else {
|
|
71
|
-
|
|
94
|
+
addPart({ type: "image", imageIndex: imageReferences.length });
|
|
72
95
|
imageReferences.push(reference);
|
|
73
96
|
}
|
|
74
97
|
}
|
|
@@ -93,7 +116,7 @@ export function toolRenderingInput(options) {
|
|
|
93
116
|
...(options.result === undefined ? {} : { result: { content, ...(details === undefined ? {} : { details }) } }),
|
|
94
117
|
};
|
|
95
118
|
const warning = () => [...warnings].join(" ");
|
|
96
|
-
const fits = () => Buffer.byteLength(JSON.stringify({ payload: options.payload, toolRendering: {
|
|
119
|
+
const fits = () => !excessBoundaries && Buffer.byteLength(JSON.stringify({ payload: options.payload, toolRendering: {
|
|
97
120
|
...toolRendering, ...(warnings.size === 0 ? {} : { unavailable: warning() }),
|
|
98
121
|
} })) <= MAX_METADATA_BYTES;
|
|
99
122
|
if (!fits()) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OwnedUiEvent, OwnedUiImageAttachment, OwnedUiTranscriptBlock, OwnedUiTranscriptImageReference } from "../../../contracts/owned-ui/index.js";
|
|
2
|
+
export declare class TranscriptImageAssets {
|
|
3
|
+
#private;
|
|
4
|
+
resolve(id: string): OwnedUiImageAttachment | null;
|
|
5
|
+
clear(): void;
|
|
6
|
+
reference(value: unknown, source: OwnedUiTranscriptImageReference["source"]): OwnedUiTranscriptImageReference | undefined;
|
|
7
|
+
retain(block: OwnedUiTranscriptBlock): void;
|
|
8
|
+
release(block: OwnedUiTranscriptBlock): void;
|
|
9
|
+
retainEvent(event: OwnedUiEvent): () => void;
|
|
10
|
+
discardUnowned(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
3
|
+
// Security: current blocks and admitted delivery snapshots own assets independently; superseded state owns none.
|
|
4
|
+
export class TranscriptImageAssets {
|
|
5
|
+
#assets = new Map();
|
|
6
|
+
#byData = new Map();
|
|
7
|
+
#unowned = new Set();
|
|
8
|
+
resolve(id) { return this.#assets.get(id)?.asset ?? null; }
|
|
9
|
+
clear() { this.#assets.clear(); this.#byData.clear(); this.#unowned.clear(); }
|
|
10
|
+
reference(value, source) {
|
|
11
|
+
if (typeof value !== "object" || value === null || !("type" in value) || value.type !== "image"
|
|
12
|
+
|| !("data" in value) || typeof value.data !== "string" || value.data.length === 0
|
|
13
|
+
|| !("mimeType" in value) || typeof value.mimeType !== "string" || !/^image\/[a-z0-9.+-]+$/i.test(value.mimeType))
|
|
14
|
+
return;
|
|
15
|
+
// Performance: reject oversized encoded data before allocating a decoded buffer or hashing it.
|
|
16
|
+
if (value.data.length > Math.ceil(MAX_IMAGE_BYTES / 3) * 4)
|
|
17
|
+
return;
|
|
18
|
+
// Performance: repeated accumulated results reuse the same retained bytes without decoding and hashing per chunk.
|
|
19
|
+
const cachedId = this.#byData.get(value.mimeType)?.get(value.data);
|
|
20
|
+
if (cachedId !== undefined)
|
|
21
|
+
return { assetId: cachedId, mimeType: value.mimeType, byteLength: this.#assets.get(cachedId).byteLength, source };
|
|
22
|
+
const byteLength = Buffer.from(value.data, "base64").byteLength;
|
|
23
|
+
if (byteLength < 1 || byteLength > MAX_IMAGE_BYTES)
|
|
24
|
+
return;
|
|
25
|
+
const assetId = `image-${createHash("sha256").update(value.mimeType).update("\0").update(value.data).digest("hex").slice(0, 24)}`;
|
|
26
|
+
if (!this.#assets.has(assetId)) {
|
|
27
|
+
this.#assets.set(assetId, { asset: { type: "image", data: value.data, mimeType: value.mimeType }, byteLength, owners: 0 });
|
|
28
|
+
let byData = this.#byData.get(value.mimeType);
|
|
29
|
+
if (byData === undefined)
|
|
30
|
+
this.#byData.set(value.mimeType, byData = new Map());
|
|
31
|
+
byData.set(value.data, assetId);
|
|
32
|
+
this.#unowned.add(assetId);
|
|
33
|
+
}
|
|
34
|
+
return { assetId, mimeType: value.mimeType, byteLength, source };
|
|
35
|
+
}
|
|
36
|
+
retain(block) {
|
|
37
|
+
for (const reference of block.imageReferences ?? []) {
|
|
38
|
+
const entry = this.#assets.get(reference.assetId);
|
|
39
|
+
if (entry === undefined)
|
|
40
|
+
continue;
|
|
41
|
+
entry.owners++;
|
|
42
|
+
this.#unowned.delete(reference.assetId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
release(block) {
|
|
46
|
+
for (const reference of block.imageReferences ?? []) {
|
|
47
|
+
const entry = this.#assets.get(reference.assetId);
|
|
48
|
+
if (entry !== undefined && --entry.owners === 0)
|
|
49
|
+
this.#delete(reference.assetId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
retainEvent(event) {
|
|
53
|
+
const blocks = event.type === "transcript-block" ? [event.block] : event.type === "session-view" ? event.view.transcript : [];
|
|
54
|
+
// Concurrency: capture entries, not ids; an obsolete generation's release cannot decrement a replacement asset.
|
|
55
|
+
const entries = blocks.flatMap(block => (block.imageReferences ?? []).map(ref => [ref.assetId, this.#assets.get(ref.assetId)]));
|
|
56
|
+
for (const [id, entry] of entries) {
|
|
57
|
+
if (entry === undefined)
|
|
58
|
+
continue;
|
|
59
|
+
entry.owners++;
|
|
60
|
+
this.#unowned.delete(id);
|
|
61
|
+
}
|
|
62
|
+
return () => {
|
|
63
|
+
for (const [id, entry] of entries) {
|
|
64
|
+
if (entry !== undefined && this.#assets.get(id) === entry && --entry.owners === 0)
|
|
65
|
+
this.#delete(id);
|
|
66
|
+
}
|
|
67
|
+
entries.length = 0;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
discardUnowned() {
|
|
71
|
+
for (const id of this.#unowned)
|
|
72
|
+
this.#delete(id);
|
|
73
|
+
this.#unowned.clear();
|
|
74
|
+
}
|
|
75
|
+
#delete(id) {
|
|
76
|
+
const entry = this.#assets.get(id);
|
|
77
|
+
if (entry === undefined)
|
|
78
|
+
return;
|
|
79
|
+
const byData = this.#byData.get(entry.asset.mimeType);
|
|
80
|
+
byData?.delete(entry.asset.data);
|
|
81
|
+
if (byData?.size === 0)
|
|
82
|
+
this.#byData.delete(entry.asset.mimeType);
|
|
83
|
+
this.#assets.delete(id);
|
|
84
|
+
this.#unowned.delete(id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PiTuiInputDiagnosticsEvent } from "../tui-runtime/index.js";
|
|
2
|
+
import type { ResponseCopyEvent } from "./response-copy-protocol.js";
|
|
3
|
+
import type { PasteEvent } from "./paste-protocol.js";
|
|
4
|
+
/** Opt-in scalar-only snapshots, bounded to 128 rows and one pending asynchronous write. */
|
|
5
|
+
export declare class ClipboardDiagnosticCapture {
|
|
6
|
+
#private;
|
|
7
|
+
private readonly destination;
|
|
8
|
+
private readonly write;
|
|
9
|
+
constructor(destination: string, write?: (file: string, data: string) => Promise<void>);
|
|
10
|
+
copy(event: ResponseCopyEvent): void;
|
|
11
|
+
paste(event: PasteEvent): void;
|
|
12
|
+
runtime(event: PiTuiInputDiagnosticsEvent): void;
|
|
13
|
+
/** Explicit test/evidence boundary; the interactive UI never waits for disk diagnostics. */
|
|
14
|
+
flush(): Promise<void>;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
const PHASES = new Set(["capture", "selection-clear", "queued", "preparing", "extracted", "encoded", "submitting", "settled", "cleanup",
|
|
3
|
+
"framing", "admitted", "predecessor", "acquiring", "acquired-text", "acquired-image", "classifying", "path-fallback", "prepared", "inserting",
|
|
4
|
+
"receipt", "semantic-start", "semantic-end", "composition-start", "composition-end", "write-start", "write-end", "heartbeat"]);
|
|
5
|
+
/** Opt-in scalar-only snapshots, bounded to 128 rows and one pending asynchronous write. */
|
|
6
|
+
export class ClipboardDiagnosticCapture {
|
|
7
|
+
destination;
|
|
8
|
+
write;
|
|
9
|
+
#records = [];
|
|
10
|
+
#payloads = new Map();
|
|
11
|
+
#pending;
|
|
12
|
+
#writing;
|
|
13
|
+
#disposed = false;
|
|
14
|
+
#heartbeat;
|
|
15
|
+
constructor(destination, write = (file, data) => writeFile(file, data, { encoding: "utf8", mode: 0o600 })) {
|
|
16
|
+
this.destination = destination;
|
|
17
|
+
this.write = write;
|
|
18
|
+
this.#heartbeat = setInterval(() => this.#record("heartbeat", { phase: "heartbeat", atMs: performance.now() }), 100);
|
|
19
|
+
this.#heartbeat.unref();
|
|
20
|
+
}
|
|
21
|
+
copy(event) { this.#record("copy", event); }
|
|
22
|
+
paste(event) { this.#record("paste", event); }
|
|
23
|
+
runtime(event) {
|
|
24
|
+
this.#record("runtime", { phase: event.phase, atMs: event.atMs, request: event.revision, pending: event.pendingDepth });
|
|
25
|
+
}
|
|
26
|
+
#record(source, event) {
|
|
27
|
+
if (this.#disposed || !PHASES.has(event.phase))
|
|
28
|
+
return;
|
|
29
|
+
const key = `${source}:${event.request ?? 0}`;
|
|
30
|
+
// Concurrency: eight pastes plus active/pending copy and the incoming superseding capture can overlap briefly.
|
|
31
|
+
if (["capture", "admitted"].includes(event.phase) && this.#payloads.size < 11)
|
|
32
|
+
this.#payloads.set(key, scalar(event.sourceUnits) * 2);
|
|
33
|
+
if (this.#payloads.has(key) && event.bytes !== undefined)
|
|
34
|
+
this.#payloads.set(key, Math.max(this.#payloads.get(key), scalar(event.bytes)));
|
|
35
|
+
if (event.phase === "settled" || event.phase === "cleanup")
|
|
36
|
+
this.#payloads.delete(key);
|
|
37
|
+
// Rationale: observed payload/source bytes, not an assertion about opaque native allocator overhead.
|
|
38
|
+
const pendingBytes = scalar([...this.#payloads.values()].reduce((sum, bytes) => sum + bytes, 0));
|
|
39
|
+
// Security: project only declared scalar fields. Never serialize the supplied event object or raw errors.
|
|
40
|
+
this.#records.push({ source, phase: event.phase, atMs: scalar(event.atMs), request: event.request !== undefined && event.request < 0 ? -scalar(-event.request) : scalar(event.request), pending: scalar(event.pending), bytes: scalar(event.bytes), elapsedMs: scalar(event.elapsedMs),
|
|
41
|
+
sourceUnits: scalar(event.sourceUnits), pendingBytes,
|
|
42
|
+
transport: ["native", "terminal", "provided", "injected"].includes(event.transport ?? "") ? event.transport : "none",
|
|
43
|
+
outcome: ["ready", "failed", "canceled", "delivered", "submitted-unverified", "timed-out", "superseded"].includes(event.outcome ?? "") ? event.outcome : "none" });
|
|
44
|
+
if (this.#records.length > 128)
|
|
45
|
+
this.#records.shift();
|
|
46
|
+
this.#pending = JSON.stringify({ schema: "clipboard-diagnostics-v1", records: this.#records });
|
|
47
|
+
this.#pump();
|
|
48
|
+
}
|
|
49
|
+
#pump() {
|
|
50
|
+
if (this.#disposed || this.#writing || this.#pending === undefined)
|
|
51
|
+
return;
|
|
52
|
+
const snapshot = this.#pending;
|
|
53
|
+
this.#pending = undefined;
|
|
54
|
+
this.#writing = Promise.resolve().then(() => this.write(this.destination, snapshot)).catch(() => {
|
|
55
|
+
// Security: diagnostics failure must not enter the normal terminal or block clipboard work.
|
|
56
|
+
}).finally(() => { this.#writing = undefined; this.#pump(); });
|
|
57
|
+
}
|
|
58
|
+
/** Explicit test/evidence boundary; the interactive UI never waits for disk diagnostics. */
|
|
59
|
+
async flush() { while (this.#writing)
|
|
60
|
+
await this.#writing; }
|
|
61
|
+
dispose() { this.#disposed = true; clearInterval(this.#heartbeat); this.#pending = undefined; this.#records = []; this.#payloads.clear(); }
|
|
62
|
+
}
|
|
63
|
+
function scalar(value) {
|
|
64
|
+
return value !== undefined && Number.isFinite(value) ? Math.max(0, Math.min(Number.MAX_SAFE_INTEGER, value)) : 0;
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Aggregate explicit URL-chip metadata allowance for one owned editor decoration pass. */
|
|
2
|
+
export declare const EDITOR_HYPERLINK_METADATA_BYTES: number;
|
|
3
|
+
/** Accounts complete OSC 8 controls before construction, without scanning oversized backing URLs. */
|
|
4
|
+
export declare class EditorHyperlinkBudget {
|
|
5
|
+
#private;
|
|
6
|
+
reset(): void;
|
|
7
|
+
takeCleanup(): boolean;
|
|
8
|
+
take(target: string): boolean;
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Aggregate explicit URL-chip metadata allowance for one owned editor decoration pass. */
|
|
2
|
+
export const EDITOR_HYPERLINK_METADATA_BYTES = 64 * 1024;
|
|
3
|
+
const CLOSE_BYTES = 7;
|
|
4
|
+
const PAIR_BYTES = CLOSE_BYTES * 2;
|
|
5
|
+
/** Accounts complete OSC 8 controls before construction, without scanning oversized backing URLs. */
|
|
6
|
+
export class EditorHyperlinkBudget {
|
|
7
|
+
#remaining = EDITOR_HYPERLINK_METADATA_BYTES;
|
|
8
|
+
reset() { this.#remaining = EDITOR_HYPERLINK_METADATA_BYTES; }
|
|
9
|
+
takeCleanup() { return this.#reserve(CLOSE_BYTES); }
|
|
10
|
+
take(target) {
|
|
11
|
+
// Performance: UTF-16 length is a lower bound on UTF-8 bytes, including replacement surrogates.
|
|
12
|
+
// Reject large backing values before byte counting, flattening, interpolation, or signatures.
|
|
13
|
+
if (target.length > this.#remaining - PAIR_BYTES)
|
|
14
|
+
return false;
|
|
15
|
+
return this.#reserve(PAIR_BYTES + Buffer.byteLength(target, "utf8"));
|
|
16
|
+
}
|
|
17
|
+
#reserve(bytes) {
|
|
18
|
+
if (bytes > this.#remaining)
|
|
19
|
+
return false;
|
|
20
|
+
this.#remaining -= bytes;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PiShellClipboardContent } from "../components/index.js";
|
|
2
|
+
import { type PreparedPaste, type PasteEvent } from "./paste-protocol.js";
|
|
3
|
+
export interface PasteExecutorJob {
|
|
4
|
+
readonly result: Promise<PreparedPaste>;
|
|
5
|
+
readonly stopped: Promise<void>;
|
|
6
|
+
cancel(): void;
|
|
7
|
+
}
|
|
8
|
+
export type PastePhase = (phase: PasteEvent["phase"], bytes?: number) => void;
|
|
9
|
+
/** Streams bounded fragments through a killable child; native/codec/filesystem work never runs on the UI thread. */
|
|
10
|
+
export declare function startPasteExecutor(content: PiShellClipboardContent | undefined, signal: AbortSignal, phase?: PastePhase, helper?: URL): PasteExecutorJob;
|