@opengeni/react 3.5.1-canary.0 → 3.7.0-canary.0
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/artifacts.d.ts +1 -1
- package/dist/artifacts.js +229 -6
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-IB27C53Y.js → chunk-C6WDLHBA.js} +31 -23
- package/dist/chunk-C6WDLHBA.js.map +1 -0
- package/dist/{chunk-WY3MELVJ.js → chunk-DOK4KZF2.js} +119 -8
- package/dist/chunk-DOK4KZF2.js.map +1 -0
- package/dist/{chunk-TJFMMDQU.js → chunk-FJBOU2TH.js} +112 -6
- package/dist/chunk-FJBOU2TH.js.map +1 -0
- package/dist/{chunk-UNCXXIQR.js → chunk-R55HFTT3.js} +66 -24
- package/dist/chunk-R55HFTT3.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/components/artifacts/index.d.ts +1 -1
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +45 -3
- package/dist/components/composer.d.ts +2 -0
- package/dist/components/session-chrome.d.ts +1 -1
- package/dist/composer.js +1 -1
- package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
- package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
- package/dist/hooks/use-file-attachments.d.ts +6 -0
- package/dist/hooks/use-sandbox-files.d.ts +4 -1
- package/dist/hooks/use-workspace-edit.d.ts +4 -0
- package/dist/index.js +114 -58
- package/dist/index.js.map +1 -1
- package/dist/session-ui.js +2 -2
- package/dist/session.js +2 -2
- package/dist/timeline/registry.d.ts +2 -0
- package/dist/timeline/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +2 -1
- package/src/components/artifacts/index.ts +3 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +284 -4
- package/src/components/composer.tsx +89 -25
- package/src/components/message-timeline.tsx +32 -8
- package/src/components/sandbox-workspace.tsx +11 -1
- package/src/components/session-chrome.tsx +3 -3
- package/src/hooks/use-file-attachments.ts +34 -0
- package/src/hooks/use-sandbox-files.ts +68 -20
- package/src/hooks/use-session-events.ts +21 -26
- package/src/hooks/use-workspace-edit.ts +23 -5
- package/src/timeline/fleet-decision-projection.ts +2 -1
- package/src/timeline/fleet-decision-row.tsx +1 -0
- package/src/timeline/projection.ts +151 -7
- package/src/timeline/registry.ts +15 -3
- package/src/timeline/tool-renderers.tsx +117 -1
- package/src/timeline/types.ts +8 -1
- package/dist/chunk-IB27C53Y.js.map +0 -1
- package/dist/chunk-TJFMMDQU.js.map +0 -1
- package/dist/chunk-UNCXXIQR.js.map +0 -1
- package/dist/chunk-WY3MELVJ.js.map +0 -1
- package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
package/dist/session-ui.js
CHANGED
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
sessionChromeGoalPillLabel,
|
|
12
12
|
sessionChromeGoalPillState,
|
|
13
13
|
userMessageLikelyNeedsDisclosure
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-DOK4KZF2.js";
|
|
15
15
|
import "./chunk-W3OUAH6M.js";
|
|
16
16
|
import "./chunk-JVYQRRC4.js";
|
|
17
17
|
import "./chunk-7EAFGICR.js";
|
|
18
18
|
import "./chunk-VZTQ73XT.js";
|
|
19
19
|
import {
|
|
20
20
|
createOlderHistoryLoadReceipt
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-FJBOU2TH.js";
|
|
22
22
|
import "./chunk-VTOREEXF.js";
|
|
23
23
|
import "./chunk-22KP6LR5.js";
|
|
24
24
|
export {
|
package/dist/session.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
useSessionLineage,
|
|
18
18
|
useSessionMcpApprovalPolicy,
|
|
19
19
|
useTurnQueue
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-C6WDLHBA.js";
|
|
21
21
|
import {
|
|
22
22
|
FILE_ONLY_MESSAGE_TEXT,
|
|
23
23
|
composeSendInput,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
projectPendingHumanInputRequests,
|
|
38
38
|
sessionStatusFromEvents,
|
|
39
39
|
toolDisplayName
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-FJBOU2TH.js";
|
|
41
41
|
import "./chunk-VTOREEXF.js";
|
|
42
42
|
export {
|
|
43
43
|
FILE_ONLY_MESSAGE_TEXT,
|
|
@@ -22,6 +22,8 @@ export type ToolRegistryEntry = {
|
|
|
22
22
|
match: "name";
|
|
23
23
|
name: string;
|
|
24
24
|
render: ToolRenderer;
|
|
25
|
+
/** Disable untrusted `<server>__${name}` leaf matching for identity-sensitive renderers. */
|
|
26
|
+
matchPrefixedLeaf?: boolean | undefined;
|
|
25
27
|
};
|
|
26
28
|
export type ToolRegistry = {
|
|
27
29
|
/** Resolve the renderer for a call (never null — falls back to generic). */
|
package/dist/timeline/types.d.ts
CHANGED
|
@@ -228,7 +228,7 @@ export type FleetDecisionItem = {
|
|
|
228
228
|
policyVersion: "adaptive-shadow-v1";
|
|
229
229
|
actualOutcome: "selected" | "waiting" | "none";
|
|
230
230
|
actualCandidateKey: string | null;
|
|
231
|
-
actualReason: "lease_reused" | "pin" | "rotation" | "active" | "all_capped" | "none";
|
|
231
|
+
actualReason: "lease_reused" | "pin" | "rotation" | "active" | "all_capped" | "allocator_disabled" | "none";
|
|
232
232
|
shadowOutcome: "selected" | "paced" | "none";
|
|
233
233
|
shadowCandidateKey: string | null;
|
|
234
234
|
shadowReason: "fenced_in_flight" | "fenced_candidate_missing" | "admission_paced" | "no_eligible_candidate" | "overlay_isolated_empty" | "best_score" | "affinity_best" | "hysteresis_hold";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0-canary.0",
|
|
4
4
|
"description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"@dnd-kit/utilities": "3.2.2",
|
|
121
121
|
"@fontsource-variable/noto-sans-arabic": "5.2.10",
|
|
122
122
|
"@fontsource-variable/noto-sans-jp": "5.2.10",
|
|
123
|
-
"@opengeni/sdk": "^3.
|
|
123
|
+
"@opengeni/sdk": "^3.7.0-canary.0",
|
|
124
124
|
"clsx": "^2.1.1",
|
|
125
125
|
"dequal": "2.0.3",
|
|
126
126
|
"lucide-react": "^1.8.0",
|
package/src/artifacts.ts
CHANGED
|
@@ -18,6 +18,8 @@ export {
|
|
|
18
18
|
PresentationProjectionEditor,
|
|
19
19
|
PublishedHtmlArtifactFrame,
|
|
20
20
|
PUBLISHED_HTML_ARTIFACT_IFRAME_SANDBOX,
|
|
21
|
+
openGeniSiteBridgePortFromBootstrap,
|
|
22
|
+
publishedHtmlArtifactDocument,
|
|
21
23
|
SpreadsheetArtifactSurface,
|
|
22
24
|
SpreadsheetGrid,
|
|
23
25
|
SpreadsheetProjectionGrid,
|
|
@@ -62,6 +64,7 @@ export {
|
|
|
62
64
|
type PresentationProjectionConnector,
|
|
63
65
|
type PresentationProjectionEditorProps,
|
|
64
66
|
type PublishedHtmlArtifactFrameProps,
|
|
67
|
+
type PublishedHtmlArtifactToolBridge,
|
|
65
68
|
type PresentationProjectionElementMetadata,
|
|
66
69
|
type PresentationProjectionElement,
|
|
67
70
|
type PresentationProjectionFill,
|
package/src/client.ts
CHANGED
|
@@ -246,7 +246,8 @@ export type EmbeddedSessionLineageClientLike = EmbeddedSessionEventClientLike &
|
|
|
246
246
|
Pick<OpenGeniClient, "getSessionLineage">;
|
|
247
247
|
|
|
248
248
|
/** Exact client surface required by {@link useFileAttachments}. */
|
|
249
|
-
export type EmbeddedFileAttachmentClientLike = Pick<OpenGeniClient, "uploadFile"
|
|
249
|
+
export type EmbeddedFileAttachmentClientLike = Pick<OpenGeniClient, "uploadFile"> &
|
|
250
|
+
Partial<Pick<OpenGeniClient, "createFileDownloadUrl">>;
|
|
250
251
|
|
|
251
252
|
/** Exact client surface required by structured human-input hooks. */
|
|
252
253
|
export type EmbeddedHumanInputSessionClientLike = EmbeddedSessionEventClientLike &
|
|
@@ -6,7 +6,10 @@ export {
|
|
|
6
6
|
export {
|
|
7
7
|
PUBLISHED_HTML_ARTIFACT_IFRAME_SANDBOX,
|
|
8
8
|
PublishedHtmlArtifactFrame,
|
|
9
|
+
openGeniSiteBridgePortFromBootstrap,
|
|
10
|
+
publishedHtmlArtifactDocument,
|
|
9
11
|
type PublishedHtmlArtifactFrameProps,
|
|
12
|
+
type PublishedHtmlArtifactToolBridge,
|
|
10
13
|
} from "./published-html-artifact-frame";
|
|
11
14
|
export {
|
|
12
15
|
SpreadsheetArtifactSurface,
|
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
OPENGENI_SITE_BRIDGE_BOOTSTRAP_GLOBAL,
|
|
3
|
+
OPENGENI_SITE_BRIDGE_READY,
|
|
4
|
+
OPENGENI_SITE_BRIDGE_RESPONSE,
|
|
5
|
+
OPENGENI_SITE_BRIDGE_VERSION,
|
|
6
|
+
isOpenGeniSiteBridgeCancelMessage,
|
|
7
|
+
isOpenGeniSiteBridgeConnectMessage,
|
|
8
|
+
isOpenGeniSiteBridgeRequestMessage,
|
|
9
|
+
sanitizeOpenGeniSiteToolCallRequest,
|
|
10
|
+
type OpenGeniSiteBridgeResponseMessage,
|
|
11
|
+
type OpenGeniSiteToolCatalog,
|
|
12
|
+
} from "@opengeni/sdk/site";
|
|
13
|
+
import type {
|
|
14
|
+
ToolGatewayCallRequest,
|
|
15
|
+
ToolGatewayCallResponse,
|
|
16
|
+
ToolGatewayDeclarationsResponse,
|
|
17
|
+
} from "@opengeni/sdk";
|
|
18
|
+
import { useEffect, useRef, type CSSProperties } from "react";
|
|
2
19
|
|
|
3
20
|
export const PUBLISHED_HTML_ARTIFACT_IFRAME_SANDBOX = [
|
|
4
21
|
"allow-downloads",
|
|
@@ -17,22 +34,285 @@ export type PublishedHtmlArtifactFrameProps = {
|
|
|
17
34
|
title: string;
|
|
18
35
|
className?: string;
|
|
19
36
|
style?: CSSProperties;
|
|
37
|
+
toolBridge?: PublishedHtmlArtifactToolBridge;
|
|
20
38
|
};
|
|
21
39
|
|
|
40
|
+
export type PublishedHtmlArtifactToolBridge = {
|
|
41
|
+
catalog: (options: { signal: AbortSignal }) => Promise<OpenGeniSiteToolCatalog>;
|
|
42
|
+
call: (
|
|
43
|
+
request: ToolGatewayCallRequest,
|
|
44
|
+
options: { signal: AbortSignal },
|
|
45
|
+
) => Promise<ToolGatewayCallResponse>;
|
|
46
|
+
declarations?: (options: { signal: AbortSignal }) => Promise<ToolGatewayDeclarationsResponse>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const SITE_BRIDGE_BOOTSTRAP_SCRIPT = `<script>(()=>{const key=${JSON.stringify(
|
|
50
|
+
OPENGENI_SITE_BRIDGE_BOOTSTRAP_GLOBAL,
|
|
51
|
+
)};const parentWindow=window.parent;if(parentWindow===window)return;window.addEventListener("message",event=>{const data=event.data;if(event.source!==parentWindow||!data||data.type!==${JSON.stringify(
|
|
52
|
+
OPENGENI_SITE_BRIDGE_READY,
|
|
53
|
+
)}||data.version!==${OPENGENI_SITE_BRIDGE_VERSION}||event.ports.length!==1)return;const port=event.ports[0];const previous=window[key]?.port;if(previous&&previous!==port)previous.close();Object.defineProperty(window,key,{configurable:true,value:{port}});port.start();});})();</script>`;
|
|
54
|
+
|
|
55
|
+
export function publishedHtmlArtifactDocument(html: string, toolBridgeEnabled: boolean): string {
|
|
56
|
+
if (!toolBridgeEnabled) return html;
|
|
57
|
+
const doctypeEnd = leadingDoctypeEnd(html);
|
|
58
|
+
if (doctypeEnd >= 0) {
|
|
59
|
+
return `${html.slice(0, doctypeEnd + 1)}${SITE_BRIDGE_BOOTSTRAP_SCRIPT}${html.slice(doctypeEnd + 1)}`;
|
|
60
|
+
}
|
|
61
|
+
return `${SITE_BRIDGE_BOOTSTRAP_SCRIPT}${html}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function leadingDoctypeEnd(html: string): number {
|
|
65
|
+
const lower = html.toLowerCase();
|
|
66
|
+
let cursor = html.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
67
|
+
while (cursor < html.length) {
|
|
68
|
+
while (/\s/.test(html[cursor] ?? "")) cursor += 1;
|
|
69
|
+
if (!html.startsWith("<!--", cursor)) break;
|
|
70
|
+
const commentEnd = html.indexOf("-->", cursor + 4);
|
|
71
|
+
if (commentEnd < 0) return -1;
|
|
72
|
+
cursor = commentEnd + 3;
|
|
73
|
+
}
|
|
74
|
+
if (!lower.startsWith("<!doctype", cursor)) return -1;
|
|
75
|
+
return html.indexOf(">", cursor + 9);
|
|
76
|
+
}
|
|
77
|
+
|
|
22
78
|
/**
|
|
23
79
|
* Render exact published HTML in an opaque-origin iframe. Artifact scripts,
|
|
24
|
-
* external resources, forms, popups, and
|
|
25
|
-
* parent-origin authority, shared cookies/storage,
|
|
80
|
+
* external resources, forms, popups, downloads, and the host-filtered tool
|
|
81
|
+
* bridge work without granting parent-origin authority, shared cookies/storage,
|
|
82
|
+
* or top-level navigation.
|
|
26
83
|
*/
|
|
27
84
|
export function PublishedHtmlArtifactFrame(props: PublishedHtmlArtifactFrameProps) {
|
|
85
|
+
const frameRef = useRef<HTMLIFrameElement | null>(null);
|
|
86
|
+
const bridgeRef = useRef(props.toolBridge);
|
|
87
|
+
const onFrameLoadRef = useRef<() => void>(() => undefined);
|
|
88
|
+
const frameLoadPendingRef = useRef(false);
|
|
89
|
+
const toolBridgeEnabled = props.toolBridge !== undefined;
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
bridgeRef.current = props.toolBridge;
|
|
92
|
+
}, [props.toolBridge]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!toolBridgeEnabled || typeof window === "undefined") return;
|
|
95
|
+
const requests = new SiteBridgeRequestRegistry();
|
|
96
|
+
const attachToolPort = (data: unknown, ports: readonly MessagePort[]) => {
|
|
97
|
+
const port = openGeniSiteBridgePortFromBootstrap(data, ports);
|
|
98
|
+
if (!port) return;
|
|
99
|
+
requests.replacePort(port);
|
|
100
|
+
port.addEventListener("message", (portEvent: MessageEvent<unknown>) => {
|
|
101
|
+
const message = portEvent.data;
|
|
102
|
+
if (isOpenGeniSiteBridgeCancelMessage(message)) {
|
|
103
|
+
requests.cancel(port, message.requestId);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!isOpenGeniSiteBridgeRequestMessage(message)) return;
|
|
107
|
+
const controller = requests.start(port, message.requestId);
|
|
108
|
+
if (!controller) {
|
|
109
|
+
port.postMessage({
|
|
110
|
+
type: OPENGENI_SITE_BRIDGE_RESPONSE,
|
|
111
|
+
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
112
|
+
requestId: message.requestId,
|
|
113
|
+
ok: false,
|
|
114
|
+
error: {
|
|
115
|
+
code: "duplicate_request_id",
|
|
116
|
+
message: "A Site tool request with this id is already running",
|
|
117
|
+
retryable: false,
|
|
118
|
+
},
|
|
119
|
+
} satisfies OpenGeniSiteBridgeResponseMessage);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
void handleSiteBridgeRequest(bridgeRef.current, message, controller.signal)
|
|
123
|
+
.then((value) => {
|
|
124
|
+
if (!controller.signal.aborted) {
|
|
125
|
+
port.postMessage({
|
|
126
|
+
type: OPENGENI_SITE_BRIDGE_RESPONSE,
|
|
127
|
+
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
128
|
+
requestId: message.requestId,
|
|
129
|
+
ok: true,
|
|
130
|
+
value,
|
|
131
|
+
} satisfies OpenGeniSiteBridgeResponseMessage);
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.catch((error) => {
|
|
135
|
+
if (!controller.signal.aborted) {
|
|
136
|
+
port.postMessage({
|
|
137
|
+
type: OPENGENI_SITE_BRIDGE_RESPONSE,
|
|
138
|
+
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
139
|
+
requestId: message.requestId,
|
|
140
|
+
ok: false,
|
|
141
|
+
error: siteBridgeError(error),
|
|
142
|
+
} satisfies OpenGeniSiteBridgeResponseMessage);
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
.finally(() => requests.complete(port, message.requestId, controller));
|
|
146
|
+
});
|
|
147
|
+
port.addEventListener("messageerror", () => requests.closePort(port));
|
|
148
|
+
port.start();
|
|
149
|
+
port.postMessage({
|
|
150
|
+
type: OPENGENI_SITE_BRIDGE_READY,
|
|
151
|
+
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const documentLease = new SiteBridgeDocumentLease(attachToolPort, () => requests.closeAll());
|
|
155
|
+
const onFrameLoad = () => {
|
|
156
|
+
frameLoadPendingRef.current = false;
|
|
157
|
+
const frameWindow = frameRef.current?.contentWindow ?? null;
|
|
158
|
+
if (!frameWindow) return;
|
|
159
|
+
documentLease.load(frameWindow);
|
|
160
|
+
};
|
|
161
|
+
onFrameLoadRef.current = onFrameLoad;
|
|
162
|
+
if (frameLoadPendingRef.current) {
|
|
163
|
+
frameLoadPendingRef.current = false;
|
|
164
|
+
onFrameLoad();
|
|
165
|
+
}
|
|
166
|
+
return () => {
|
|
167
|
+
onFrameLoadRef.current = () => undefined;
|
|
168
|
+
documentLease.close();
|
|
169
|
+
};
|
|
170
|
+
}, [props.html, toolBridgeEnabled]);
|
|
28
171
|
return (
|
|
29
172
|
<iframe
|
|
173
|
+
ref={frameRef}
|
|
30
174
|
title={props.title}
|
|
31
175
|
sandbox={PUBLISHED_HTML_ARTIFACT_IFRAME_SANDBOX}
|
|
32
176
|
referrerPolicy="no-referrer"
|
|
33
|
-
srcDoc={props.html}
|
|
177
|
+
srcDoc={publishedHtmlArtifactDocument(props.html, toolBridgeEnabled)}
|
|
178
|
+
onLoad={() => {
|
|
179
|
+
frameLoadPendingRef.current = true;
|
|
180
|
+
onFrameLoadRef.current();
|
|
181
|
+
}}
|
|
34
182
|
className={props.className}
|
|
35
183
|
style={props.style}
|
|
36
184
|
/>
|
|
37
185
|
);
|
|
38
186
|
}
|
|
187
|
+
|
|
188
|
+
export class SiteBridgeRequestRegistry {
|
|
189
|
+
private readonly controllersByPort = new Map<MessagePort, Map<string, AbortController>>();
|
|
190
|
+
|
|
191
|
+
replacePort(port: MessagePort): void {
|
|
192
|
+
this.closeAll();
|
|
193
|
+
this.controllersByPort.set(port, new Map());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
start(port: MessagePort, requestId: string): AbortController | null {
|
|
197
|
+
const controllers = this.controllersByPort.get(port);
|
|
198
|
+
if (!controllers || controllers.has(requestId)) return null;
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
controllers.set(requestId, controller);
|
|
201
|
+
return controller;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
cancel(port: MessagePort, requestId: string): void {
|
|
205
|
+
const controllers = this.controllersByPort.get(port);
|
|
206
|
+
const controller = controllers?.get(requestId);
|
|
207
|
+
if (!controller) return;
|
|
208
|
+
controller.abort(new Error("Site tool request cancelled"));
|
|
209
|
+
controllers!.delete(requestId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
complete(port: MessagePort, requestId: string, controller: AbortController): void {
|
|
213
|
+
const controllers = this.controllersByPort.get(port);
|
|
214
|
+
if (controllers?.get(requestId) === controller) controllers.delete(requestId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
closePort(port: MessagePort): void {
|
|
218
|
+
const controllers = this.controllersByPort.get(port);
|
|
219
|
+
if (!controllers) return;
|
|
220
|
+
for (const controller of controllers.values()) {
|
|
221
|
+
controller.abort(new Error("Site tool bridge port closed"));
|
|
222
|
+
}
|
|
223
|
+
controllers.clear();
|
|
224
|
+
this.controllersByPort.delete(port);
|
|
225
|
+
port.close();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
closeAll(): void {
|
|
229
|
+
for (const port of [...this.controllersByPort.keys()]) this.closePort(port);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export class SiteBridgeDocumentLease {
|
|
234
|
+
private loaded = false;
|
|
235
|
+
private bootstrapPort: MessagePort | null = null;
|
|
236
|
+
|
|
237
|
+
constructor(
|
|
238
|
+
private readonly attachToolPort: (data: unknown, ports: readonly MessagePort[]) => void,
|
|
239
|
+
private readonly closeActivePorts: () => void,
|
|
240
|
+
private readonly createMessageChannel: () => MessageChannel = () => new MessageChannel(),
|
|
241
|
+
) {}
|
|
242
|
+
|
|
243
|
+
load(frameWindow: Pick<Window, "postMessage">): boolean {
|
|
244
|
+
if (this.loaded) {
|
|
245
|
+
this.close();
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
this.loaded = true;
|
|
249
|
+
const channel = this.createMessageChannel();
|
|
250
|
+
this.bootstrapPort = channel.port1;
|
|
251
|
+
channel.port1.addEventListener("message", (event: MessageEvent<unknown>) => {
|
|
252
|
+
this.attachToolPort(event.data, event.ports);
|
|
253
|
+
});
|
|
254
|
+
channel.port1.start();
|
|
255
|
+
frameWindow.postMessage(
|
|
256
|
+
{
|
|
257
|
+
type: OPENGENI_SITE_BRIDGE_READY,
|
|
258
|
+
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
259
|
+
},
|
|
260
|
+
"*",
|
|
261
|
+
[channel.port2],
|
|
262
|
+
);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
close(): void {
|
|
267
|
+
this.bootstrapPort?.close();
|
|
268
|
+
this.bootstrapPort = null;
|
|
269
|
+
this.closeActivePorts();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function openGeniSiteBridgePortFromBootstrap(
|
|
274
|
+
data: unknown,
|
|
275
|
+
ports: readonly MessagePort[],
|
|
276
|
+
): MessagePort | null {
|
|
277
|
+
if (!isOpenGeniSiteBridgeConnectMessage(data)) return null;
|
|
278
|
+
return ports.length === 1 ? ports[0]! : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function handleSiteBridgeRequest(
|
|
282
|
+
bridge: PublishedHtmlArtifactToolBridge | undefined,
|
|
283
|
+
message: Parameters<typeof isOpenGeniSiteBridgeRequestMessage>[0] & {
|
|
284
|
+
method: "catalog" | "call" | "declarations";
|
|
285
|
+
requestId: string;
|
|
286
|
+
payload?: ToolGatewayCallRequest;
|
|
287
|
+
},
|
|
288
|
+
signal: AbortSignal,
|
|
289
|
+
): Promise<OpenGeniSiteToolCatalog | ToolGatewayCallResponse | ToolGatewayDeclarationsResponse> {
|
|
290
|
+
if (!bridge) throw new Error("Site tool bridge is unavailable");
|
|
291
|
+
if (message.method === "catalog") return await bridge.catalog({ signal });
|
|
292
|
+
if (message.method === "declarations") {
|
|
293
|
+
if (!bridge.declarations) throw new Error("Site tool declarations are unavailable");
|
|
294
|
+
return await bridge.declarations({ signal });
|
|
295
|
+
}
|
|
296
|
+
return await bridge.call(sanitizeOpenGeniSiteToolCallRequest(message.payload!), { signal });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function siteBridgeError(error: unknown): {
|
|
300
|
+
code: string;
|
|
301
|
+
message: string;
|
|
302
|
+
retryable: boolean;
|
|
303
|
+
outcomeUnknown: boolean;
|
|
304
|
+
} {
|
|
305
|
+
const candidate = error as {
|
|
306
|
+
code?: unknown;
|
|
307
|
+
message?: unknown;
|
|
308
|
+
retryable?: unknown;
|
|
309
|
+
outcomeUnknown?: unknown;
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
code: typeof candidate?.code === "string" ? candidate.code : "site_tool_call_failed",
|
|
313
|
+
message:
|
|
314
|
+
typeof candidate?.message === "string" ? candidate.message : "Site tool request failed",
|
|
315
|
+
retryable: candidate?.retryable === true,
|
|
316
|
+
outcomeUnknown: candidate?.outcomeUnknown === true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -137,6 +137,7 @@ export type ChatComposerMessages = {
|
|
|
137
137
|
uploading: string;
|
|
138
138
|
uploadFailed: string;
|
|
139
139
|
previewAttachment: (name: string) => string;
|
|
140
|
+
previewUnavailable?: string | undefined;
|
|
140
141
|
attachmentPreviewLabel: string;
|
|
141
142
|
downloadAttachment: (name: string) => string;
|
|
142
143
|
closeAttachmentPreview: string;
|
|
@@ -204,6 +205,7 @@ export const defaultChatComposerMessages: ChatComposerMessages = {
|
|
|
204
205
|
uploading: "Uploading",
|
|
205
206
|
uploadFailed: "Upload failed",
|
|
206
207
|
previewAttachment: (name) => `Preview ${name}`,
|
|
208
|
+
previewUnavailable: "Preview unavailable",
|
|
207
209
|
attachmentPreviewLabel: "Attachment preview",
|
|
208
210
|
downloadAttachment: (name) => `Download ${name}`,
|
|
209
211
|
closeAttachmentPreview: "Close",
|
|
@@ -947,6 +949,7 @@ export function Attachments() {
|
|
|
947
949
|
onRemove={controller.attachments.remove}
|
|
948
950
|
onRetry={controller.attachments.retry}
|
|
949
951
|
onRetainPreview={controller.attachments.retainPreview}
|
|
952
|
+
onLoadPreview={controller.attachments.loadPreview}
|
|
950
953
|
/>
|
|
951
954
|
);
|
|
952
955
|
}
|
|
@@ -1638,21 +1641,83 @@ function AttachmentChips({
|
|
|
1638
1641
|
onRemove,
|
|
1639
1642
|
onRetry,
|
|
1640
1643
|
onRetainPreview,
|
|
1644
|
+
onLoadPreview,
|
|
1641
1645
|
}: {
|
|
1642
1646
|
attachments: UseFileAttachmentsResult["attachments"];
|
|
1643
1647
|
messages: ChatComposerMessages;
|
|
1644
1648
|
onRemove: (id: string) => void;
|
|
1645
1649
|
onRetry?: ((id: string) => void) | undefined;
|
|
1646
1650
|
onRetainPreview: UseFileAttachmentsResult["retainPreview"];
|
|
1651
|
+
onLoadPreview: UseFileAttachmentsResult["loadPreview"];
|
|
1647
1652
|
}) {
|
|
1648
1653
|
const lightbox = useLightboxOptional();
|
|
1654
|
+
const previewRequest = useRef<AbortController | null>(null);
|
|
1655
|
+
const [previewLoadingId, setPreviewLoadingId] = useState<string | null>(null);
|
|
1656
|
+
const [previewErrorId, setPreviewErrorId] = useState<string | null>(null);
|
|
1657
|
+
|
|
1658
|
+
useEffect(() => () => previewRequest.current?.abort(), [onLoadPreview]);
|
|
1659
|
+
|
|
1660
|
+
async function openPreview(
|
|
1661
|
+
attachment: UseFileAttachmentsResult["attachments"][number],
|
|
1662
|
+
source: HTMLButtonElement,
|
|
1663
|
+
canLoadPreview: boolean,
|
|
1664
|
+
) {
|
|
1665
|
+
previewRequest.current?.abort();
|
|
1666
|
+
const attachmentId = attachment.id;
|
|
1667
|
+
const releaseSource = onRetainPreview(attachmentId);
|
|
1668
|
+
let src = attachment.previewUrl;
|
|
1669
|
+
if (!releaseSource && canLoadPreview) {
|
|
1670
|
+
const controller = new AbortController();
|
|
1671
|
+
previewRequest.current = controller;
|
|
1672
|
+
setPreviewLoadingId(attachmentId);
|
|
1673
|
+
setPreviewErrorId(null);
|
|
1674
|
+
try {
|
|
1675
|
+
src = await onLoadPreview!(attachmentId, controller.signal);
|
|
1676
|
+
if (controller.signal.aborted) return;
|
|
1677
|
+
if (!src) {
|
|
1678
|
+
setPreviewErrorId(attachmentId);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
} catch {
|
|
1682
|
+
if (!controller.signal.aborted) setPreviewErrorId(attachmentId);
|
|
1683
|
+
return;
|
|
1684
|
+
} finally {
|
|
1685
|
+
if (previewRequest.current === controller) {
|
|
1686
|
+
setPreviewLoadingId(null);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
lightbox!.open(
|
|
1691
|
+
src!,
|
|
1692
|
+
attachment.name,
|
|
1693
|
+
source,
|
|
1694
|
+
messages.attachmentPreviewLabel,
|
|
1695
|
+
attachment.name,
|
|
1696
|
+
{
|
|
1697
|
+
download: messages.downloadAttachment(attachment.name),
|
|
1698
|
+
close: messages.closeAttachmentPreview,
|
|
1699
|
+
},
|
|
1700
|
+
releaseSource,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1649
1704
|
return (
|
|
1650
1705
|
<div className="flex flex-wrap gap-2 px-3 py-2">
|
|
1651
1706
|
{attachments.map((attachment) => {
|
|
1652
1707
|
const failed = attachment.status === "failed";
|
|
1653
1708
|
const secureContextRequired = attachment.errorCode === "secure_context_required";
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1709
|
+
const previewFailed = previewErrorId === attachment.id;
|
|
1710
|
+
const previewLoading = previewLoadingId === attachment.id;
|
|
1711
|
+
const canLoadPreview = Boolean(
|
|
1712
|
+
lightbox &&
|
|
1713
|
+
onLoadPreview &&
|
|
1714
|
+
attachment.status === "ready" &&
|
|
1715
|
+
attachment.file &&
|
|
1716
|
+
attachment.contentType.startsWith("image/"),
|
|
1717
|
+
);
|
|
1718
|
+
const statusText = previewFailed
|
|
1719
|
+
? (messages.previewUnavailable ?? "Preview unavailable")
|
|
1720
|
+
: attachment.status === "uploading"
|
|
1656
1721
|
? messages.uploading
|
|
1657
1722
|
: failed
|
|
1658
1723
|
? attachment.error || messages.uploadFailed
|
|
@@ -1672,32 +1737,31 @@ function AttachmentChips({
|
|
|
1672
1737
|
: "max-w-[240px] border-og-border bg-og-surface-2",
|
|
1673
1738
|
)}
|
|
1674
1739
|
>
|
|
1675
|
-
{attachment.previewUrl
|
|
1740
|
+
{lightbox && (attachment.previewUrl || canLoadPreview) ? (
|
|
1676
1741
|
<button
|
|
1677
1742
|
type="button"
|
|
1678
|
-
className=
|
|
1743
|
+
className={cn(
|
|
1744
|
+
"size-8 shrink-0 overflow-hidden rounded outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
1745
|
+
!attachment.previewUrl && "flex items-center justify-center disabled:cursor-wait",
|
|
1746
|
+
)}
|
|
1679
1747
|
aria-label={messages.previewAttachment(attachment.name)}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
event.currentTarget,
|
|
1686
|
-
messages.attachmentPreviewLabel,
|
|
1687
|
-
attachment.name,
|
|
1688
|
-
{
|
|
1689
|
-
download: messages.downloadAttachment(attachment.name),
|
|
1690
|
-
close: messages.closeAttachmentPreview,
|
|
1691
|
-
},
|
|
1692
|
-
releasePreview,
|
|
1693
|
-
);
|
|
1694
|
-
}}
|
|
1748
|
+
aria-busy={previewLoading || undefined}
|
|
1749
|
+
disabled={previewLoading}
|
|
1750
|
+
onClick={(event) =>
|
|
1751
|
+
void openPreview(attachment, event.currentTarget, canLoadPreview)
|
|
1752
|
+
}
|
|
1695
1753
|
>
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1754
|
+
{attachment.previewUrl ? (
|
|
1755
|
+
<img
|
|
1756
|
+
src={attachment.previewUrl}
|
|
1757
|
+
alt=""
|
|
1758
|
+
className="h-full w-full object-cover transition-opacity hover:opacity-80"
|
|
1759
|
+
/>
|
|
1760
|
+
) : previewLoading ? (
|
|
1761
|
+
<LoaderCircleIcon className="size-4 animate-og-spin text-og-fg-muted" />
|
|
1762
|
+
) : (
|
|
1763
|
+
<ImageIcon className="size-4 text-og-fg-muted" />
|
|
1764
|
+
)}
|
|
1701
1765
|
</button>
|
|
1702
1766
|
) : attachment.previewUrl ? (
|
|
1703
1767
|
<img
|
|
@@ -1716,7 +1780,7 @@ function AttachmentChips({
|
|
|
1716
1780
|
<div className="break-words text-og-xs leading-4 text-og-status-failed">
|
|
1717
1781
|
{statusText}
|
|
1718
1782
|
</div>
|
|
1719
|
-
) : failed ? (
|
|
1783
|
+
) : failed || previewFailed ? (
|
|
1720
1784
|
<ComposerTip tip={statusText}>
|
|
1721
1785
|
<div className="truncate text-og-xs text-og-status-failed">{statusText}</div>
|
|
1722
1786
|
</ComposerTip>
|
|
@@ -1183,17 +1183,23 @@ export function MessageTimeline({
|
|
|
1183
1183
|
applyPinned(false);
|
|
1184
1184
|
}
|
|
1185
1185
|
} else if (prepended) {
|
|
1186
|
+
const olderAttemptState = olderLoadAttemptRef.current?.[1];
|
|
1187
|
+
const underfillOwned =
|
|
1188
|
+
!!olderLoadAttemptRef.current &&
|
|
1189
|
+
olderAttemptState !== 1 &&
|
|
1190
|
+
olderAttemptState !== 2 &&
|
|
1191
|
+
olderAttemptState !== 3;
|
|
1186
1192
|
if (
|
|
1187
1193
|
autoFollow &&
|
|
1188
1194
|
pinnedRef.current &&
|
|
1189
1195
|
!hasNewer &&
|
|
1190
1196
|
!pendingReaderLeaveRef.current &&
|
|
1191
|
-
(wasAtLiveTailBeforeCommit ||
|
|
1197
|
+
(wasAtLiveTailBeforeCommit || underfillOwned)
|
|
1192
1198
|
) {
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
1199
|
+
// Still following the live tip: underfill, or a prefetch the reader
|
|
1200
|
+
// started and then returned from. Park at the new tip. A stale pin
|
|
1201
|
+
// while they are actually up in a short window (gap is not inside
|
|
1202
|
+
// PIN_THRESHOLD of maxScroll, so wasAtLiveTail is false) restores.
|
|
1197
1203
|
clearPendingReaderLeave();
|
|
1198
1204
|
snapToBottom(node);
|
|
1199
1205
|
} else {
|
|
@@ -1598,6 +1604,19 @@ export function MessageTimeline({
|
|
|
1598
1604
|
return;
|
|
1599
1605
|
}
|
|
1600
1606
|
|
|
1607
|
+
if (programmatic && !pinnedRef.current) {
|
|
1608
|
+
// Restore/camera writes while reading history are not a return to the tip.
|
|
1609
|
+
// Still expire a pending Jump-to-latest latch: the in-window jump itself
|
|
1610
|
+
// is a programmatic snap, and a later reader scroll-away can arrive
|
|
1611
|
+
// before that echo is consumed. Skipping this left the latch armed and
|
|
1612
|
+
// snapped the reader when hasNewer later flipped false.
|
|
1613
|
+
syncScrollBaseline(node);
|
|
1614
|
+
if (wantPinRef.current && !isNearBottom(node)) {
|
|
1615
|
+
wantPinRef.current = false;
|
|
1616
|
+
}
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1601
1620
|
if (autoFollow && pinnedRef.current && !hasNewer) {
|
|
1602
1621
|
// Fold / composer / SessionChrome: viewport shrink raises maxScroll without
|
|
1603
1622
|
// growing content. Must hit tipFollow before we adopt the new clientHeight
|
|
@@ -1659,10 +1678,15 @@ export function MessageTimeline({
|
|
|
1659
1678
|
syncScrollBaseline(node);
|
|
1660
1679
|
const nearBottom = isNearBottom(node);
|
|
1661
1680
|
|
|
1662
|
-
// Re-pin only when the reader moved toward/at the tip
|
|
1663
|
-
//
|
|
1681
|
+
// Re-pin only when the reader moved toward/at the tip without a content
|
|
1682
|
+
// insertion. Prepend restore and overflow-anchor raise scrollTop by
|
|
1683
|
+
// roughly the same amount as maxScroll; treating that as a scroll-down
|
|
1684
|
+
// re-pinned a compact-tail history reader (their preserved gap falls
|
|
1685
|
+
// inside PIN_THRESHOLD once the window is tall) and snapped them back.
|
|
1686
|
+
const inserted = Math.max(0, nextMaxScroll - previousMaxScroll);
|
|
1687
|
+
const towardTip = nextTop - previousTop - inserted;
|
|
1664
1688
|
const nextPinned =
|
|
1665
|
-
!hasNewer && nearBottom &&
|
|
1689
|
+
!hasNewer && nearBottom && towardTip >= -TIP_FOLLOW_READER_UP_EPS_PX && inserted <= 1;
|
|
1666
1690
|
if (!nextPinned) {
|
|
1667
1691
|
stopFollow();
|
|
1668
1692
|
}
|