@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.
Files changed (141) hide show
  1. package/README.md +68 -12
  2. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  3. package/dist/chunk-FK6VG4LL.js.map +1 -0
  4. package/dist/{chunk-HFO4ERGQ.js → chunk-FWXGSNXT.js} +2 -2
  5. package/dist/{chunk-SRFUT2ZU.js → chunk-I4MPFDRP.js} +119 -63
  6. package/dist/chunk-I4MPFDRP.js.map +1 -0
  7. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  8. package/dist/chunk-KZ73RL76.js.map +1 -0
  9. package/dist/{chunk-LWR4MXSS.js → chunk-LHLYWG5R.js} +10 -1
  10. package/dist/chunk-LHLYWG5R.js.map +1 -0
  11. package/dist/{chunk-L3EWO3UK.js → chunk-NJ7M2B6Z.js} +445 -84
  12. package/dist/chunk-NJ7M2B6Z.js.map +1 -0
  13. package/dist/{chunk-VCBFUSUZ.js → chunk-O4VXOBGF.js} +3 -3
  14. package/dist/{chunk-VCBFUSUZ.js.map → chunk-O4VXOBGF.js.map} +1 -1
  15. package/dist/{chunk-6EDV6YFE.js → chunk-OL6QVXGF.js} +121 -29
  16. package/dist/chunk-OL6QVXGF.js.map +1 -0
  17. package/dist/chunk-VZTQ73XT.js +86 -0
  18. package/dist/chunk-VZTQ73XT.js.map +1 -0
  19. package/dist/{chunk-EHSYZ4KP.js → chunk-WYA65Y3F.js} +16 -16
  20. package/dist/chunk-WYA65Y3F.js.map +1 -0
  21. package/dist/{chunk-FD72GJMK.js → chunk-XCNTXWBB.js} +274 -125
  22. package/dist/chunk-XCNTXWBB.js.map +1 -0
  23. package/dist/client.d.ts +1 -1
  24. package/dist/components/chat-composer.d.ts +7 -2
  25. package/dist/components/composer-transcription-control.d.ts +2 -0
  26. package/dist/components/composer.d.ts +14 -0
  27. package/dist/components/machines/machine-detail.d.ts +3 -1
  28. package/dist/components/machines-dashboard.d.ts +3 -3
  29. package/dist/components/message-timeline.d.ts +12 -2
  30. package/dist/components/sandbox-files.d.ts +2 -2
  31. package/dist/components/sandbox-terminal.d.ts +29 -0
  32. package/dist/components/sandbox-workspace.d.ts +5 -1
  33. package/dist/components/workspace-dock.d.ts +3 -1
  34. package/dist/composer.d.ts +2 -2
  35. package/dist/composer.js +6 -6
  36. package/dist/hooks/internal.d.ts +6 -0
  37. package/dist/hooks/use-codex-accounts.d.ts +1 -1
  38. package/dist/hooks/use-environments.d.ts +10 -4
  39. package/dist/hooks/use-machines.d.ts +7 -0
  40. package/dist/hooks/use-sandbox-files.d.ts +19 -0
  41. package/dist/hooks/use-sandbox-git.d.ts +7 -0
  42. package/dist/hooks/use-transcription.d.ts +2 -2
  43. package/dist/hooks/use-voice-input.d.ts +13 -4
  44. package/dist/index.d.ts +6 -5
  45. package/dist/index.js +904 -538
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/composer-responsive-context.d.ts +9 -0
  48. package/dist/lib/use-portal-token-style.d.ts +14 -3
  49. package/dist/machines.js +2 -2
  50. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-U5JFG7HT.js} +19 -18
  51. package/dist/queue-surface-implementation-U5JFG7HT.js.map +1 -0
  52. package/dist/realtime/realtime-control.d.ts +9 -0
  53. package/dist/realtime.js +29 -32
  54. package/dist/realtime.js.map +1 -1
  55. package/dist/session-ui.js +5 -5
  56. package/dist/session.js +4 -4
  57. package/dist/timeline/activity-rail.d.ts +3 -2
  58. package/dist/timeline/index.d.ts +2 -2
  59. package/dist/timeline/parsers.d.ts +3 -3
  60. package/dist/timeline/registry.d.ts +3 -0
  61. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  62. package/dist/timeline/types.d.ts +4 -0
  63. package/dist/voice-recording-store.d.ts +8 -2
  64. package/package.json +20 -4
  65. package/src/client.ts +3 -0
  66. package/src/components/chat-composer.tsx +12 -2
  67. package/src/components/code-editor.tsx +1 -1
  68. package/src/components/command-palette.tsx +1 -1
  69. package/src/components/composer-transcription-control.tsx +41 -16
  70. package/src/components/composer.tsx +52 -26
  71. package/src/components/copy-button.tsx +2 -2
  72. package/src/components/desktop-viewer.tsx +2 -2
  73. package/src/components/file-browser.tsx +39 -17
  74. package/src/components/human-input-form.tsx +2 -2
  75. package/src/components/machine-card.tsx +1 -1
  76. package/src/components/machines/machine-detail.tsx +39 -15
  77. package/src/components/machines-dashboard.tsx +7 -6
  78. package/src/components/message-timeline.tsx +44 -11
  79. package/src/components/model-picker.tsx +2 -2
  80. package/src/components/model-policy-picker.tsx +16 -15
  81. package/src/components/queue-surface-implementation.tsx +20 -16
  82. package/src/components/queue-surface-state.tsx +2 -2
  83. package/src/components/sandbox-files.tsx +140 -60
  84. package/src/components/sandbox-terminal.tsx +24 -5
  85. package/src/components/sandbox-workspace.tsx +180 -142
  86. package/src/components/session-chrome.tsx +14 -14
  87. package/src/components/tooltip.tsx +13 -16
  88. package/src/components/user-message-body.tsx +1 -1
  89. package/src/components/workbench-changes.tsx +4 -4
  90. package/src/components/workspace-dock.tsx +13 -5
  91. package/src/composer.ts +3 -0
  92. package/src/hooks/internal.ts +132 -38
  93. package/src/hooks/use-codex-accounts.ts +4 -4
  94. package/src/hooks/use-environments.ts +24 -5
  95. package/src/hooks/use-file-attachments.ts +70 -32
  96. package/src/hooks/use-goal.ts +17 -5
  97. package/src/hooks/use-machines.ts +47 -1
  98. package/src/hooks/use-sandbox-files.ts +310 -59
  99. package/src/hooks/use-sandbox-git.ts +148 -41
  100. package/src/hooks/use-session-capabilities.ts +35 -24
  101. package/src/hooks/use-session-events.ts +18 -4
  102. package/src/hooks/use-transcription.ts +4 -14
  103. package/src/hooks/use-turn-queue.ts +17 -5
  104. package/src/hooks/use-voice-input.ts +481 -36
  105. package/src/hooks/use-windowed-sections.ts +9 -7
  106. package/src/index.ts +11 -6
  107. package/src/lib/composer-responsive-context.ts +15 -0
  108. package/src/lib/use-portal-token-style.ts +60 -15
  109. package/src/provider.tsx +52 -7
  110. package/src/realtime/dropdown-menu.tsx +15 -19
  111. package/src/realtime/realtime-control.tsx +18 -7
  112. package/src/timeline/activity-rail.tsx +15 -6
  113. package/src/timeline/index.ts +1 -1
  114. package/src/timeline/parsers.ts +33 -20
  115. package/src/timeline/projection.ts +23 -0
  116. package/src/timeline/registry.ts +7 -0
  117. package/src/timeline/screenshot-lightbox.tsx +15 -4
  118. package/src/timeline/shared.tsx +8 -8
  119. package/src/timeline/tool-renderers.tsx +166 -22
  120. package/src/timeline/turn-summary.tsx +6 -6
  121. package/src/timeline/types.ts +4 -0
  122. package/src/voice-recording-store.ts +15 -0
  123. package/styles/compiled.css +5065 -0
  124. package/styles/compiled.d.ts +2 -0
  125. package/styles/effective-tokens.css +27 -0
  126. package/styles/index.css +3 -0
  127. package/styles/responsive.css +196 -0
  128. package/styles/responsive.d.ts +2 -0
  129. package/styles/tokens.css +50 -17
  130. package/dist/chunk-6EDV6YFE.js.map +0 -1
  131. package/dist/chunk-EB67J347.js.map +0 -1
  132. package/dist/chunk-EHSYZ4KP.js.map +0 -1
  133. package/dist/chunk-FD72GJMK.js.map +0 -1
  134. package/dist/chunk-JB5725BI.js +0 -48
  135. package/dist/chunk-JB5725BI.js.map +0 -1
  136. package/dist/chunk-L3EWO3UK.js.map +0 -1
  137. package/dist/chunk-LWR4MXSS.js.map +0 -1
  138. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  139. package/dist/chunk-U3CSCMWA.js.map +0 -1
  140. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
  141. /package/dist/{chunk-HFO4ERGQ.js.map → chunk-FWXGSNXT.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { FileAsset, FileResourceRef } from "@opengeni/sdk";
2
- import { useCallback, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import {
4
4
  useEmbeddedFileAttachments,
5
5
  type EmbeddedFileAttachmentClientOverride,
@@ -87,12 +87,64 @@ export function useFileAttachments(
87
87
  // Keep the source File per attachment id so a failed upload can be retried
88
88
  // in place. Cleared on remove/clear so it never outlives its attachment.
89
89
  const sources = useRef<Map<string, File>>(new Map());
90
+ // Object URLs must be owned synchronously: React may discard an attachment
91
+ // state update when its component unmounts in the same batch that minted the
92
+ // preview. The registry remains available to cleanup even before a render.
93
+ const previewUrls = useRef<Map<string, string>>(new Map());
94
+ const revokePreview = useCallback((id: string) => {
95
+ const previewUrl = previewUrls.current.get(id);
96
+ if (!previewUrl) return;
97
+ previewUrls.current.delete(id);
98
+ URL.revokeObjectURL(previewUrl);
99
+ }, []);
100
+ const revokeAllPreviews = useCallback(() => {
101
+ const urls = [...previewUrls.current.values()];
102
+ previewUrls.current.clear();
103
+ for (const previewUrl of urls) URL.revokeObjectURL(previewUrl);
104
+ }, []);
105
+ // Ready-file reconciliation uses functional state updaters, which React may
106
+ // evaluate during a concurrent render that later suspends or is abandoned.
107
+ // Diff only committed attachment sets here so URL revocation cannot run from
108
+ // render-phase code while the previously committed DOM still uses a preview.
109
+ const committedAttachmentIds = useRef<Set<string>>(new Set());
110
+ useEffect(() => {
111
+ const currentIds = new Set(attachments.map((attachment) => attachment.id));
112
+ for (const id of committedAttachmentIds.current) {
113
+ if (!currentIds.has(id)) revokePreview(id);
114
+ }
115
+ committedAttachmentIds.current = currentIds;
116
+ }, [attachments, revokePreview]);
117
+ // Upload promises are not cancellable at this layer. Fence their settlements
118
+ // when the hook changes client/workspace or unmounts so an old tenant/session
119
+ // cannot mutate the next attachment queue (or an already-unmounted component).
120
+ const scopeGeneration = useRef(0);
121
+ const previousScope = useRef({ client, workspaceId });
122
+
123
+ useEffect(() => {
124
+ const previous = previousScope.current;
125
+ if (previous.client === client && previous.workspaceId === workspaceId) return;
126
+ previousScope.current = { client, workspaceId };
127
+ scopeGeneration.current += 1;
128
+ sources.current.clear();
129
+ revokeAllPreviews();
130
+ setAttachments([]);
131
+ }, [client, revokeAllPreviews, workspaceId]);
132
+
133
+ useEffect(
134
+ () => () => {
135
+ scopeGeneration.current += 1;
136
+ sources.current.clear();
137
+ revokeAllPreviews();
138
+ },
139
+ [revokeAllPreviews],
140
+ );
90
141
 
91
142
  // Run (or re-run) the upload for one already-tracked attachment id. Sets it
92
143
  // back to `uploading`, then resolves to `ready` (with the asset) or `failed`
93
144
  // (with the error message).
94
145
  const startUpload = useCallback(
95
146
  (id: string, file: File) => {
147
+ const generation = scopeGeneration.current;
96
148
  void client
97
149
  .uploadFile(workspaceId, {
98
150
  filename: file.name || "file",
@@ -100,6 +152,7 @@ export function useFileAttachments(
100
152
  data: file,
101
153
  })
102
154
  .then((asset) => {
155
+ if (scopeGeneration.current !== generation) return;
103
156
  // Retry bytes are useful only until durable finalization succeeds.
104
157
  // Drop the source File immediately; restored/ready attachments must
105
158
  // never retain browser-local byte authority.
@@ -121,6 +174,7 @@ export function useFileAttachments(
121
174
  );
122
175
  })
123
176
  .catch((error: unknown) => {
177
+ if (scopeGeneration.current !== generation) return;
124
178
  setAttachments((current) =>
125
179
  current.map((attachment) =>
126
180
  attachment.id === id
@@ -143,6 +197,7 @@ export function useFileAttachments(
143
197
  const id = crypto.randomUUID();
144
198
  sources.current.set(id, file);
145
199
  const previewUrl = isImage(file) ? URL.createObjectURL(file) : undefined;
200
+ if (previewUrl) previewUrls.current.set(id, previewUrl);
146
201
  setAttachments((current) => [
147
202
  ...current,
148
203
  {
@@ -232,11 +287,6 @@ export function useFileAttachments(
232
287
  // only authority restored across page/device boundaries.
233
288
  };
234
289
  });
235
- for (const [fileId, attachment] of existingReady) {
236
- if (!incoming.has(fileId) && attachment.previewUrl) {
237
- URL.revokeObjectURL(attachment.previewUrl);
238
- }
239
- }
240
290
  // A server restoration is authoritative for finalized assets, but an
241
291
  // upload that has not finalized still belongs to the local actor. Keep
242
292
  // those unresolved entries while replacing the ready set exactly.
@@ -246,46 +296,34 @@ export function useFileAttachments(
246
296
  [workspaceId],
247
297
  );
248
298
 
249
- const remove = useCallback((id: string) => {
250
- sources.current.delete(id);
251
- setAttachments((current) => {
252
- const removed = current.find((attachment) => attachment.id === id);
253
- if (removed?.previewUrl) {
254
- URL.revokeObjectURL(removed.previewUrl);
255
- }
256
- return current.filter((attachment) => attachment.id !== id);
257
- });
258
- }, []);
299
+ const remove = useCallback(
300
+ (id: string) => {
301
+ sources.current.delete(id);
302
+ revokePreview(id);
303
+ setAttachments((current) => current.filter((attachment) => attachment.id !== id));
304
+ },
305
+ [revokePreview],
306
+ );
259
307
 
260
308
  const removeReadyFiles = useCallback((fileIds: Iterable<string>) => {
261
309
  const accepted = new Set(fileIds);
262
310
  if (accepted.size === 0) return;
263
311
  setAttachments((current) =>
264
312
  current.filter((attachment) => {
265
- const removeAccepted =
313
+ return !(
266
314
  attachment.status === "ready" &&
267
315
  attachment.file !== undefined &&
268
- accepted.has(attachment.file.id);
269
- if (removeAccepted) {
270
- sources.current.delete(attachment.id);
271
- if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
272
- }
273
- return !removeAccepted;
316
+ accepted.has(attachment.file.id)
317
+ );
274
318
  }),
275
319
  );
276
320
  }, []);
277
321
 
278
322
  const clear = useCallback(() => {
279
323
  sources.current.clear();
280
- setAttachments((current) => {
281
- for (const attachment of current) {
282
- if (attachment.previewUrl) {
283
- URL.revokeObjectURL(attachment.previewUrl);
284
- }
285
- }
286
- return [];
287
- });
288
- }, []);
324
+ revokeAllPreviews();
325
+ setAttachments([]);
326
+ }, [revokeAllPreviews]);
289
327
 
290
328
  return {
291
329
  attachments,
@@ -4,6 +4,7 @@ import { useEmbeddedGoal, type EmbeddedGoalClientOverride } from "../session-con
4
4
  import {
5
5
  useDebouncedCallback,
6
6
  useMutationRunner,
7
+ usePageLiveActivity,
7
8
  useSessionEventTrigger,
8
9
  type SessionEventFeedOptions,
9
10
  } from "./internal";
@@ -75,6 +76,7 @@ export function useGoal(
75
76
  const enabled = (options.enabled ?? true) && Boolean(sessionId);
76
77
  const sharedEvents = options.events;
77
78
  const sharedFeed = sharedEvents !== undefined;
79
+ const pageLive = usePageLiveActivity();
78
80
  const [goal, setGoal] = useState<SessionGoal | null>(null);
79
81
  const [loading, setLoading] = useState(enabled);
80
82
  const [error, setError] = useState<Error | null>(null);
@@ -116,7 +118,7 @@ export function useGoal(
116
118
  setGoal(null);
117
119
  setError(null);
118
120
  }
119
- if (!enabled) {
121
+ if (!enabled || !pageLive) {
120
122
  setLoading(false);
121
123
  return;
122
124
  }
@@ -127,19 +129,29 @@ export function useGoal(
127
129
  };
128
130
  }
129
131
  setLoading(true);
130
- void load();
131
132
  const pollIntervalMs = options.pollIntervalMs;
132
133
  if (pollIntervalMs === undefined || pollIntervalMs <= 0) {
134
+ void load();
133
135
  return () => {
134
136
  generation.current += 1;
135
137
  };
136
138
  }
137
- const timer = setInterval(() => void load(), pollIntervalMs);
139
+ let cancelled = false;
140
+ let timer: ReturnType<typeof setTimeout> | null = null;
141
+ const schedule = () => {
142
+ if (cancelled) return;
143
+ timer = setTimeout(() => {
144
+ timer = null;
145
+ void load().finally(schedule);
146
+ }, pollIntervalMs);
147
+ };
148
+ void load().finally(schedule);
138
149
  return () => {
139
- clearInterval(timer);
150
+ cancelled = true;
151
+ if (timer !== null) clearTimeout(timer);
140
152
  generation.current += 1;
141
153
  };
142
- }, [load, enabled, workspaceId, sessionId, options.pollIntervalMs, sharedFeed]);
154
+ }, [load, enabled, pageLive, workspaceId, sessionId, options.pollIntervalMs, sharedFeed]);
143
155
 
144
156
  const scheduleRefresh = useDebouncedCallback(() => void load());
145
157
  useSessionEventTrigger(client, workspaceId, sessionId, isGoalRefreshEvent, scheduleRefresh, {
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useRef, useState } from "react";
2
+ import type { RemoveEnrollmentRequest, RemoveEnrollmentResponse } from "@opengeni/sdk";
2
3
  import { useOpenGeni, type ClientOverride } from "../provider";
3
4
  import { useMutationRunner, usePolledValue } from "./internal";
4
5
  import type { MachinesResponse, MachineView, MetricSample } from "../types/machines";
@@ -31,6 +32,12 @@ export type MachinesClientLike = {
31
32
  enrollmentId: string,
32
33
  options?: { window?: "15m" | "1h" | "6h" | "24h" },
33
34
  ) => Promise<MetricSample[]>;
35
+ /** POST .../enrollments/:enrollmentId/revoke — workspace-admin removal. */
36
+ removeEnrollment?: (
37
+ workspaceId: string,
38
+ enrollmentId: string,
39
+ request?: RemoveEnrollmentRequest,
40
+ ) => Promise<RemoveEnrollmentResponse>;
34
41
  /**
35
42
  * POST .../sessions/:sessionId/active-sandbox — swap the session's active
36
43
  * sandbox to a machine. The default swap path; the real SDK client provides it.
@@ -69,6 +76,13 @@ export type UseMachinesResult = {
69
76
  refresh: () => Promise<void>;
70
77
  /** Attach/swap the session's active sandbox to a machine (returns the new pointer). */
71
78
  attach: (sandboxId: string) => Promise<boolean>;
79
+ /** Remove a self-hosted enrollment while retaining history. */
80
+ remove: (
81
+ enrollmentId: string,
82
+ request?: RemoveEnrollmentRequest,
83
+ ) => Promise<RemoveEnrollmentResponse | null>;
84
+ canRemove: boolean;
85
+ removingEnrollmentId: string | null;
72
86
  /** Whether the host wired an attach/swap path (drives the card affordance). */
73
87
  canAttach: boolean;
74
88
  /** Fetch a downsampled metric series for one enrolled machine. */
@@ -128,6 +142,12 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
128
142
  sandboxId: string | null;
129
143
  }>(() => ({ identity: identityKey, sandboxId: null }));
130
144
  const attachingSandboxId = attachState.identity === identityKey ? attachState.sandboxId : null;
145
+ const [removeState, setRemoveState] = useState<{
146
+ identity: string;
147
+ enrollmentId: string | null;
148
+ }>(() => ({ identity: identityKey, enrollmentId: null }));
149
+ const removingEnrollmentId =
150
+ removeState.identity === identityKey ? removeState.enrollmentId : null;
131
151
 
132
152
  const data = loadedData ?? EMPTY;
133
153
  // The swap is session-scoped: a host adapter (`attachMachine`) wins; otherwise
@@ -137,6 +157,7 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
137
157
  sessionId !== undefined &&
138
158
  (typeof machinesClient.attachMachine === "function" ||
139
159
  typeof machinesClient.swapActiveSandbox === "function");
160
+ const canRemove = typeof machinesClient.removeEnrollment === "function";
140
161
 
141
162
  const attach = useCallback(
142
163
  async (sandboxId: string): Promise<boolean> => {
@@ -173,6 +194,28 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
173
194
  [machinesClient, workspaceId],
174
195
  );
175
196
 
197
+ const remove = useCallback(
198
+ async (
199
+ enrollmentId: string,
200
+ request: RemoveEnrollmentRequest = {},
201
+ ): Promise<RemoveEnrollmentResponse | null> => {
202
+ if (!machinesClient.removeEnrollment) return null;
203
+ const ownedIdentity = identityKey;
204
+ setRemoveState({ identity: ownedIdentity, enrollmentId });
205
+ const result = await run(() =>
206
+ machinesClient.removeEnrollment!(workspaceId, enrollmentId, request),
207
+ );
208
+ if (identityRef.current === ownedIdentity) {
209
+ setRemoveState({ identity: ownedIdentity, enrollmentId: null });
210
+ if (result?.outcome === "removed" || result?.outcome === "already_removed") {
211
+ await refresh();
212
+ }
213
+ }
214
+ return result;
215
+ },
216
+ [machinesClient, workspaceId, identityKey, run, refresh],
217
+ );
218
+
176
219
  return {
177
220
  machines: data.machines,
178
221
  activeSandboxId: data.activeSandboxId,
@@ -183,8 +226,11 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
183
226
  attach,
184
227
  canAttach,
185
228
  fetchSeries,
186
- attaching: mutating,
229
+ attaching: mutating && attachingSandboxId !== null,
187
230
  attachingSandboxId,
231
+ remove,
232
+ canRemove,
233
+ removingEnrollmentId,
188
234
  mutationError,
189
235
  clearMutationError,
190
236
  };