@opengeni/react 0.45.0 → 0.48.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 +68 -12
- package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
- package/dist/chunk-FK6VG4LL.js.map +1 -0
- package/dist/{chunk-HFO4ERGQ.js → chunk-FWXGSNXT.js} +2 -2
- package/dist/{chunk-SRFUT2ZU.js → chunk-I4MPFDRP.js} +119 -63
- package/dist/chunk-I4MPFDRP.js.map +1 -0
- package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
- package/dist/chunk-KZ73RL76.js.map +1 -0
- package/dist/{chunk-LWR4MXSS.js → chunk-LHLYWG5R.js} +10 -1
- package/dist/chunk-LHLYWG5R.js.map +1 -0
- package/dist/{chunk-L3EWO3UK.js → chunk-NJ7M2B6Z.js} +445 -84
- package/dist/chunk-NJ7M2B6Z.js.map +1 -0
- package/dist/{chunk-VCBFUSUZ.js → chunk-O4VXOBGF.js} +3 -3
- package/dist/{chunk-VCBFUSUZ.js.map → chunk-O4VXOBGF.js.map} +1 -1
- package/dist/{chunk-6EDV6YFE.js → chunk-OL6QVXGF.js} +121 -29
- package/dist/chunk-OL6QVXGF.js.map +1 -0
- package/dist/chunk-VZTQ73XT.js +86 -0
- package/dist/chunk-VZTQ73XT.js.map +1 -0
- package/dist/{chunk-EHSYZ4KP.js → chunk-WYA65Y3F.js} +16 -16
- package/dist/chunk-WYA65Y3F.js.map +1 -0
- package/dist/{chunk-FD72GJMK.js → chunk-XCNTXWBB.js} +274 -125
- package/dist/chunk-XCNTXWBB.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/components/chat-composer.d.ts +7 -2
- package/dist/components/composer-transcription-control.d.ts +2 -0
- package/dist/components/composer.d.ts +14 -0
- package/dist/components/machines/machine-detail.d.ts +3 -1
- package/dist/components/machines-dashboard.d.ts +3 -3
- package/dist/components/message-timeline.d.ts +12 -2
- package/dist/components/sandbox-files.d.ts +2 -2
- package/dist/components/sandbox-terminal.d.ts +29 -0
- package/dist/components/sandbox-workspace.d.ts +5 -1
- package/dist/components/workspace-dock.d.ts +3 -1
- package/dist/composer.d.ts +2 -2
- package/dist/composer.js +6 -6
- package/dist/hooks/internal.d.ts +6 -0
- package/dist/hooks/use-codex-accounts.d.ts +1 -1
- package/dist/hooks/use-environments.d.ts +10 -4
- package/dist/hooks/use-machines.d.ts +7 -0
- package/dist/hooks/use-sandbox-files.d.ts +19 -0
- package/dist/hooks/use-sandbox-git.d.ts +7 -0
- package/dist/hooks/use-transcription.d.ts +2 -2
- package/dist/hooks/use-voice-input.d.ts +13 -4
- package/dist/index.d.ts +6 -5
- package/dist/index.js +904 -538
- package/dist/index.js.map +1 -1
- package/dist/lib/composer-responsive-context.d.ts +9 -0
- package/dist/lib/use-portal-token-style.d.ts +14 -3
- package/dist/machines.js +2 -2
- package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-U5JFG7HT.js} +19 -18
- package/dist/queue-surface-implementation-U5JFG7HT.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +9 -0
- package/dist/realtime.js +29 -32
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.js +5 -5
- package/dist/session.js +4 -4
- package/dist/timeline/activity-rail.d.ts +3 -2
- package/dist/timeline/index.d.ts +2 -2
- package/dist/timeline/parsers.d.ts +3 -3
- package/dist/timeline/registry.d.ts +3 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/types.d.ts +4 -0
- package/dist/voice-recording-store.d.ts +8 -2
- package/package.json +20 -4
- package/src/client.ts +3 -0
- package/src/components/chat-composer.tsx +12 -2
- package/src/components/code-editor.tsx +1 -1
- package/src/components/command-palette.tsx +1 -1
- package/src/components/composer-transcription-control.tsx +41 -16
- package/src/components/composer.tsx +52 -26
- package/src/components/copy-button.tsx +2 -2
- package/src/components/desktop-viewer.tsx +2 -2
- package/src/components/file-browser.tsx +39 -17
- package/src/components/human-input-form.tsx +2 -2
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines/machine-detail.tsx +39 -15
- package/src/components/machines-dashboard.tsx +7 -6
- package/src/components/message-timeline.tsx +44 -11
- package/src/components/model-picker.tsx +2 -2
- package/src/components/model-policy-picker.tsx +16 -15
- package/src/components/queue-surface-implementation.tsx +20 -16
- package/src/components/queue-surface-state.tsx +2 -2
- package/src/components/sandbox-files.tsx +140 -60
- package/src/components/sandbox-terminal.tsx +24 -5
- package/src/components/sandbox-workspace.tsx +180 -142
- package/src/components/session-chrome.tsx +14 -14
- package/src/components/tooltip.tsx +13 -16
- package/src/components/user-message-body.tsx +1 -1
- package/src/components/workbench-changes.tsx +4 -4
- package/src/components/workspace-dock.tsx +13 -5
- package/src/composer.ts +3 -0
- package/src/hooks/internal.ts +132 -38
- package/src/hooks/use-codex-accounts.ts +4 -4
- package/src/hooks/use-environments.ts +24 -5
- package/src/hooks/use-file-attachments.ts +70 -32
- package/src/hooks/use-goal.ts +17 -5
- package/src/hooks/use-machines.ts +47 -1
- package/src/hooks/use-sandbox-files.ts +310 -59
- package/src/hooks/use-sandbox-git.ts +148 -41
- package/src/hooks/use-session-capabilities.ts +35 -24
- package/src/hooks/use-session-events.ts +18 -4
- package/src/hooks/use-transcription.ts +4 -14
- package/src/hooks/use-turn-queue.ts +17 -5
- package/src/hooks/use-voice-input.ts +481 -36
- package/src/hooks/use-windowed-sections.ts +9 -7
- package/src/index.ts +11 -6
- package/src/lib/composer-responsive-context.ts +15 -0
- package/src/lib/use-portal-token-style.ts +60 -15
- package/src/provider.tsx +52 -7
- package/src/realtime/dropdown-menu.tsx +15 -19
- package/src/realtime/realtime-control.tsx +18 -7
- package/src/timeline/activity-rail.tsx +15 -6
- package/src/timeline/index.ts +1 -1
- package/src/timeline/parsers.ts +33 -20
- package/src/timeline/projection.ts +23 -0
- package/src/timeline/registry.ts +7 -0
- package/src/timeline/screenshot-lightbox.tsx +15 -4
- package/src/timeline/shared.tsx +8 -8
- package/src/timeline/tool-renderers.tsx +166 -22
- package/src/timeline/turn-summary.tsx +6 -6
- package/src/timeline/types.ts +4 -0
- package/src/voice-recording-store.ts +15 -0
- package/styles/compiled.css +5065 -0
- package/styles/compiled.d.ts +2 -0
- package/styles/effective-tokens.css +27 -0
- package/styles/index.css +3 -0
- package/styles/responsive.css +196 -0
- package/styles/responsive.d.ts +2 -0
- package/styles/tokens.css +50 -17
- package/dist/chunk-6EDV6YFE.js.map +0 -1
- package/dist/chunk-EB67J347.js.map +0 -1
- package/dist/chunk-EHSYZ4KP.js.map +0 -1
- package/dist/chunk-FD72GJMK.js.map +0 -1
- package/dist/chunk-JB5725BI.js +0 -48
- package/dist/chunk-JB5725BI.js.map +0 -1
- package/dist/chunk-L3EWO3UK.js.map +0 -1
- package/dist/chunk-LWR4MXSS.js.map +0 -1
- package/dist/chunk-SRFUT2ZU.js.map +0 -1
- package/dist/chunk-U3CSCMWA.js.map +0 -1
- package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
- /package/dist/{chunk-HFO4ERGQ.js.map → chunk-FWXGSNXT.js.map} +0 -0
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
WorkspaceCaptureRepo,
|
|
6
6
|
} from "@opengeni/sdk";
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
-
import {
|
|
8
|
+
import { type ClientOverride, useOpenGeni } from "../provider";
|
|
9
9
|
|
|
10
10
|
export type UseSandboxGitOptions = ClientOverride & {
|
|
11
11
|
/** Live event log (usually `useSessionEvents().events`) — drives auto-refresh
|
|
@@ -18,8 +18,14 @@ export type UseSandboxGitOptions = ClientOverride & {
|
|
|
18
18
|
repoPaths?: readonly string[] | undefined;
|
|
19
19
|
/** Diff the staged index vs HEAD (`--cached`) instead of the working tree. */
|
|
20
20
|
staged?: boolean | undefined;
|
|
21
|
+
/** Review scope. `branch` compares the current branch with the remote default
|
|
22
|
+
* branch, `working` shows uncommitted edits, and `staged` shows the index.
|
|
23
|
+
* `staged` above remains as a compatibility alias. */
|
|
24
|
+
comparison?: SandboxGitComparison | undefined;
|
|
21
25
|
/** Hold off the initial fetch. Default true. */
|
|
22
26
|
enabled?: boolean | undefined;
|
|
27
|
+
/** Pause live reads/invalidation while preserving the last rendered result. */
|
|
28
|
+
active?: boolean | undefined;
|
|
23
29
|
/** Lease liveness ("warm" | "draining" | "cold"). When NOT warm, the diff is
|
|
24
30
|
* served from the capture (cold/offline) instead of a live `gitDiff` RPC. */
|
|
25
31
|
liveness?: string | undefined;
|
|
@@ -29,10 +35,14 @@ export type UseSandboxGitOptions = ClientOverride & {
|
|
|
29
35
|
capture?: WorkspaceCaptureManifest | null | undefined;
|
|
30
36
|
};
|
|
31
37
|
|
|
38
|
+
export type SandboxGitComparison = "branch" | "working" | "staged";
|
|
39
|
+
|
|
32
40
|
/** A Git diff qualified into workspace scope. `repoRoot` is present for the
|
|
33
41
|
* workspace-wide path and lets review UI group real repositories without
|
|
34
42
|
* guessing from the first path segment. */
|
|
35
|
-
export type SandboxGitFileDiff = GitFileDiff & {
|
|
43
|
+
export type SandboxGitFileDiff = GitFileDiff & {
|
|
44
|
+
repoRoot?: string | undefined;
|
|
45
|
+
};
|
|
36
46
|
|
|
37
47
|
export type UseSandboxGitResult = {
|
|
38
48
|
/** Working-tree (or staged) diff vs HEAD — the structured hunks the Pierre
|
|
@@ -175,6 +185,7 @@ export function useSandboxGit(
|
|
|
175
185
|
): UseSandboxGitResult {
|
|
176
186
|
const { client, workspaceId } = useOpenGeni(options);
|
|
177
187
|
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
188
|
+
const active = options.active ?? true;
|
|
178
189
|
const repoPath = options.repoPath ?? "";
|
|
179
190
|
const workspaceWide = options.repoPaths !== undefined;
|
|
180
191
|
// Serialize the caller's array so inline values do not retrigger effects. An
|
|
@@ -183,10 +194,15 @@ export function useSandboxGit(
|
|
|
183
194
|
options.repoPaths && options.repoPaths.length > 0 ? options.repoPaths : [repoPath],
|
|
184
195
|
).join("\u0000");
|
|
185
196
|
const repoPaths = useMemo(() => repoPathsKey.split("\u0000"), [repoPathsKey]);
|
|
186
|
-
const
|
|
187
|
-
|
|
197
|
+
const comparison: SandboxGitComparison =
|
|
198
|
+
options.comparison ?? (options.staged ? "staged" : "working");
|
|
199
|
+
const hasAdvertisedRepoPaths = (options.repoPaths?.length ?? 0) > 0;
|
|
200
|
+
// New captures retain both working-tree and branch comparisons. Staged state
|
|
201
|
+
// remains live-only; legacy captures simply lack branchDiff and fall back via
|
|
202
|
+
// the workspace controller without inventing an empty branch comparison.
|
|
203
|
+
const capture = comparison === "staged" ? null : (options.capture ?? null);
|
|
188
204
|
const isLive = options.liveness === "warm" || options.liveness === "draining";
|
|
189
|
-
const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${repoPathsKey}\u0000${workspaceWide}\u0000${
|
|
205
|
+
const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${repoPathsKey}\u0000${workspaceWide}\u0000${comparison}`;
|
|
190
206
|
|
|
191
207
|
const [diff, setDiff] = useState<SandboxGitFileDiff[]>([]);
|
|
192
208
|
const [branch, setBranch] = useState<string | null>(null);
|
|
@@ -204,6 +220,7 @@ export function useSandboxGit(
|
|
|
204
220
|
const refreshGenerationRef = useRef(0);
|
|
205
221
|
const refreshAbortRef = useRef<AbortController | null>(null);
|
|
206
222
|
const lastChangeRef = useRef(0);
|
|
223
|
+
const commandRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
207
224
|
|
|
208
225
|
const refresh = useCallback(async () => {
|
|
209
226
|
if (!sessionId) return;
|
|
@@ -214,17 +231,69 @@ export function useSandboxGit(
|
|
|
214
231
|
setLoading(true);
|
|
215
232
|
setError(null);
|
|
216
233
|
try {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
const diffRequest = (root: string) =>
|
|
235
|
+
client.gitDiff(
|
|
236
|
+
workspaceId,
|
|
237
|
+
sessionId,
|
|
238
|
+
{
|
|
239
|
+
path: root,
|
|
240
|
+
...(comparison === "branch"
|
|
241
|
+
? { fromRef: "origin/HEAD", includeUntracked: true }
|
|
242
|
+
: comparison === "working"
|
|
243
|
+
? { fromRef: "HEAD", includeUntracked: true }
|
|
244
|
+
: { staged: true, includeUntracked: false }),
|
|
245
|
+
},
|
|
246
|
+
{ signal: refreshAbort.signal },
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Capability-advertised roots are known repositories. Fetch status metadata
|
|
250
|
+
// and diff hunks together: both are independent Channel-A reads and each can
|
|
251
|
+
// take several seconds on a remote sandbox. The legacy root probe remains
|
|
252
|
+
// serial so a non-repository workspace never receives a failing git diff.
|
|
253
|
+
const prefetched = hasAdvertisedRepoPaths
|
|
254
|
+
? await client
|
|
255
|
+
.gitReadBatch(
|
|
256
|
+
workspaceId,
|
|
257
|
+
sessionId,
|
|
258
|
+
{
|
|
259
|
+
requests: repoPaths.map((root) => ({
|
|
260
|
+
status: { path: root },
|
|
261
|
+
diff: {
|
|
262
|
+
path: root,
|
|
263
|
+
...(comparison === "branch"
|
|
264
|
+
? { fromRef: "origin/HEAD", includeUntracked: true }
|
|
265
|
+
: comparison === "working"
|
|
266
|
+
? { fromRef: "HEAD", includeUntracked: true }
|
|
267
|
+
: { staged: true, includeUntracked: false }),
|
|
268
|
+
},
|
|
269
|
+
})),
|
|
270
|
+
},
|
|
271
|
+
{ signal: refreshAbort.signal },
|
|
272
|
+
)
|
|
273
|
+
.then((batch) =>
|
|
274
|
+
batch.results.map((item, index) => ({
|
|
275
|
+
root: repoPaths[index] ?? "",
|
|
276
|
+
status: item.status,
|
|
277
|
+
result: item.diff ?? {
|
|
278
|
+
files: [],
|
|
279
|
+
revision: item.status.revision,
|
|
280
|
+
},
|
|
281
|
+
})),
|
|
282
|
+
)
|
|
283
|
+
: null;
|
|
284
|
+
const statuses =
|
|
285
|
+
prefetched ??
|
|
286
|
+
(await Promise.all(
|
|
287
|
+
repoPaths.map(async (root) => ({
|
|
288
|
+
root,
|
|
289
|
+
status: await client.gitStatus(
|
|
290
|
+
workspaceId,
|
|
291
|
+
sessionId,
|
|
292
|
+
{ path: root },
|
|
293
|
+
{ signal: refreshAbort.signal },
|
|
294
|
+
),
|
|
295
|
+
})),
|
|
296
|
+
));
|
|
228
297
|
if (refreshGenerationRef.current !== generation) return;
|
|
229
298
|
const repositories = statuses.filter(({ status }) => status.isRepo);
|
|
230
299
|
const roots = repositories.map(({ root }) => root);
|
|
@@ -239,19 +308,11 @@ export function useSandboxGit(
|
|
|
239
308
|
setSource("live");
|
|
240
309
|
return;
|
|
241
310
|
}
|
|
311
|
+
const prefetchedByRoot = new Map(prefetched?.map(({ root, result }) => [root, result]));
|
|
242
312
|
const results = await Promise.all(
|
|
243
313
|
repositories.map(async ({ root }) => ({
|
|
244
314
|
root,
|
|
245
|
-
result: await
|
|
246
|
-
workspaceId,
|
|
247
|
-
sessionId,
|
|
248
|
-
{
|
|
249
|
-
path: root,
|
|
250
|
-
staged,
|
|
251
|
-
includeUntracked: !staged,
|
|
252
|
-
},
|
|
253
|
-
{ signal: refreshAbort.signal },
|
|
254
|
-
),
|
|
315
|
+
result: prefetchedByRoot.get(root) ?? (await diffRequest(root)),
|
|
255
316
|
})),
|
|
256
317
|
);
|
|
257
318
|
if (refreshGenerationRef.current !== generation) return;
|
|
@@ -273,15 +334,25 @@ export function useSandboxGit(
|
|
|
273
334
|
if (refreshGenerationRef.current === generation) setLoading(false);
|
|
274
335
|
if (refreshAbortRef.current === refreshAbort) refreshAbortRef.current = null;
|
|
275
336
|
}
|
|
276
|
-
}, [
|
|
337
|
+
}, [
|
|
338
|
+
client,
|
|
339
|
+
workspaceId,
|
|
340
|
+
sessionId,
|
|
341
|
+
repoPaths,
|
|
342
|
+
hasAdvertisedRepoPaths,
|
|
343
|
+
workspaceWide,
|
|
344
|
+
comparison,
|
|
345
|
+
]);
|
|
277
346
|
|
|
278
|
-
// Serve the
|
|
279
|
-
// meaning here: the capture records the combined turn-end change surface vs HEAD.
|
|
347
|
+
// Serve the requested review scope from a capture when that scope exists.
|
|
280
348
|
const seedFromCapture = useCallback(
|
|
281
349
|
(manifest: WorkspaceCaptureManifest) => {
|
|
282
350
|
const repositories = workspaceWide
|
|
283
351
|
? manifest.repos
|
|
284
352
|
: ([repoForPath(manifest, repoPath)].filter(Boolean) as WorkspaceCaptureRepo[]);
|
|
353
|
+
if (comparison === "branch" && repositories.some((repo) => repo.branchDiff === undefined)) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
285
356
|
if (repositories.length === 0) {
|
|
286
357
|
setIsRepo(false);
|
|
287
358
|
setBranch(null);
|
|
@@ -301,16 +372,19 @@ export function useSandboxGit(
|
|
|
301
372
|
setDiff((prev) =>
|
|
302
373
|
mergeDiffs(
|
|
303
374
|
prev,
|
|
304
|
-
repositories.flatMap((repo) =>
|
|
305
|
-
|
|
306
|
-
|
|
375
|
+
repositories.flatMap((repo) => {
|
|
376
|
+
const capturedDiff = comparison === "branch" ? (repo.branchDiff ?? []) : repo.diff;
|
|
377
|
+
return workspaceWide
|
|
378
|
+
? capturedDiff.map((file) => qualifyDiff(repo.root, file))
|
|
379
|
+
: capturedDiff;
|
|
380
|
+
}),
|
|
307
381
|
),
|
|
308
382
|
);
|
|
309
383
|
sourceRef.current = "capture";
|
|
310
384
|
setSource("capture");
|
|
311
385
|
setError(null);
|
|
312
386
|
},
|
|
313
|
-
[repoPath, workspaceWide],
|
|
387
|
+
[comparison, repoPath, workspaceWide],
|
|
314
388
|
);
|
|
315
389
|
|
|
316
390
|
// Source selection on mount / liveness / capture-revision change. Key on the
|
|
@@ -349,6 +423,10 @@ export function useSandboxGit(
|
|
|
349
423
|
if (currentCapture && (identityChanged || !isLive || sourceRef.current !== "live")) {
|
|
350
424
|
seedFromCapture(currentCapture);
|
|
351
425
|
}
|
|
426
|
+
if (!active) {
|
|
427
|
+
if (!active) setLoading(false);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
352
430
|
if (isLive || !currentCapture) {
|
|
353
431
|
void refresh();
|
|
354
432
|
}
|
|
@@ -357,25 +435,54 @@ export function useSandboxGit(
|
|
|
357
435
|
refreshAbortRef.current = null;
|
|
358
436
|
refreshGenerationRef.current += 1;
|
|
359
437
|
};
|
|
360
|
-
}, [enabled, isLive, captureRevision, identityKey, refresh, seedFromCapture]);
|
|
438
|
+
}, [enabled, active, isLive, captureRevision, identityKey, refresh, seedFromCapture]);
|
|
361
439
|
|
|
362
|
-
// git.changed → re-fetch the LIVE diff.
|
|
363
|
-
//
|
|
364
|
-
//
|
|
440
|
+
// git.changed → re-fetch the LIVE diff. Agent tool completion is also an
|
|
441
|
+
// invalidation point because shell/apply-patch writes can bypass Channel-A and
|
|
442
|
+
// therefore emit no git.changed event. This is event-driven, never polling.
|
|
365
443
|
const events = options.events;
|
|
366
444
|
useEffect(() => {
|
|
367
|
-
if (!enabled || !events)
|
|
445
|
+
if (!enabled || !active || !events) {
|
|
446
|
+
if (commandRefreshTimerRef.current) {
|
|
447
|
+
clearTimeout(commandRefreshTimerRef.current);
|
|
448
|
+
commandRefreshTimerRef.current = null;
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
368
452
|
let latest = lastChangeRef.current;
|
|
453
|
+
let immediate = false;
|
|
454
|
+
let commandDelta = false;
|
|
369
455
|
for (const event of events) {
|
|
370
|
-
if (event.
|
|
456
|
+
if (event.sequence <= latest) continue;
|
|
457
|
+
if (event.type === "git.changed" || event.type === "agent.toolCall.output") {
|
|
458
|
+
latest = event.sequence;
|
|
459
|
+
immediate = true;
|
|
460
|
+
} else if (event.type === "sandbox.command.output.delta") {
|
|
371
461
|
latest = event.sequence;
|
|
462
|
+
commandDelta = true;
|
|
372
463
|
}
|
|
373
464
|
}
|
|
374
465
|
if (latest > lastChangeRef.current) {
|
|
375
466
|
lastChangeRef.current = latest;
|
|
376
|
-
|
|
467
|
+
if (immediate) {
|
|
468
|
+
if (commandRefreshTimerRef.current) clearTimeout(commandRefreshTimerRef.current);
|
|
469
|
+
commandRefreshTimerRef.current = null;
|
|
470
|
+
void refresh();
|
|
471
|
+
} else if (commandDelta && !commandRefreshTimerRef.current) {
|
|
472
|
+
commandRefreshTimerRef.current = setTimeout(() => {
|
|
473
|
+
commandRefreshTimerRef.current = null;
|
|
474
|
+
void refresh();
|
|
475
|
+
}, 1_000);
|
|
476
|
+
}
|
|
377
477
|
}
|
|
378
|
-
}, [enabled, events, refresh]);
|
|
478
|
+
}, [enabled, active, events, refresh]);
|
|
479
|
+
|
|
480
|
+
useEffect(
|
|
481
|
+
() => () => {
|
|
482
|
+
if (commandRefreshTimerRef.current) clearTimeout(commandRefreshTimerRef.current);
|
|
483
|
+
},
|
|
484
|
+
[],
|
|
485
|
+
);
|
|
379
486
|
|
|
380
487
|
const identityMatches = enabled && stateIdentity === identityKey;
|
|
381
488
|
const visibleRepoRoots = identityMatches ? repoRoots : [];
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@opengeni/sdk";
|
|
8
8
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
9
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
10
|
+
import { usePageLiveActivity } from "./internal";
|
|
10
11
|
|
|
11
12
|
// "on-demand" is the boxless-benign resting state: the lease is cold and NOTHING
|
|
12
13
|
// asked to warm a box (no viewer/consent action, no files-tab warm). Under lazy
|
|
@@ -157,6 +158,8 @@ export function useSessionCapabilities(
|
|
|
157
158
|
): UseSessionCapabilitiesResult {
|
|
158
159
|
const { client, workspaceId } = useOpenGeni(options);
|
|
159
160
|
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
161
|
+
const pageLive = usePageLiveActivity();
|
|
162
|
+
const lifecycleEnabled = enabled && pageLive;
|
|
160
163
|
const attachDesktop = options.attachDesktop ?? false;
|
|
161
164
|
const attachTerminal = options.attachTerminal ?? false;
|
|
162
165
|
const attachFiles = options.attachFiles ?? false;
|
|
@@ -201,21 +204,21 @@ export function useSessionCapabilities(
|
|
|
201
204
|
epochRef.current = 0;
|
|
202
205
|
viewerIdRef.current = null;
|
|
203
206
|
}
|
|
204
|
-
if (!
|
|
207
|
+
if (!lifecycleEnabled || !sessionId) {
|
|
205
208
|
setState("idle");
|
|
206
209
|
setCapabilities(null);
|
|
207
210
|
return;
|
|
208
211
|
}
|
|
209
212
|
let cancelled = false;
|
|
210
213
|
let pollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
211
|
-
let heartbeatTimer: ReturnType<typeof
|
|
214
|
+
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
212
215
|
let localViewerId: string | null = null;
|
|
213
216
|
const requestAbort = new AbortController();
|
|
214
217
|
const startedAt = Date.now();
|
|
215
218
|
|
|
216
219
|
const clearTimers = () => {
|
|
217
220
|
if (pollTimer !== null) clearTimeout(pollTimer);
|
|
218
|
-
if (heartbeatTimer !== null)
|
|
221
|
+
if (heartbeatTimer !== null) clearTimeout(heartbeatTimer);
|
|
219
222
|
pollTimer = null;
|
|
220
223
|
heartbeatTimer = null;
|
|
221
224
|
};
|
|
@@ -231,27 +234,35 @@ export function useSessionCapabilities(
|
|
|
231
234
|
if (!localViewerId || heartbeatTimer !== null) return;
|
|
232
235
|
const intervalMs =
|
|
233
236
|
caps.viewerHeartbeatIntervalMs > 0 ? caps.viewerHeartbeatIntervalMs : 30_000;
|
|
234
|
-
|
|
237
|
+
const schedule = () => {
|
|
235
238
|
if (cancelled || !localViewerId) return;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
239
|
+
heartbeatTimer = setTimeout(() => {
|
|
240
|
+
heartbeatTimer = null;
|
|
241
|
+
if (cancelled || !localViewerId) return;
|
|
242
|
+
void client
|
|
243
|
+
.heartbeatViewer(workspaceId, sessionId, localViewerId, {
|
|
244
|
+
leaseEpoch: epochRef.current,
|
|
245
|
+
})
|
|
246
|
+
.then((res) => {
|
|
247
|
+
// alive:false ⇒ the holder was reaped or the epoch moved under us;
|
|
248
|
+
// re-negotiate to re-acquire against the new owner.
|
|
249
|
+
if (!res.alive && !cancelled) {
|
|
250
|
+
renegotiate();
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
.catch((cause) => {
|
|
254
|
+
if (cancelled) return;
|
|
255
|
+
if (
|
|
256
|
+
cause instanceof OpenGeniApiError &&
|
|
257
|
+
(cause.status === 409 || cause.status === 410)
|
|
258
|
+
) {
|
|
259
|
+
renegotiate();
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
.finally(schedule);
|
|
263
|
+
}, intervalMs);
|
|
264
|
+
};
|
|
265
|
+
schedule();
|
|
255
266
|
};
|
|
256
267
|
|
|
257
268
|
const pollUntilWarm = () => {
|
|
@@ -455,7 +466,7 @@ export function useSessionCapabilities(
|
|
|
455
466
|
client,
|
|
456
467
|
workspaceId,
|
|
457
468
|
sessionId,
|
|
458
|
-
|
|
469
|
+
lifecycleEnabled,
|
|
459
470
|
attachDesktop,
|
|
460
471
|
attachTerminal,
|
|
461
472
|
attachFiles,
|
|
@@ -4,6 +4,7 @@ import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../sessi
|
|
|
4
4
|
import { buildTimeline, groupTimeline, sessionStatusFromEvents } from "../timeline/projection";
|
|
5
5
|
import type { TimelineItem } from "../timeline/types";
|
|
6
6
|
import type { EmbeddedSessionClientLike } from "../client";
|
|
7
|
+
import { usePageLiveActivity } from "./internal";
|
|
7
8
|
|
|
8
9
|
export type SessionEventsConnectionState = StreamConnectionState | "idle" | "ended" | "error";
|
|
9
10
|
|
|
@@ -135,6 +136,8 @@ export function useSessionEvents(
|
|
|
135
136
|
): UseSessionEventsResult {
|
|
136
137
|
const { client, workspaceId, reconcileSession } = useEmbeddedSession(options);
|
|
137
138
|
const enabled = options.enabled ?? true;
|
|
139
|
+
const pageLive = usePageLiveActivity();
|
|
140
|
+
const streamEnabled = enabled && pageLive;
|
|
138
141
|
const after = options.after ?? 0;
|
|
139
142
|
const replay = options.replay ?? "windowed";
|
|
140
143
|
const fullReplay = replay === "full" || after !== 0;
|
|
@@ -224,7 +227,7 @@ export function useSessionEvents(
|
|
|
224
227
|
loadingOlderRef.current = false;
|
|
225
228
|
setLoadingOlder(false);
|
|
226
229
|
}
|
|
227
|
-
if (!sessionId || !
|
|
230
|
+
if (!sessionId || !streamEnabled) {
|
|
228
231
|
setConnectionState("idle");
|
|
229
232
|
return;
|
|
230
233
|
}
|
|
@@ -338,7 +341,18 @@ export function useSessionEvents(
|
|
|
338
341
|
const stream = client.streamEvents(workspaceId, sessionId, {
|
|
339
342
|
after: streamResumeSequenceRef.current,
|
|
340
343
|
signal: controller.signal,
|
|
341
|
-
|
|
344
|
+
onOpen: () => {
|
|
345
|
+
// Reconciliation repairs projections, but it must not block reading
|
|
346
|
+
// the already-open SSE body. Otherwise live events accumulate in the
|
|
347
|
+
// browser and appear as one delayed burst after every reconciler ends.
|
|
348
|
+
void Promise.resolve()
|
|
349
|
+
.then(() => reconcileSession(sessionId))
|
|
350
|
+
.catch((cause) => {
|
|
351
|
+
if (isCurrent()) {
|
|
352
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
},
|
|
342
356
|
onStateChange: (state) => {
|
|
343
357
|
if (isCurrent()) {
|
|
344
358
|
setConnectionState(state);
|
|
@@ -399,7 +413,7 @@ export function useSessionEvents(
|
|
|
399
413
|
workspaceId,
|
|
400
414
|
sessionId,
|
|
401
415
|
after,
|
|
402
|
-
|
|
416
|
+
streamEnabled,
|
|
403
417
|
fullReplay,
|
|
404
418
|
streamKey,
|
|
405
419
|
streamEpoch,
|
|
@@ -1175,7 +1189,7 @@ async function loadPreviousPage(
|
|
|
1175
1189
|
compact: true,
|
|
1176
1190
|
// Monitoring summaries omit correlation fields once a bounded durable
|
|
1177
1191
|
// payload exceeds their preview threshold. The browser timeline needs the
|
|
1178
|
-
//
|
|
1192
|
+
// exact stored payload so persisted tool outputs still match their
|
|
1179
1193
|
// calls and expose truthful truncation telemetry after a reload.
|
|
1180
1194
|
payloadMode: "full",
|
|
1181
1195
|
});
|
|
@@ -214,7 +214,7 @@ export type UseTranscriptionOptions = {
|
|
|
214
214
|
disabled?: boolean | undefined;
|
|
215
215
|
createLocalSessionId?: (() => string) | undefined;
|
|
216
216
|
lifecycleTimeouts?: Partial<TranscriptionLifecycleTimeouts> | undefined;
|
|
217
|
-
/** Receives
|
|
217
|
+
/** Receives the exact diagnostic emitted by the transcription transport. */
|
|
218
218
|
onDiagnostic?: ((diagnostic: TranscriptionDiagnostic) => void) | undefined;
|
|
219
219
|
};
|
|
220
220
|
|
|
@@ -277,7 +277,7 @@ export function useTranscription({
|
|
|
277
277
|
(diagnostic: unknown) => {
|
|
278
278
|
if (!onDiagnostic) return;
|
|
279
279
|
try {
|
|
280
|
-
onDiagnostic(
|
|
280
|
+
onDiagnostic(normalizeTranscriptionDiagnostic(diagnostic));
|
|
281
281
|
} catch {
|
|
282
282
|
// Observability must never break the local privacy or UI lifecycle.
|
|
283
283
|
}
|
|
@@ -625,7 +625,7 @@ export function normalizeTranscriptionErrorCode(code: unknown): TranscriptionErr
|
|
|
625
625
|
}
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
-
export function
|
|
628
|
+
export function normalizeTranscriptionDiagnostic(diagnostic: unknown): TranscriptionDiagnostic {
|
|
629
629
|
const source = isRecord(diagnostic) ? diagnostic : {};
|
|
630
630
|
const operation =
|
|
631
631
|
source.operation === "start" ||
|
|
@@ -635,20 +635,10 @@ export function sanitizeTranscriptionDiagnostic(diagnostic: unknown): Transcript
|
|
|
635
635
|
? source.operation
|
|
636
636
|
: "session";
|
|
637
637
|
const rawDetail = typeof source.detail === "string" ? source.detail : "No diagnostic detail.";
|
|
638
|
-
const detail = rawDetail
|
|
639
|
-
.replace(/[\u0000-\u001f\u007f]+/gu, " ")
|
|
640
|
-
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/giu, "Bearer [REDACTED]")
|
|
641
|
-
.replace(/\bsk-[A-Za-z0-9_-]{4,}\b/gu, "[REDACTED]")
|
|
642
|
-
.replace(
|
|
643
|
-
/\b(api[-_ ]?key|authorization|access[-_ ]?token|refresh[-_ ]?token|token|secret)\b\s*[:=]\s*[^\s,;]+/giu,
|
|
644
|
-
"$1=[REDACTED]",
|
|
645
|
-
)
|
|
646
|
-
.trim()
|
|
647
|
-
.slice(0, 512);
|
|
648
638
|
return {
|
|
649
639
|
operation,
|
|
650
640
|
code: normalizeTranscriptionErrorCode(source.code),
|
|
651
|
-
detail:
|
|
641
|
+
detail: rawDetail,
|
|
652
642
|
};
|
|
653
643
|
}
|
|
654
644
|
|
|
@@ -12,6 +12,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
|
|
|
12
12
|
import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
|
|
13
13
|
import {
|
|
14
14
|
useDebouncedCallback,
|
|
15
|
+
usePageLiveActivity,
|
|
15
16
|
useSessionEventTrigger,
|
|
16
17
|
type SessionEventFeedOptions,
|
|
17
18
|
} from "./internal";
|
|
@@ -91,6 +92,7 @@ export function useTurnQueue(
|
|
|
91
92
|
useEmbeddedSession(options);
|
|
92
93
|
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
93
94
|
const targetKey = `${workspaceId}\u0000${sessionId ?? ""}`;
|
|
95
|
+
const pageLive = usePageLiveActivity();
|
|
94
96
|
const [snapshot, setSnapshot] = useState<SessionQueueSnapshot | null>(null);
|
|
95
97
|
const [stateTargetKey, setStateTargetKey] = useState(targetKey);
|
|
96
98
|
const [loading, setLoading] = useState(enabled);
|
|
@@ -174,24 +176,34 @@ export function useTurnQueue(
|
|
|
174
176
|
);
|
|
175
177
|
|
|
176
178
|
useEffect(() => {
|
|
177
|
-
if (!enabled) {
|
|
179
|
+
if (!enabled || !pageLive) {
|
|
178
180
|
setLoading(false);
|
|
179
181
|
return;
|
|
180
182
|
}
|
|
181
183
|
setLoading(true);
|
|
182
|
-
void load();
|
|
183
184
|
const pollIntervalMs = options.pollIntervalMs;
|
|
184
185
|
if (pollIntervalMs === undefined || pollIntervalMs <= 0) {
|
|
186
|
+
void load();
|
|
185
187
|
return () => {
|
|
186
188
|
readGeneration.current += 1;
|
|
187
189
|
};
|
|
188
190
|
}
|
|
189
|
-
|
|
191
|
+
let cancelled = false;
|
|
192
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
193
|
+
const schedule = () => {
|
|
194
|
+
if (cancelled) return;
|
|
195
|
+
timer = setTimeout(() => {
|
|
196
|
+
timer = null;
|
|
197
|
+
void load().finally(schedule);
|
|
198
|
+
}, pollIntervalMs);
|
|
199
|
+
};
|
|
200
|
+
void load().finally(schedule);
|
|
190
201
|
return () => {
|
|
191
|
-
|
|
202
|
+
cancelled = true;
|
|
203
|
+
if (timer !== null) clearTimeout(timer);
|
|
192
204
|
readGeneration.current += 1;
|
|
193
205
|
};
|
|
194
|
-
}, [load, enabled, options.pollIntervalMs]);
|
|
206
|
+
}, [load, enabled, pageLive, options.pollIntervalMs]);
|
|
195
207
|
|
|
196
208
|
useEffect(() => {
|
|
197
209
|
if (enabled && workspaceControlEvent) void load();
|