@opengeni/react 0.15.0 → 0.23.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/README.md +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
- package/dist/chunk-6XUS5VFM.js.map +1 -0
- package/dist/chunk-CATATY77.js +2393 -0
- package/dist/chunk-CATATY77.js.map +1 -0
- package/dist/chunk-I3BJZIG5.js +439 -0
- package/dist/chunk-I3BJZIG5.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/chunk-UAHPKLB6.js +2237 -0
- package/dist/chunk-UAHPKLB6.js.map +1 -0
- package/dist/chunk-UQS7OQDE.js +591 -0
- package/dist/chunk-UQS7OQDE.js.map +1 -0
- package/dist/chunk-YDNWMKXO.js +102 -0
- package/dist/chunk-YDNWMKXO.js.map +1 -0
- package/dist/composer-CVm5uoUI.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +1288 -1785
- package/dist/index.js +7140 -8817
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js +890 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
- package/dist/session-Db00TRwl.d.ts +446 -0
- package/dist/session-context-rgrfElMl.d.ts +58 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +54 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-By1wn3MM.d.ts +99 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +44 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +205 -25
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-draft-policy.ts +18 -0
- package/src/components/queue-surface-implementation.tsx +1037 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +42 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +385 -104
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +85 -6
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/provider.tsx +4 -69
- package/src/session-context.ts +160 -0
- package/src/session.ts +89 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +10 -9
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
|
@@ -7,13 +7,13 @@ import type {
|
|
|
7
7
|
SendMessageInput,
|
|
8
8
|
SessionEvent,
|
|
9
9
|
} from "@opengeni/sdk";
|
|
10
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
11
|
-
import {
|
|
10
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
11
|
+
import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
|
|
12
12
|
import { useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
|
|
13
13
|
|
|
14
14
|
export type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId">;
|
|
15
15
|
|
|
16
|
-
export type UseComposerOptions =
|
|
16
|
+
export type UseComposerOptions = EmbeddedSessionClientOverride &
|
|
17
17
|
SessionEventFeedOptions & {
|
|
18
18
|
/** Called with the accepted text after a successful send. */
|
|
19
19
|
onSent?: ((text: string) => void) | undefined;
|
|
@@ -27,11 +27,15 @@ export type UseComposerOptions = ClientOverride &
|
|
|
27
27
|
effectiveControl?: EffectiveSessionControl | null | undefined;
|
|
28
28
|
/** Apply durable model/tool/reasoning settings in the host's controlled UI. */
|
|
29
29
|
onDraftApplied?: ((draft: ComposerDraft) => void) | undefined;
|
|
30
|
+
/** Disable remote composer-draft reads and writes for embedded hosts. */
|
|
31
|
+
draftPersistence?: "durable" | "disabled" | undefined;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
export type ComposerState = {
|
|
33
35
|
value: string;
|
|
34
36
|
setValue: (value: string) => void;
|
|
37
|
+
/** Read the current draft synchronously before a destructive replacement. */
|
|
38
|
+
hasDraftContent: () => boolean;
|
|
35
39
|
/** Append the draft behind prompts already visible in the queue. */
|
|
36
40
|
send: (text?: string) => Promise<boolean>;
|
|
37
41
|
/** Supersede current direction with the draft. */
|
|
@@ -49,6 +53,8 @@ export type ComposerState = {
|
|
|
49
53
|
draftLoading: boolean;
|
|
50
54
|
draftSaving: boolean;
|
|
51
55
|
draftConflict: Error | null;
|
|
56
|
+
/** Whether this controller owns a durable server-side draft. */
|
|
57
|
+
draftPersistence?: "durable" | "disabled" | undefined;
|
|
52
58
|
/** Apply an atomic queue Edit checkout without a second read. */
|
|
53
59
|
applyDraft: (draft: ComposerDraft) => void;
|
|
54
60
|
reloadDraft: () => Promise<void>;
|
|
@@ -69,21 +75,29 @@ export function useComposer(
|
|
|
69
75
|
sessionId: string | null | undefined,
|
|
70
76
|
options: UseComposerOptions = {},
|
|
71
77
|
): ComposerState {
|
|
72
|
-
const { client, workspaceId, registerSessionReconciler } =
|
|
78
|
+
const { client, workspaceId, registerSessionReconciler } = useEmbeddedSession(options);
|
|
79
|
+
const durableDrafts = options.draftPersistence !== "disabled";
|
|
80
|
+
const targetKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${durableDrafts ? "durable" : "disabled"}`;
|
|
73
81
|
const [value, setValue] = useState("");
|
|
82
|
+
// Keep rendered state behind the committed target identity for one frame:
|
|
83
|
+
// a parent may switch sessionId without remounting this public hook.
|
|
84
|
+
const [stateTargetKey, setStateTargetKey] = useState(targetKey);
|
|
74
85
|
const [sending, setSending] = useState(false);
|
|
75
86
|
const [pausing, setPausing] = useState(false);
|
|
76
87
|
const [resuming, setResuming] = useState(false);
|
|
77
88
|
const [error, setError] = useState<Error | null>(null);
|
|
78
89
|
const [draft, setDraft] = useState<ComposerDraft | null>(null);
|
|
79
|
-
const [draftLoading, setDraftLoading] = useState(Boolean(sessionId));
|
|
90
|
+
const [draftLoading, setDraftLoading] = useState(Boolean(sessionId) && durableDrafts);
|
|
80
91
|
const [draftSaving, setDraftSaving] = useState(false);
|
|
81
92
|
const [draftConflict, setDraftConflict] = useState<Error | null>(null);
|
|
82
93
|
const [restoredResources, setRestoredResources] = useState<ResourceRef[]>([]);
|
|
83
94
|
const pendingClientEventId = useRef<string | null>(null);
|
|
95
|
+
const valueRef = useRef("");
|
|
84
96
|
const draftRef = useRef<ComposerDraft | null>(null);
|
|
97
|
+
const restoredResourcesRef = useRef<ResourceRef[]>([]);
|
|
85
98
|
const localEditRevision = useRef(0);
|
|
86
99
|
const targetGeneration = useRef(0);
|
|
100
|
+
const draftReadGeneration = useRef(0);
|
|
87
101
|
const lastSavedSignature = useRef<string | null>(null);
|
|
88
102
|
const saveChain = useRef<Promise<void>>(Promise.resolve());
|
|
89
103
|
const onSent = options.onSent;
|
|
@@ -96,27 +110,53 @@ export function useComposer(
|
|
|
96
110
|
|
|
97
111
|
// A composer is bound to one session: switching targets must not leak the
|
|
98
112
|
// previous session's draft, error, or retry idempotency key.
|
|
99
|
-
const targetKey = `${workspaceId}\u0000${sessionId ?? ""}`;
|
|
100
113
|
const targetKeyRef = useRef(targetKey);
|
|
101
|
-
|
|
102
|
-
if (targetKeyRef.current
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
useLayoutEffect(() => {
|
|
115
|
+
if (targetKeyRef.current === targetKey) return;
|
|
116
|
+
targetKeyRef.current = targetKey;
|
|
117
|
+
targetGeneration.current += 1;
|
|
118
|
+
draftReadGeneration.current += 1;
|
|
119
|
+
pendingClientEventId.current = null;
|
|
120
|
+
localEditRevision.current = 0;
|
|
121
|
+
valueRef.current = "";
|
|
122
|
+
draftRef.current = null;
|
|
123
|
+
restoredResourcesRef.current = [];
|
|
124
|
+
lastSavedSignature.current = null;
|
|
125
|
+
// Old saves may still be awaiting the network. Their generation fence
|
|
126
|
+
// prevents settlement, and a fresh chain avoids blocking this target.
|
|
127
|
+
saveChain.current = Promise.resolve();
|
|
128
|
+
setStateTargetKey(targetKey);
|
|
129
|
+
setValue("");
|
|
130
|
+
setSending(false);
|
|
131
|
+
setPausing(false);
|
|
132
|
+
setResuming(false);
|
|
133
|
+
setError(null);
|
|
134
|
+
setDraft(null);
|
|
135
|
+
setDraftLoading(Boolean(sessionId) && durableDrafts);
|
|
136
|
+
setDraftSaving(false);
|
|
137
|
+
setDraftConflict(null);
|
|
138
|
+
setRestoredResources([]);
|
|
139
|
+
}, [durableDrafts, sessionId, targetKey]);
|
|
116
140
|
|
|
117
141
|
const applyDraft = useCallback(
|
|
118
142
|
(next: ComposerDraft): void => {
|
|
143
|
+
if (targetKeyRef.current !== targetKey) return;
|
|
144
|
+
if (!durableDrafts) {
|
|
145
|
+
pendingClientEventId.current = null;
|
|
146
|
+
localEditRevision.current += 1;
|
|
147
|
+
valueRef.current = next.text;
|
|
148
|
+
restoredResourcesRef.current = next.resources;
|
|
149
|
+
draftRef.current = null;
|
|
150
|
+
lastSavedSignature.current = null;
|
|
151
|
+
setDraft(null);
|
|
152
|
+
setValue(next.text);
|
|
153
|
+
setRestoredResources(next.resources);
|
|
154
|
+
setDraftConflict(null);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
valueRef.current = next.text;
|
|
119
158
|
draftRef.current = next;
|
|
159
|
+
restoredResourcesRef.current = next.resources;
|
|
120
160
|
lastSavedSignature.current = draftSignature(draftPayload(next));
|
|
121
161
|
localEditRevision.current += 1;
|
|
122
162
|
pendingClientEventId.current = null;
|
|
@@ -126,47 +166,94 @@ export function useComposer(
|
|
|
126
166
|
setDraftConflict(null);
|
|
127
167
|
onDraftApplied?.(next);
|
|
128
168
|
},
|
|
129
|
-
[onDraftApplied],
|
|
169
|
+
[durableDrafts, onDraftApplied, targetKey],
|
|
130
170
|
);
|
|
131
171
|
|
|
132
172
|
const loadDraft = useCallback(
|
|
133
173
|
async (replaceLocal: boolean): Promise<void> => {
|
|
134
|
-
if (
|
|
174
|
+
if (targetKeyRef.current !== targetKey) return;
|
|
175
|
+
if (!sessionId || !durableDrafts) {
|
|
176
|
+
setDraftLoading(false);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
135
179
|
const generation = targetGeneration.current;
|
|
180
|
+
const readTicket = ++draftReadGeneration.current;
|
|
136
181
|
const localAtStart = localEditRevision.current;
|
|
182
|
+
const baseAtStart = draftRef.current;
|
|
183
|
+
const extrasAtStart = resolveSendExtras(sendExtrasRef.current);
|
|
184
|
+
const localSignatureAtStart = baseAtStart
|
|
185
|
+
? draftSignature(
|
|
186
|
+
composerDraftPayload(
|
|
187
|
+
baseAtStart,
|
|
188
|
+
valueRef.current,
|
|
189
|
+
restoredResourcesRef.current,
|
|
190
|
+
extrasAtStart,
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
: null;
|
|
194
|
+
const localWasDirtyAtStart =
|
|
195
|
+
localSignatureAtStart === null
|
|
196
|
+
? localAtStart !== 0
|
|
197
|
+
: localSignatureAtStart !== lastSavedSignature.current;
|
|
137
198
|
setDraftLoading(true);
|
|
138
199
|
try {
|
|
139
200
|
const fetched = await client.getComposerDraft(workspaceId, sessionId);
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
201
|
+
if (
|
|
202
|
+
generation !== targetGeneration.current ||
|
|
203
|
+
targetKeyRef.current !== targetKey ||
|
|
204
|
+
readTicket !== draftReadGeneration.current
|
|
205
|
+
) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const currentRevision = draftRef.current?.revision ?? -1;
|
|
209
|
+
if (fetched.revision >= currentRevision) {
|
|
210
|
+
draftRef.current = fetched;
|
|
211
|
+
setDraft(fetched);
|
|
212
|
+
setDraftConflict(null);
|
|
213
|
+
if (
|
|
214
|
+
replaceLocal ||
|
|
215
|
+
(!localWasDirtyAtStart && localAtStart === localEditRevision.current)
|
|
216
|
+
) {
|
|
217
|
+
valueRef.current = fetched.text;
|
|
218
|
+
restoredResourcesRef.current = fetched.resources;
|
|
219
|
+
lastSavedSignature.current = draftSignature(draftPayload(fetched));
|
|
220
|
+
setValue(fetched.text);
|
|
221
|
+
setRestoredResources(fetched.resources);
|
|
222
|
+
onDraftApplied?.(fetched);
|
|
223
|
+
}
|
|
149
224
|
}
|
|
150
225
|
} catch (cause) {
|
|
151
|
-
if (
|
|
226
|
+
if (
|
|
227
|
+
generation === targetGeneration.current &&
|
|
228
|
+
targetKeyRef.current === targetKey &&
|
|
229
|
+
readTicket === draftReadGeneration.current
|
|
230
|
+
) {
|
|
231
|
+
setError(asError(cause));
|
|
232
|
+
}
|
|
152
233
|
} finally {
|
|
153
|
-
if (
|
|
234
|
+
if (
|
|
235
|
+
generation === targetGeneration.current &&
|
|
236
|
+
targetKeyRef.current === targetKey &&
|
|
237
|
+
readTicket === draftReadGeneration.current
|
|
238
|
+
) {
|
|
239
|
+
setDraftLoading(false);
|
|
240
|
+
}
|
|
154
241
|
}
|
|
155
242
|
},
|
|
156
|
-
[client, onDraftApplied, sessionId, workspaceId],
|
|
243
|
+
[client, durableDrafts, onDraftApplied, sessionId, targetKey, workspaceId],
|
|
157
244
|
);
|
|
158
245
|
|
|
159
246
|
useEffect(() => {
|
|
160
|
-
if (!sessionId) {
|
|
247
|
+
if (!sessionId || !durableDrafts) {
|
|
161
248
|
setDraftLoading(false);
|
|
162
249
|
return;
|
|
163
250
|
}
|
|
164
251
|
void loadDraft(false);
|
|
165
|
-
}, [loadDraft, sessionId]);
|
|
252
|
+
}, [durableDrafts, loadDraft, sessionId]);
|
|
166
253
|
useEffect(() => {
|
|
167
|
-
if (!sessionId) return;
|
|
254
|
+
if (!sessionId || !durableDrafts) return;
|
|
168
255
|
return registerSessionReconciler(sessionId, "composer", async () => await loadDraft(false));
|
|
169
|
-
}, [loadDraft, registerSessionReconciler, sessionId]);
|
|
256
|
+
}, [durableDrafts, loadDraft, registerSessionReconciler, sessionId]);
|
|
170
257
|
useSessionEventTrigger(
|
|
171
258
|
client,
|
|
172
259
|
workspaceId,
|
|
@@ -174,30 +261,34 @@ export function useComposer(
|
|
|
174
261
|
isComposerDraftEvent,
|
|
175
262
|
() => void loadDraft(false),
|
|
176
263
|
{
|
|
177
|
-
enabled: Boolean(sessionId),
|
|
264
|
+
enabled: Boolean(sessionId) && durableDrafts,
|
|
178
265
|
...(options.events !== undefined ? { events: options.events } : {}),
|
|
179
266
|
},
|
|
180
267
|
);
|
|
181
268
|
|
|
182
269
|
const currentDraftPayload = useCallback((): SaveComposerDraftRequest | null => {
|
|
270
|
+
if (!durableDrafts || targetKeyRef.current !== targetKey) return null;
|
|
183
271
|
const base = draftRef.current;
|
|
184
272
|
if (!base) return null;
|
|
185
273
|
const extras = resolveSendExtras(sendExtrasRef.current);
|
|
186
|
-
return
|
|
187
|
-
|
|
188
|
-
text: value,
|
|
189
|
-
resources: mergeResources(restoredResources, extras.resources ?? []),
|
|
190
|
-
tools: extras.tools ?? base.tools,
|
|
191
|
-
model: extras.model ?? base.model,
|
|
192
|
-
reasoningEffort: extras.reasoningEffort ?? base.reasoningEffort,
|
|
193
|
-
};
|
|
194
|
-
}, [restoredResources, value]);
|
|
274
|
+
return composerDraftPayload(base, value, restoredResources, extras);
|
|
275
|
+
}, [durableDrafts, restoredResources, targetKey, value]);
|
|
195
276
|
|
|
196
277
|
const persistPayload = useCallback(
|
|
197
278
|
async (payload: SaveComposerDraftRequest): Promise<boolean> => {
|
|
198
|
-
|
|
279
|
+
const ownedTargetKey = targetKey;
|
|
280
|
+
const ownedGeneration = targetGeneration.current;
|
|
281
|
+
if (!sessionId || !durableDrafts || targetKeyRef.current !== ownedTargetKey) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
199
284
|
let success = false;
|
|
200
285
|
const run = async () => {
|
|
286
|
+
if (
|
|
287
|
+
targetKeyRef.current !== ownedTargetKey ||
|
|
288
|
+
targetGeneration.current !== ownedGeneration
|
|
289
|
+
) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
201
292
|
const current = draftRef.current;
|
|
202
293
|
if (!current) return;
|
|
203
294
|
const request = { ...payload, expectedRevision: current.revision };
|
|
@@ -209,36 +300,61 @@ export function useComposer(
|
|
|
209
300
|
setDraftSaving(true);
|
|
210
301
|
try {
|
|
211
302
|
const saved = await client.saveComposerDraft(workspaceId, sessionId, request);
|
|
303
|
+
if (
|
|
304
|
+
targetKeyRef.current !== ownedTargetKey ||
|
|
305
|
+
targetGeneration.current !== ownedGeneration
|
|
306
|
+
) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
212
309
|
draftRef.current = saved;
|
|
213
310
|
setDraft(saved);
|
|
214
311
|
lastSavedSignature.current = signature;
|
|
215
312
|
setDraftConflict(null);
|
|
216
313
|
success = true;
|
|
217
314
|
} catch (cause) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
315
|
+
if (
|
|
316
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
317
|
+
targetGeneration.current === ownedGeneration
|
|
318
|
+
) {
|
|
319
|
+
const problem = asError(cause);
|
|
320
|
+
setDraftConflict(problem);
|
|
321
|
+
setError(problem);
|
|
322
|
+
}
|
|
221
323
|
} finally {
|
|
222
|
-
|
|
324
|
+
if (
|
|
325
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
326
|
+
targetGeneration.current === ownedGeneration
|
|
327
|
+
) {
|
|
328
|
+
setDraftSaving(false);
|
|
329
|
+
}
|
|
223
330
|
}
|
|
224
331
|
};
|
|
225
332
|
saveChain.current = saveChain.current.then(run, run);
|
|
226
333
|
await saveChain.current;
|
|
227
334
|
return success;
|
|
228
335
|
},
|
|
229
|
-
[client, sessionId, workspaceId],
|
|
336
|
+
[client, durableDrafts, sessionId, targetKey, workspaceId],
|
|
230
337
|
);
|
|
231
338
|
|
|
232
339
|
// Private durable autosave. A newer local edit is never replaced by an older
|
|
233
340
|
// response; saves serialize and each reads the latest acknowledged revision.
|
|
234
341
|
useEffect(() => {
|
|
235
|
-
if (
|
|
342
|
+
if (
|
|
343
|
+
!durableDrafts ||
|
|
344
|
+
!sessionId ||
|
|
345
|
+
draftLoading ||
|
|
346
|
+
sending ||
|
|
347
|
+
!draftRef.current ||
|
|
348
|
+
draftConflict
|
|
349
|
+
)
|
|
350
|
+
return;
|
|
236
351
|
const payload = currentDraftPayload();
|
|
237
352
|
if (!payload || draftSignature(payload) === lastSavedSignature.current) return;
|
|
238
353
|
const timer = window.setTimeout(() => void persistPayload(payload), 500);
|
|
239
354
|
return () => window.clearTimeout(timer);
|
|
240
355
|
}, [
|
|
241
356
|
currentDraftPayload,
|
|
357
|
+
durableDrafts,
|
|
242
358
|
draftConflict,
|
|
243
359
|
draftLoading,
|
|
244
360
|
liveExtrasVersion,
|
|
@@ -249,13 +365,21 @@ export function useComposer(
|
|
|
249
365
|
|
|
250
366
|
const dispatch = useCallback(
|
|
251
367
|
async (delivery: "send" | "steer", explicit?: string): Promise<boolean> => {
|
|
368
|
+
const ownedTargetKey = targetKey;
|
|
369
|
+
const ownedGeneration = targetGeneration.current;
|
|
252
370
|
const draftAtSend = value;
|
|
253
|
-
const
|
|
371
|
+
const rawText = explicit ?? draftAtSend;
|
|
372
|
+
const hasText = rawText.trim().length > 0;
|
|
254
373
|
// Resolve the extras once: a file-only message (empty text + ≥1 ready
|
|
255
374
|
// resource) is legitimate, so we must not bail on empty text alone.
|
|
256
375
|
const extras = resolveSendExtras(sendExtrasRef.current);
|
|
257
376
|
const hasResources = restoredResources.length > 0 || (extras.resources?.length ?? 0) > 0;
|
|
258
|
-
if (
|
|
377
|
+
if (
|
|
378
|
+
(!hasText && !hasResources) ||
|
|
379
|
+
!sessionId ||
|
|
380
|
+
sending ||
|
|
381
|
+
targetKeyRef.current !== ownedTargetKey
|
|
382
|
+
) {
|
|
259
383
|
return false;
|
|
260
384
|
}
|
|
261
385
|
// Reuse the clientEventId across retries of the same draft so a
|
|
@@ -264,17 +388,36 @@ export function useComposer(
|
|
|
264
388
|
setSending(true);
|
|
265
389
|
setError(null);
|
|
266
390
|
try {
|
|
267
|
-
|
|
391
|
+
// Trimming is only an emptiness check. A non-blank prompt is persisted
|
|
392
|
+
// and submitted byte-for-byte, while file-only sends use the same
|
|
393
|
+
// placeholder for both operations so the server content fence cannot
|
|
394
|
+
// reject its own client.
|
|
395
|
+
const sendText = hasText ? rawText : FILE_ONLY_MESSAGE_TEXT;
|
|
396
|
+
const currentPayload = currentDraftPayload();
|
|
397
|
+
const payload = currentPayload ? { ...currentPayload, text: sendText } : null;
|
|
268
398
|
if (payload && !(await persistPayload(payload))) return false;
|
|
399
|
+
if (
|
|
400
|
+
targetKeyRef.current !== ownedTargetKey ||
|
|
401
|
+
targetGeneration.current !== ownedGeneration
|
|
402
|
+
) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
269
405
|
// The wire contract requires non-empty text (z.string().min(1)) and the
|
|
270
406
|
// worker rejects whitespace-only text; a file-only message therefore
|
|
271
407
|
// carries a minimal default so the attachments still get delivered.
|
|
272
|
-
const
|
|
273
|
-
const
|
|
408
|
+
const acknowledgedDraft = draftRef.current;
|
|
409
|
+
const sendExtras =
|
|
410
|
+
acknowledgedDraft?.toolsProvided === true &&
|
|
411
|
+
!Object.prototype.hasOwnProperty.call(extras, "tools")
|
|
412
|
+
? { ...extras, tools: acknowledgedDraft.tools }
|
|
413
|
+
: extras;
|
|
414
|
+
const input = composeSendInput(sendText, pendingClientEventId.current, sendExtras, {
|
|
274
415
|
...(options.effectiveControl?.controlEtag
|
|
275
416
|
? { controlEtag: options.effectiveControl.controlEtag }
|
|
276
417
|
: {}),
|
|
277
|
-
...(
|
|
418
|
+
...(durableDrafts && draftRef.current
|
|
419
|
+
? { expectedDraftRevision: draftRef.current.revision }
|
|
420
|
+
: {}),
|
|
278
421
|
resources: mergeResources(restoredResources, extras.resources ?? []),
|
|
279
422
|
});
|
|
280
423
|
if (delivery === "steer") {
|
|
@@ -282,6 +425,12 @@ export function useComposer(
|
|
|
282
425
|
} else {
|
|
283
426
|
await client.sendMessage(workspaceId, sessionId, input);
|
|
284
427
|
}
|
|
428
|
+
if (
|
|
429
|
+
targetKeyRef.current !== ownedTargetKey ||
|
|
430
|
+
targetGeneration.current !== ownedGeneration
|
|
431
|
+
) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
285
434
|
pendingClientEventId.current = null;
|
|
286
435
|
const previousDraft = draftRef.current;
|
|
287
436
|
if (previousDraft) {
|
|
@@ -295,6 +444,7 @@ export function useComposer(
|
|
|
295
444
|
updatedAt: null,
|
|
296
445
|
};
|
|
297
446
|
draftRef.current = cleared;
|
|
447
|
+
restoredResourcesRef.current = [];
|
|
298
448
|
setDraft(cleared);
|
|
299
449
|
setRestoredResources([]);
|
|
300
450
|
lastSavedSignature.current = draftSignature(draftPayload(cleared));
|
|
@@ -302,26 +452,41 @@ export function useComposer(
|
|
|
302
452
|
if (explicit === undefined) {
|
|
303
453
|
// Clear only the draft that was sent: edits made while the request
|
|
304
454
|
// was in flight were never delivered and must survive.
|
|
305
|
-
|
|
455
|
+
if (valueRef.current === draftAtSend) {
|
|
456
|
+
valueRef.current = "";
|
|
457
|
+
setValue("");
|
|
458
|
+
}
|
|
306
459
|
}
|
|
307
460
|
onSent?.(sendText);
|
|
308
461
|
return true;
|
|
309
462
|
} catch (cause) {
|
|
310
|
-
|
|
463
|
+
if (
|
|
464
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
465
|
+
targetGeneration.current === ownedGeneration
|
|
466
|
+
) {
|
|
467
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
468
|
+
}
|
|
311
469
|
return false;
|
|
312
470
|
} finally {
|
|
313
|
-
|
|
471
|
+
if (
|
|
472
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
473
|
+
targetGeneration.current === ownedGeneration
|
|
474
|
+
) {
|
|
475
|
+
setSending(false);
|
|
476
|
+
}
|
|
314
477
|
}
|
|
315
478
|
},
|
|
316
479
|
[
|
|
317
480
|
client,
|
|
318
481
|
currentDraftPayload,
|
|
482
|
+
durableDrafts,
|
|
319
483
|
onSent,
|
|
320
484
|
options.effectiveControl?.controlEtag,
|
|
321
485
|
persistPayload,
|
|
322
486
|
restoredResources,
|
|
323
487
|
sending,
|
|
324
488
|
sessionId,
|
|
489
|
+
targetKey,
|
|
325
490
|
value,
|
|
326
491
|
workspaceId,
|
|
327
492
|
],
|
|
@@ -341,7 +506,9 @@ export function useComposer(
|
|
|
341
506
|
|
|
342
507
|
const pause = useCallback(
|
|
343
508
|
async (reason?: string): Promise<void> => {
|
|
344
|
-
|
|
509
|
+
const ownedTargetKey = targetKey;
|
|
510
|
+
const ownedGeneration = targetGeneration.current;
|
|
511
|
+
if (!sessionId || pausing || targetKeyRef.current !== ownedTargetKey) {
|
|
345
512
|
return;
|
|
346
513
|
}
|
|
347
514
|
setPausing(true);
|
|
@@ -354,17 +521,29 @@ export function useComposer(
|
|
|
354
521
|
: {}),
|
|
355
522
|
});
|
|
356
523
|
} catch (cause) {
|
|
357
|
-
|
|
524
|
+
if (
|
|
525
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
526
|
+
targetGeneration.current === ownedGeneration
|
|
527
|
+
) {
|
|
528
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
529
|
+
}
|
|
358
530
|
} finally {
|
|
359
|
-
|
|
531
|
+
if (
|
|
532
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
533
|
+
targetGeneration.current === ownedGeneration
|
|
534
|
+
) {
|
|
535
|
+
setPausing(false);
|
|
536
|
+
}
|
|
360
537
|
}
|
|
361
538
|
},
|
|
362
|
-
[client, workspaceId, sessionId, pausing, options.effectiveControl?.controlEtag],
|
|
539
|
+
[client, workspaceId, sessionId, pausing, options.effectiveControl?.controlEtag, targetKey],
|
|
363
540
|
);
|
|
364
541
|
|
|
365
542
|
const resume = useCallback(
|
|
366
543
|
async (reason?: string): Promise<void> => {
|
|
367
|
-
|
|
544
|
+
const ownedTargetKey = targetKey;
|
|
545
|
+
const ownedGeneration = targetGeneration.current;
|
|
546
|
+
if (!sessionId || resuming || targetKeyRef.current !== ownedTargetKey) return;
|
|
368
547
|
setResuming(true);
|
|
369
548
|
setError(null);
|
|
370
549
|
try {
|
|
@@ -375,17 +554,29 @@ export function useComposer(
|
|
|
375
554
|
: {}),
|
|
376
555
|
});
|
|
377
556
|
} catch (cause) {
|
|
378
|
-
|
|
557
|
+
if (
|
|
558
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
559
|
+
targetGeneration.current === ownedGeneration
|
|
560
|
+
) {
|
|
561
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
562
|
+
}
|
|
379
563
|
} finally {
|
|
380
|
-
|
|
564
|
+
if (
|
|
565
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
566
|
+
targetGeneration.current === ownedGeneration
|
|
567
|
+
) {
|
|
568
|
+
setResuming(false);
|
|
569
|
+
}
|
|
381
570
|
}
|
|
382
571
|
},
|
|
383
|
-
[client, workspaceId, sessionId, resuming, options.effectiveControl?.controlEtag],
|
|
572
|
+
[client, workspaceId, sessionId, resuming, options.effectiveControl?.controlEtag, targetKey],
|
|
384
573
|
);
|
|
385
574
|
|
|
386
575
|
const resumeScope = useCallback(
|
|
387
576
|
async (option: EffectiveControlResumeOption): Promise<void> => {
|
|
388
|
-
|
|
577
|
+
const ownedTargetKey = targetKey;
|
|
578
|
+
const ownedGeneration = targetGeneration.current;
|
|
579
|
+
if (!sessionId || resuming || targetKeyRef.current !== ownedTargetKey) return;
|
|
389
580
|
setResuming(true);
|
|
390
581
|
setError(null);
|
|
391
582
|
try {
|
|
@@ -393,6 +584,11 @@ export function useComposer(
|
|
|
393
584
|
const workspaceBlocker = options.effectiveControl?.blockers.find(
|
|
394
585
|
(blocker) => blocker.kind === "workspace",
|
|
395
586
|
);
|
|
587
|
+
if (!client.setWorkspaceInferenceState) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
"@opengeni/react: workspace-scoped resume requires setWorkspaceInferenceState.",
|
|
590
|
+
);
|
|
591
|
+
}
|
|
396
592
|
await client.setWorkspaceInferenceState(workspaceId, {
|
|
397
593
|
action: "resume",
|
|
398
594
|
clientEventId: generateClientEventId(),
|
|
@@ -400,6 +596,12 @@ export function useComposer(
|
|
|
400
596
|
});
|
|
401
597
|
} else if (option.scope === "session" && option.targetId) {
|
|
402
598
|
const target = await client.getQueue(workspaceId, option.targetId);
|
|
599
|
+
if (
|
|
600
|
+
targetKeyRef.current !== ownedTargetKey ||
|
|
601
|
+
targetGeneration.current !== ownedGeneration
|
|
602
|
+
) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
403
605
|
await client.resumeSession(workspaceId, option.targetId, {
|
|
404
606
|
expectedControlEtag: target.effectiveControl.controlEtag,
|
|
405
607
|
});
|
|
@@ -411,29 +613,69 @@ export function useComposer(
|
|
|
411
613
|
});
|
|
412
614
|
}
|
|
413
615
|
} catch (cause) {
|
|
414
|
-
|
|
616
|
+
if (
|
|
617
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
618
|
+
targetGeneration.current === ownedGeneration
|
|
619
|
+
) {
|
|
620
|
+
setError(asError(cause));
|
|
621
|
+
}
|
|
415
622
|
} finally {
|
|
416
|
-
|
|
623
|
+
if (
|
|
624
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
625
|
+
targetGeneration.current === ownedGeneration
|
|
626
|
+
) {
|
|
627
|
+
setResuming(false);
|
|
628
|
+
}
|
|
417
629
|
}
|
|
418
630
|
},
|
|
419
|
-
[client, options.effectiveControl, resuming, sessionId, workspaceId],
|
|
631
|
+
[client, options.effectiveControl, resuming, sessionId, targetKey, workspaceId],
|
|
420
632
|
);
|
|
421
633
|
|
|
422
|
-
const updateValue = useCallback(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
634
|
+
const updateValue = useCallback(
|
|
635
|
+
(next: string) => {
|
|
636
|
+
if (targetKeyRef.current !== targetKey) return;
|
|
637
|
+
pendingClientEventId.current = null;
|
|
638
|
+
localEditRevision.current += 1;
|
|
639
|
+
valueRef.current = next;
|
|
640
|
+
setValue(next);
|
|
641
|
+
},
|
|
642
|
+
[targetKey],
|
|
643
|
+
);
|
|
427
644
|
|
|
428
|
-
const removeRestoredResource = useCallback(
|
|
429
|
-
|
|
430
|
-
|
|
645
|
+
const removeRestoredResource = useCallback(
|
|
646
|
+
(index: number) => {
|
|
647
|
+
if (targetKeyRef.current !== targetKey) return;
|
|
648
|
+
localEditRevision.current += 1;
|
|
649
|
+
const next = restoredResourcesRef.current.filter((_, candidate) => candidate !== index);
|
|
650
|
+
restoredResourcesRef.current = next;
|
|
651
|
+
setRestoredResources(next);
|
|
652
|
+
},
|
|
653
|
+
[targetKey],
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const hasDraftContent = useCallback((): boolean => {
|
|
657
|
+
const current = draftRef.current;
|
|
658
|
+
const extras = resolveSendExtras(sendExtrasRef.current);
|
|
659
|
+
const toolsProvidedByHost = Object.prototype.hasOwnProperty.call(extras, "tools");
|
|
660
|
+
const tools = toolsProvidedByHost ? (extras.tools ?? []) : (current?.tools ?? []);
|
|
661
|
+
return (
|
|
662
|
+
valueRef.current.length > 0 ||
|
|
663
|
+
restoredResourcesRef.current.length > 0 ||
|
|
664
|
+
(extras.resources?.length ?? 0) > 0 ||
|
|
665
|
+
tools.length > 0 ||
|
|
666
|
+
(current?.sourceTurnId !== null && current?.sourceTurnId !== undefined)
|
|
667
|
+
);
|
|
431
668
|
}, []);
|
|
432
669
|
|
|
433
670
|
const resolveDraftConflict = useCallback(
|
|
434
671
|
async (choice: "keep_mine" | "use_remote"): Promise<void> => {
|
|
435
|
-
|
|
672
|
+
const ownedTargetKey = targetKey;
|
|
673
|
+
const ownedGeneration = targetGeneration.current;
|
|
674
|
+
if (!sessionId || !durableDrafts || targetKeyRef.current !== ownedTargetKey) return;
|
|
436
675
|
const remote = await client.getComposerDraft(workspaceId, sessionId);
|
|
676
|
+
if (targetKeyRef.current !== ownedTargetKey || targetGeneration.current !== ownedGeneration) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
437
679
|
if (choice === "use_remote") {
|
|
438
680
|
applyDraft(remote);
|
|
439
681
|
return;
|
|
@@ -444,36 +686,56 @@ export function useComposer(
|
|
|
444
686
|
const payload = currentDraftPayload();
|
|
445
687
|
if (payload) await persistPayload({ ...payload, expectedRevision: remote.revision });
|
|
446
688
|
},
|
|
447
|
-
[
|
|
689
|
+
[
|
|
690
|
+
applyDraft,
|
|
691
|
+
client,
|
|
692
|
+
currentDraftPayload,
|
|
693
|
+
durableDrafts,
|
|
694
|
+
persistPayload,
|
|
695
|
+
sessionId,
|
|
696
|
+
targetKey,
|
|
697
|
+
workspaceId,
|
|
698
|
+
],
|
|
448
699
|
);
|
|
449
700
|
|
|
701
|
+
const identityMatches = stateTargetKey === targetKey;
|
|
702
|
+
const reloadDraft = useCallback(async () => await loadDraft(true), [loadDraft]);
|
|
703
|
+
const clearError = useCallback(() => {
|
|
704
|
+
if (targetKeyRef.current !== targetKey) return;
|
|
705
|
+
setError(null);
|
|
706
|
+
setDraftConflict(null);
|
|
707
|
+
}, [targetKey]);
|
|
708
|
+
|
|
450
709
|
return {
|
|
451
|
-
value,
|
|
710
|
+
value: identityMatches ? value : "",
|
|
452
711
|
setValue: updateValue,
|
|
712
|
+
hasDraftContent,
|
|
453
713
|
send,
|
|
454
714
|
steer,
|
|
455
|
-
sending,
|
|
456
|
-
canSend:
|
|
715
|
+
sending: identityMatches ? sending : false,
|
|
716
|
+
canSend:
|
|
717
|
+
identityMatches &&
|
|
718
|
+
Boolean(sessionId) &&
|
|
719
|
+
!sending &&
|
|
720
|
+
(value.trim().length > 0 || hasReadyResources),
|
|
457
721
|
pause,
|
|
458
|
-
pausing,
|
|
722
|
+
pausing: identityMatches ? pausing : false,
|
|
459
723
|
resume,
|
|
460
724
|
resumeScope,
|
|
461
|
-
resuming,
|
|
462
|
-
draft,
|
|
463
|
-
draftRevision: draft?.revision ?? 0,
|
|
464
|
-
draftLoading,
|
|
465
|
-
draftSaving,
|
|
466
|
-
draftConflict,
|
|
725
|
+
resuming: identityMatches ? resuming : false,
|
|
726
|
+
draft: identityMatches ? draft : null,
|
|
727
|
+
draftRevision: identityMatches ? (draft?.revision ?? 0) : 0,
|
|
728
|
+
draftLoading: identityMatches ? draftLoading : Boolean(sessionId) && durableDrafts,
|
|
729
|
+
draftSaving: identityMatches ? draftSaving : false,
|
|
730
|
+
draftConflict: identityMatches ? draftConflict : null,
|
|
731
|
+
draftPersistence: durableDrafts ? "durable" : "disabled",
|
|
467
732
|
applyDraft,
|
|
468
|
-
reloadDraft
|
|
733
|
+
reloadDraft,
|
|
469
734
|
resolveDraftConflict,
|
|
470
|
-
restoredResources,
|
|
735
|
+
restoredResources: identityMatches ? restoredResources : [],
|
|
471
736
|
removeRestoredResource,
|
|
472
|
-
error,
|
|
473
|
-
clearError
|
|
474
|
-
setError(null);
|
|
475
|
-
setDraftConflict(null);
|
|
476
|
-
}, []),
|
|
737
|
+
error: identityMatches ? error : null,
|
|
738
|
+
clearError,
|
|
477
739
|
};
|
|
478
740
|
}
|
|
479
741
|
|
|
@@ -543,11 +805,30 @@ function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
|
|
|
543
805
|
text: draft.text,
|
|
544
806
|
resources: draft.resources,
|
|
545
807
|
tools: draft.tools,
|
|
808
|
+
toolsProvided: draft.toolsProvided,
|
|
546
809
|
model: draft.model,
|
|
547
810
|
reasoningEffort: draft.reasoningEffort,
|
|
548
811
|
};
|
|
549
812
|
}
|
|
550
813
|
|
|
814
|
+
function composerDraftPayload(
|
|
815
|
+
base: ComposerDraft,
|
|
816
|
+
text: string,
|
|
817
|
+
restoredResources: ResourceRef[],
|
|
818
|
+
extras: ComposerSendExtras,
|
|
819
|
+
): SaveComposerDraftRequest {
|
|
820
|
+
const toolsProvidedByHost = Object.prototype.hasOwnProperty.call(extras, "tools");
|
|
821
|
+
return {
|
|
822
|
+
expectedRevision: base.revision,
|
|
823
|
+
text,
|
|
824
|
+
resources: mergeResources(restoredResources, extras.resources ?? []),
|
|
825
|
+
tools: toolsProvidedByHost ? (extras.tools ?? []) : base.tools,
|
|
826
|
+
toolsProvided: toolsProvidedByHost ? true : base.toolsProvided,
|
|
827
|
+
model: extras.model ?? base.model,
|
|
828
|
+
reasoningEffort: extras.reasoningEffort ?? base.reasoningEffort,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
551
832
|
function draftSignature(payload: SaveComposerDraftRequest): string {
|
|
552
833
|
const { expectedRevision: _revision, ...content } = payload;
|
|
553
834
|
return JSON.stringify(content);
|