@opengeni/react 0.15.0 → 0.20.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 (101) hide show
  1. package/README.md +110 -10
  2. package/dist/{chunk-TOJR776I.js → chunk-6BXBGJ3B.js} +45 -369
  3. package/dist/chunk-6BXBGJ3B.js.map +1 -0
  4. package/dist/chunk-6UT5OC3P.js +2133 -0
  5. package/dist/chunk-6UT5OC3P.js.map +1 -0
  6. package/dist/chunk-EBTT3MYN.js +92 -0
  7. package/dist/chunk-EBTT3MYN.js.map +1 -0
  8. package/dist/chunk-HHFA4AFX.js +425 -0
  9. package/dist/chunk-HHFA4AFX.js.map +1 -0
  10. package/dist/chunk-LAZ2A743.js +2393 -0
  11. package/dist/chunk-LAZ2A743.js.map +1 -0
  12. package/dist/chunk-MRSECXRP.js +563 -0
  13. package/dist/chunk-MRSECXRP.js.map +1 -0
  14. package/dist/chunk-TK7G6XLT.js +18 -0
  15. package/dist/chunk-TK7G6XLT.js.map +1 -0
  16. package/dist/composer-DvUaa5ki.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +1288 -1785
  21. package/dist/index.js +7015 -8806
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +3 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
  30. package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
  31. package/dist/session-Gd4iVPYw.d.ts +425 -0
  32. package/dist/session-context-DIFFlXKc.d.ts +52 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +50 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-BCfm4mzX.d.ts +112 -0
  37. package/package.json +20 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +40 -0
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +94 -993
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +1 -1
  51. package/src/components/machines-dashboard.tsx +3 -3
  52. package/src/components/message-timeline.tsx +15 -7
  53. package/src/components/model-picker.tsx +12 -3
  54. package/src/components/pierre-diff.tsx +32 -6
  55. package/src/components/queue-surface-implementation.tsx +1041 -0
  56. package/src/components/queue-surface-state.tsx +94 -0
  57. package/src/components/queue-surface.tsx +41 -559
  58. package/src/components/sandbox-files.tsx +138 -14
  59. package/src/components/sandbox-terminal.tsx +8 -2
  60. package/src/components/sandbox-workspace.tsx +339 -119
  61. package/src/components/workbench-changes.tsx +84 -33
  62. package/src/components/workspace-dock.tsx +188 -18
  63. package/src/composer.ts +60 -0
  64. package/src/hooks/internal.ts +115 -34
  65. package/src/hooks/use-composer.ts +335 -104
  66. package/src/hooks/use-human-input.ts +131 -0
  67. package/src/hooks/use-machine-chip.ts +2 -2
  68. package/src/hooks/use-machines.ts +27 -11
  69. package/src/hooks/use-sandbox-files.ts +310 -62
  70. package/src/hooks/use-sandbox-git.ts +154 -40
  71. package/src/hooks/use-sandbox-terminal.ts +28 -6
  72. package/src/hooks/use-session-capabilities.ts +38 -9
  73. package/src/hooks/use-session-control.ts +37 -8
  74. package/src/hooks/use-session-events.ts +522 -48
  75. package/src/hooks/use-slash-commands.ts +38 -38
  76. package/src/hooks/use-transcription.ts +757 -0
  77. package/src/hooks/use-turn-queue.ts +134 -66
  78. package/src/hooks/use-windowed-sections.ts +2 -2
  79. package/src/hooks/use-workspace-capture.ts +146 -40
  80. package/src/hooks/use-workspace-edit.ts +50 -14
  81. package/src/hooks/use-workspace-sessions.ts +3 -0
  82. package/src/human-input.ts +72 -0
  83. package/src/index.ts +76 -6
  84. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  85. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  86. package/src/lib/use-portal-token-style.ts +51 -0
  87. package/src/lib/use-unicode-fonts.ts +57 -0
  88. package/src/provider.tsx +4 -69
  89. package/src/session-context.ts +135 -0
  90. package/src/session.ts +79 -0
  91. package/src/timeline/parsers.ts +35 -0
  92. package/src/timeline/projection.ts +12 -4
  93. package/src/timeline/tool-renderers.tsx +41 -0
  94. package/src/timeline/turn-summary.tsx +2 -2
  95. package/src/timeline/types.ts +16 -8
  96. package/styles/index.css +56 -0
  97. package/styles/index.d.ts +2 -0
  98. package/styles/tokens.css +11 -6
  99. package/styles/tokens.d.ts +2 -0
  100. package/dist/chunk-TOJR776I.js.map +0 -1
  101. package/dist/machines-BpdwuQcD.d.ts +0 -449
package/README.md CHANGED
@@ -4,16 +4,17 @@ React hooks and styled components for OpenGeni, built on
4
4
  [`@opengeni/sdk`](../sdk): live session streaming, a chat composer, a message
5
5
  timeline that renders streaming deltas / tool calls / spawned-worker status,
6
6
  session status badges, and fleet tiles for workspace overviews. Two opt-in
7
- surfaces layer on top: a **sandbox-surfacing** workbench (files, terminal, diff,
7
+ surfaces layer on top: a **sandbox workspace** workbench (files, terminal, diff,
8
8
  and an optional desktop stream) and, at the
9
9
  [`@opengeni/react/machines`](#connected-machines-opengenireactmachines) subpath,
10
10
  the **Connected Machines** dashboard + enrollment flow.
11
11
 
12
12
  The default root import (`@opengeni/react`) is the clean sandbox-agnostic
13
- surface — the chat/timeline hooks and components plus the sandbox-surfacing
14
- suite. Connected-Machine UI lives under the `@opengeni/react/machines` subpath so
15
- consumers that never surface machines don't pull it in. (The root barrel still
16
- re-exports the machines island for back-compat, deprecated per #144.)
13
+ surface — the chat/timeline hooks and components plus the sandbox workspace
14
+ suite. Advanced chat-composer composition lives under
15
+ `@opengeni/react/composer`; Connected-Machine UI lives under
16
+ `@opengeni/react/machines`. (The root barrel still re-exports the machines
17
+ island for back-compat, deprecated per #144.)
17
18
 
18
19
  Design-system-first: every visual decision routes through CSS-variable tokens
19
20
  (`styles/tokens.css`) — color, typography, radius, shadow, motion. Dark mode is
@@ -24,6 +25,27 @@ animation. Override the tokens to rebrand everything.
24
25
 
25
26
  ## Install & styles
26
27
 
28
+ Hosts that render their own UI should import the session-only surface:
29
+
30
+ ```tsx
31
+ import {
32
+ useComposer,
33
+ useSessionControl,
34
+ useSessionEvents,
35
+ useTurnQueue,
36
+ } from "@opengeni/react/session";
37
+ ```
38
+
39
+ That subpath contains session hooks, approval helpers, and the pure timeline
40
+ projection only. It does not load the styled composer, timeline, workbench,
41
+ CSS, or their optional editor/terminal peers. Pass `{ client, workspaceId }` to
42
+ each hook when the host intentionally does not mount `OpenGeniProvider`. The
43
+ exported `SessionClientLike` is deliberately narrow: a tenant-safe proxy needs
44
+ only session events, composer draft/send, queue, pause/resume, and approval
45
+ operations. Workspace-level Resume is an optional authority.
46
+
47
+ The styled root surface uses Tailwind v4 and the package CSS entries:
48
+
27
49
  The package ships TypeScript source plus two CSS entries. In your Tailwind v4
28
50
  entry CSS:
29
51
 
@@ -84,6 +106,82 @@ export function App() {
84
106
  }
85
107
  ```
86
108
 
109
+ ## Composer customization (`@opengeni/react/composer`)
110
+
111
+ Use `ChatComposer` for the standard layout and its `controlsStart`, `header`,
112
+ and `messages` props for small additions. For a different layout, import the
113
+ headless controller and compound primitives as a namespace. The controller is
114
+ the one behavior path for keyboard routing, guarded queue/steer submission,
115
+ attachments, commands, pause/resume, focus, confirmations, and feedback.
116
+
117
+ ```tsx
118
+ import * as Composer from "@opengeni/react/composer";
119
+
120
+ function InsertTranscript() {
121
+ const composer = Composer.useChatComposer();
122
+ return (
123
+ <button
124
+ type="button"
125
+ onClick={() => {
126
+ const separator = composer.value.trim().length > 0 ? " " : "";
127
+ composer.setValue(`${composer.value}${separator}Transcribed text`);
128
+ composer.focusInput();
129
+ }}
130
+ >
131
+ Insert transcript
132
+ </button>
133
+ );
134
+ }
135
+
136
+ function CustomComposer({ sessionComposer, attachments, effectiveControl }) {
137
+ const controller = Composer.useChatComposerController({
138
+ delivery: sessionComposer,
139
+ draft: sessionComposer,
140
+ control: sessionComposer,
141
+ attachments,
142
+ effectiveControl,
143
+ });
144
+
145
+ return (
146
+ <Composer.Root controller={controller}>
147
+ <Composer.Frame>
148
+ <Composer.CommandPalette />
149
+ <Composer.Surface>
150
+ <Composer.PausedState />
151
+ <Composer.RestoredResources />
152
+ <Composer.Attachments />
153
+ <Composer.Input />
154
+ {controller.confirmState ? (
155
+ <Composer.Confirmation />
156
+ ) : (
157
+ <Composer.Footer>
158
+ <Composer.Controls>
159
+ <Composer.AttachButton />
160
+ <InsertTranscript />
161
+ </Composer.Controls>
162
+ <Composer.Actions>
163
+ <Composer.PauseButton />
164
+ <Composer.SendButton />
165
+ </Composer.Actions>
166
+ </Composer.Footer>
167
+ )}
168
+ </Composer.Surface>
169
+ </Composer.Frame>
170
+ <Composer.Help />
171
+ <Composer.Status />
172
+ </Composer.Root>
173
+ );
174
+ }
175
+ ```
176
+
177
+ For a pre-session or otherwise limited composer, pass only `delivery`; `draft`
178
+ and `control` are optional capabilities rather than no-op requirements. Custom
179
+ controls should call `controller.submit("queue" | "steer")` (or read it through
180
+ `useChatComposer`) instead of calling a delivery adapter directly, so upload,
181
+ disabled, in-flight, and slash-command guards stay intact. Accessory-local UI
182
+ state remains application-owned; durable draft and session state remain in
183
+ `useComposer`.
184
+
87
185
  ## Hooks
88
186
 
89
187
  - `useSessionEvents(sessionId)` — loads a compact, bounded tail window by
@@ -108,7 +206,8 @@ export function App() {
108
206
  `steerTurn`, and `removeTurn`. Mutations carry observed revisions and conflicts
109
207
  refetch server truth. Live-updates on `turn.*` and `session.queue.*` events — pass the
110
208
  `events` log from `useSessionEvents` to reuse its stream, or let it tail the
111
- session itself.
209
+ session itself. Providerless self-streams reconcile authoritative queue/draft
210
+ state after the SSE connection opens, closing the initial-read handoff race.
112
211
  - `useGoal(sessionId, { events })` — goal state + autonomy counters
113
212
  (`autoContinuations`, `noProgressStreak`) with `pause(rationale?)` /
114
213
  `resume()`. Goal-less sessions yield `goal: null`. Live-updates on `goal.*`
@@ -165,11 +264,12 @@ intentional changes should regenerate those snapshots and review the diff.
165
264
 
166
265
  ## Components
167
266
 
168
- - `ChatComposer` — auto-growing textarea, Enter-to-send (IME-safe), stop
169
- control while a turn runs, inline error recovery. Slots for app chrome:
267
+ - `ChatComposer` — auto-growing textarea, Enter-to-send (IME-safe), pause/resume
268
+ controls, inline error recovery. Slots for app chrome:
170
269
  `controlsStart` (footer controls like model pickers / attach buttons),
171
270
  `header` (e.g. attachment chips above the field), and `onPaste`
172
- (paste-image-to-attach).
271
+ (paste-image-to-attach). Its advanced controller and compound primitives are
272
+ exported from `@opengeni/react/composer`.
173
273
  - `MessageTimeline` — the session timeline with stick-to-bottom scrolling, a
174
274
  "jump to latest" affordance, streaming caret, collapsible activity clusters,
175
275
  and worker cards (wire `onOpenSession` to drill into a worker). Pass
@@ -253,7 +353,7 @@ end-to-end embedder story (create-on-machine, discover, swap, enroll, revoke).
253
353
 
254
354
  ## Optional peer dependencies
255
355
 
256
- The chat/timeline surface has none. The sandbox-surfacing and diff surfaces pull
356
+ The chat/timeline surface has none. The sandbox workspace and diff surfaces pull
257
357
  their heavy libraries from **optional** `peerDependencies`, so you install only
258
358
  what the surfaces you mount need:
259
359
 
@@ -1,3 +1,15 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-TK7G6XLT.js";
4
+ import {
5
+ OpenGeniContext,
6
+ formatBytes,
7
+ formatRelativeTime,
8
+ useMutationRunner,
9
+ useOpenGeni,
10
+ usePolledValue
11
+ } from "./chunk-HHFA4AFX.js";
12
+
1
13
  // src/types/machines.ts
2
14
  function connectionStatusForState(state) {
3
15
  switch (state) {
@@ -17,20 +29,6 @@ function connectionStatusForState(state) {
17
29
  }
18
30
  }
19
31
 
20
- // src/lib/cn.ts
21
- import { clsx } from "clsx";
22
- import { extendTailwindMerge } from "tailwind-merge";
23
- var twMerge = extendTailwindMerge({
24
- extend: {
25
- classGroups: {
26
- "font-size": [{ text: ["og-xs", "og-sm", "og-base", "og-md"] }]
27
- }
28
- }
29
- });
30
- function cn(...inputs) {
31
- return twMerge(clsx(inputs));
32
- }
33
-
34
32
  // src/components/machine-status-pill.tsx
35
33
  import { jsx, jsxs } from "react/jsx-runtime";
36
34
  var CONNECTION_STATUS_META = {
@@ -153,116 +151,6 @@ function MachineStatusPill({
153
151
  );
154
152
  }
155
153
 
156
- // src/lib/format.ts
157
- function formatRelativeTime(iso, now = /* @__PURE__ */ new Date()) {
158
- const then = new Date(iso).getTime();
159
- if (Number.isNaN(then)) {
160
- return "";
161
- }
162
- const seconds = Math.max(0, Math.floor((now.getTime() - then) / 1e3));
163
- if (seconds < 10) {
164
- return "now";
165
- }
166
- if (seconds < 60) {
167
- return `${seconds}s`;
168
- }
169
- const minutes = Math.floor(seconds / 60);
170
- if (minutes < 60) {
171
- return `${minutes}m`;
172
- }
173
- const hours = Math.floor(minutes / 60);
174
- if (hours < 24) {
175
- return `${hours}h`;
176
- }
177
- const days = Math.floor(hours / 24);
178
- if (days < 14) {
179
- return `${days}d`;
180
- }
181
- return new Date(iso).toLocaleDateString();
182
- }
183
- function formatBytes(bytes) {
184
- if (bytes < 1024) {
185
- return `${bytes} B`;
186
- }
187
- const units = ["KB", "MB", "GB"];
188
- let value = bytes / 1024;
189
- for (const unit of units) {
190
- if (value < 1024 || unit === "GB") {
191
- return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`;
192
- }
193
- value /= 1024;
194
- }
195
- return `${bytes} B`;
196
- }
197
- function truncate(text, maxLength) {
198
- const collapsed = text.replace(/\s+/g, " ").trim();
199
- if (collapsed.length <= maxLength) {
200
- return collapsed;
201
- }
202
- return `${collapsed.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
203
- }
204
- function stringifyPayload(value) {
205
- if (value === null || value === void 0) {
206
- return "";
207
- }
208
- if (typeof value === "string") {
209
- const parsed = tryParseJson(value);
210
- if (parsed !== void 0 && typeof parsed === "object") {
211
- return stringifyPayload(parsed);
212
- }
213
- return value;
214
- }
215
- try {
216
- return JSON.stringify(value, null, 2) ?? String(value);
217
- } catch {
218
- return String(value);
219
- }
220
- }
221
- function tryParseJson(text) {
222
- const trimmed = text.trim();
223
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[") && !trimmed.startsWith('"')) {
224
- return void 0;
225
- }
226
- try {
227
- return JSON.parse(trimmed);
228
- } catch {
229
- return void 0;
230
- }
231
- }
232
- var CREDIT_EXHAUSTION_MESSAGE = "Out of OpenGeni credits \u2014 this workspace's balance is empty. Add credits to continue; the conversation is preserved.";
233
- function isCreditExhaustion(input) {
234
- if (typeof input === "string") {
235
- return input.toLowerCase().includes("insufficient opengeni credits");
236
- }
237
- if (input.segmentLimit === "budget_exhausted") {
238
- return true;
239
- }
240
- for (const text of [input.error, input.detail]) {
241
- if (typeof text === "string" && text.toLowerCase().includes("insufficient opengeni credits")) {
242
- return true;
243
- }
244
- }
245
- return false;
246
- }
247
- function humanizeFailureReason(reason) {
248
- if (!reason) {
249
- return reason;
250
- }
251
- if (isCreditExhaustion(reason)) {
252
- return CREDIT_EXHAUSTION_MESSAGE;
253
- }
254
- const normalized = reason.toLowerCase();
255
- const authFailure = normalized.includes("incorrect api key") || normalized.includes("invalid api key") || normalized.includes("invalid_api_key") || normalized.includes("platform.openai.com/account/api-keys") || normalized.includes("401") && (normalized.includes("api key") || normalized.includes("unauthorized"));
256
- if (authFailure) {
257
- return "The model provider rejected this deployment's engine credentials. Sending messages won't help until the deployment's engine configuration is fixed.";
258
- }
259
- const quotaFailure = normalized.includes("insufficient_quota") || normalized.includes("exceeded your current quota");
260
- if (quotaFailure) {
261
- return "The model provider refused the request: this deployment's provider quota is exhausted.";
262
- }
263
- return reason;
264
- }
265
-
266
154
  // src/components/machine-metrics.tsx
267
155
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
268
156
  function pct(value) {
@@ -741,7 +629,7 @@ function MachineCard({
741
629
  onAttach?.(machine);
742
630
  },
743
631
  className: cn(
744
- "rounded-og-sm border border-og-border px-2.5 py-1 text-og-xs font-medium text-og-fg-muted transition-colors pointer-coarse:min-h-10",
632
+ "rounded-og-sm border border-og-border px-2.5 py-1 text-og-xs font-medium text-og-fg-muted transition-colors pointer-coarse:min-h-11",
745
633
  "hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50"
746
634
  ),
747
635
  children: attaching ? "Switching\u2026" : "Attach"
@@ -1260,7 +1148,7 @@ function EmptyState({ onEnroll }) {
1260
1148
  type: "button",
1261
1149
  "data-enroll-cta": true,
1262
1150
  onClick: onEnroll,
1263
- className: "inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-10",
1151
+ className: "inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-11",
1264
1152
  children: [
1265
1153
  /* @__PURE__ */ jsx8(PlusIcon, { className: "size-3.5", "aria-hidden": true }),
1266
1154
  "Enroll a machine"
@@ -1300,7 +1188,7 @@ function Header({
1300
1188
  type: "button",
1301
1189
  "data-enroll-cta": true,
1302
1190
  onClick: onEnroll,
1303
- className: "inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-10",
1191
+ className: "inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-11",
1304
1192
  children: [
1305
1193
  /* @__PURE__ */ jsx8(PlusIcon, { className: "size-3.5", "aria-hidden": true }),
1306
1194
  "Enroll machine"
@@ -1344,7 +1232,7 @@ function MachinesDashboard({
1344
1232
  "data-machines-retry": true,
1345
1233
  onClick: onRefresh,
1346
1234
  disabled: loading,
1347
- className: "inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-10",
1235
+ className: "inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-11",
1348
1236
  children: [
1349
1237
  /* @__PURE__ */ jsx8(RefreshCwIcon, { className: cn("size-3.5", loading && "animate-og-spin"), "aria-hidden": true }),
1350
1238
  loading ? "Retrying\u2026" : "Retry"
@@ -1751,7 +1639,7 @@ function EnrollmentConsent({
1751
1639
  "data-deny": true,
1752
1640
  disabled: busy,
1753
1641
  onClick: () => onDeny?.(),
1754
- className: "flex-1 rounded-og-sm border border-og-border px-3 py-2 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg disabled:opacity-50 pointer-coarse:min-h-10",
1642
+ className: "flex-1 rounded-og-sm border border-og-border px-3 py-2 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg disabled:opacity-50 pointer-coarse:min-h-11",
1755
1643
  children: "Cancel"
1756
1644
  }
1757
1645
  ),
@@ -1762,7 +1650,7 @@ function EnrollmentConsent({
1762
1650
  "data-approve": true,
1763
1651
  disabled: busy,
1764
1652
  onClick: () => onApprove?.(allowScreenControl),
1765
- className: "flex-1 rounded-og-sm bg-og-status-failed px-3 py-2 text-og-sm font-semibold text-og-accent-fg transition-colors hover:opacity-90 disabled:opacity-50 pointer-coarse:min-h-10",
1653
+ className: "flex-1 rounded-og-sm bg-og-status-failed px-3 py-2 text-og-sm font-semibold text-og-accent-fg transition-colors hover:opacity-90 disabled:opacity-50 pointer-coarse:min-h-11",
1766
1654
  children: busy ? "Connecting\u2026" : "Grant full access"
1767
1655
  }
1768
1656
  )
@@ -1807,26 +1695,15 @@ function ConsentResult({
1807
1695
  }
1808
1696
 
1809
1697
  // src/hooks/use-machines.ts
1810
- import { useCallback as useCallback3, useState as useState5 } from "react";
1698
+ import { useCallback as useCallback2, useRef as useRef3, useState as useState4 } from "react";
1811
1699
 
1812
1700
  // src/provider.tsx
1813
1701
  import {
1814
1702
  OpenGeniApiContractMismatchError,
1815
1703
  OPENGENI_API_CONTRACT_REVISION
1816
1704
  } from "@opengeni/sdk";
1817
- import {
1818
- createContext,
1819
- useCallback,
1820
- useContext,
1821
- useEffect,
1822
- useMemo,
1823
- useRef as useRef2,
1824
- useState as useState3
1825
- } from "react";
1705
+ import { useCallback, useEffect, useMemo, useRef as useRef2, useState as useState3 } from "react";
1826
1706
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1827
- var OpenGeniContext = createContext(null);
1828
- var NOOP_REGISTER_RECONCILER = () => () => void 0;
1829
- var NOOP_RECONCILE_SESSION = async () => void 0;
1830
1707
  var CONTRACT_RELOAD_STORAGE_PREFIX = "opengeni.reloadForApiContract:";
1831
1708
  function OpenGeniProvider({
1832
1709
  client,
@@ -1972,205 +1849,6 @@ function reloadForContractMismatchOnce(mismatch) {
1972
1849
  sessionStorage.setItem(key, OPENGENI_API_CONTRACT_REVISION);
1973
1850
  window.setTimeout(() => window.location.reload(), 150);
1974
1851
  }
1975
- function useOpenGeni(override = {}) {
1976
- const context = useContext(OpenGeniContext);
1977
- const client = override.client ?? context?.client;
1978
- const workspaceId = override.workspaceId ?? context?.workspaceId;
1979
- if (!client || !workspaceId) {
1980
- throw new Error(
1981
- "@opengeni/react: no OpenGeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook."
1982
- );
1983
- }
1984
- return {
1985
- client,
1986
- workspaceId,
1987
- workspaceControlEvent: context?.workspaceControlEvent ?? null,
1988
- workspaceControlConnectionState: context?.workspaceControlConnectionState ?? "idle",
1989
- registerSessionReconciler: context?.registerSessionReconciler ?? NOOP_REGISTER_RECONCILER,
1990
- reconcileSession: context?.reconcileSession ?? NOOP_RECONCILE_SESSION
1991
- };
1992
- }
1993
- function useOpenGeniClient(override = {}) {
1994
- const context = useContext(OpenGeniContext);
1995
- const client = override.client ?? context?.client;
1996
- if (!client) {
1997
- throw new Error(
1998
- "@opengeni/react: no OpenGeni client available. Wrap the tree in <OpenGeniProvider> or pass { client } to the hook."
1999
- );
2000
- }
2001
- return client;
2002
- }
2003
-
2004
- // src/hooks/internal.ts
2005
- import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState4 } from "react";
2006
- function usePolledValue(load, options = {}) {
2007
- const enabled = options.enabled ?? true;
2008
- const pollIntervalMs = options.pollIntervalMs;
2009
- const [data, setData] = useState4(null);
2010
- const [loading, setLoading] = useState4(enabled);
2011
- const [error, setError] = useState4(null);
2012
- const generation = useRef3(0);
2013
- const loadRef = useRef3(load);
2014
- useEffect2(() => {
2015
- if (loadRef.current !== load) {
2016
- loadRef.current = load;
2017
- setData(null);
2018
- setError(null);
2019
- }
2020
- }, [load]);
2021
- const run = useCallback2(async () => {
2022
- const ticket = ++generation.current;
2023
- try {
2024
- const result = await load();
2025
- if (ticket === generation.current) {
2026
- setData(result);
2027
- setError(null);
2028
- setLoading(false);
2029
- }
2030
- } catch (cause) {
2031
- if (ticket === generation.current) {
2032
- setError(cause instanceof Error ? cause : new Error(String(cause)));
2033
- setLoading(false);
2034
- }
2035
- }
2036
- }, [load]);
2037
- useEffect2(() => {
2038
- if (!enabled) {
2039
- setLoading(false);
2040
- return;
2041
- }
2042
- setLoading(true);
2043
- void run();
2044
- if (pollIntervalMs === void 0 || pollIntervalMs <= 0) {
2045
- return () => {
2046
- generation.current += 1;
2047
- };
2048
- }
2049
- const timer = setInterval(() => void run(), pollIntervalMs);
2050
- return () => {
2051
- clearInterval(timer);
2052
- generation.current += 1;
2053
- };
2054
- }, [run, enabled, pollIntervalMs]);
2055
- return { data, loading, error, refresh: run };
2056
- }
2057
- function useMutationRunner() {
2058
- const [mutating, setMutating] = useState4(false);
2059
- const [mutationError, setMutationError] = useState4(null);
2060
- const inFlight = useRef3(0);
2061
- const mounted = useRef3(true);
2062
- useEffect2(() => {
2063
- mounted.current = true;
2064
- return () => {
2065
- mounted.current = false;
2066
- };
2067
- }, []);
2068
- const run = useCallback2(async (operation) => {
2069
- inFlight.current += 1;
2070
- if (mounted.current) {
2071
- setMutating(true);
2072
- setMutationError(null);
2073
- }
2074
- try {
2075
- return await operation();
2076
- } catch (cause) {
2077
- if (mounted.current) {
2078
- setMutationError(cause instanceof Error ? cause : new Error(String(cause)));
2079
- }
2080
- return null;
2081
- } finally {
2082
- inFlight.current -= 1;
2083
- if (mounted.current && inFlight.current === 0) {
2084
- setMutating(false);
2085
- }
2086
- }
2087
- }, []);
2088
- return {
2089
- mutating,
2090
- mutationError,
2091
- clearMutationError: useCallback2(() => setMutationError(null), []),
2092
- run
2093
- };
2094
- }
2095
- function useSessionEventTrigger(client, workspaceId, sessionId, match, onEvent, options = {}) {
2096
- const enabled = options.enabled ?? true;
2097
- const events = options.events;
2098
- const sharedFeed = events !== void 0;
2099
- const matchRef = useRef3(match);
2100
- matchRef.current = match;
2101
- const onEventRef = useRef3(onEvent);
2102
- onEventRef.current = onEvent;
2103
- const consumedRef = useRef3(0);
2104
- const feedKeyRef = useRef3(null);
2105
- useEffect2(() => {
2106
- if (!sharedFeed || !enabled || !sessionId) {
2107
- return;
2108
- }
2109
- const feedKey = `${workspaceId}\0${sessionId}`;
2110
- const firstSequence = events[0]?.sequence ?? 0;
2111
- if (feedKeyRef.current !== feedKey || firstSequence > consumedRef.current + 1) {
2112
- feedKeyRef.current = feedKey;
2113
- consumedRef.current = 0;
2114
- }
2115
- for (const event of events) {
2116
- if (event.sequence <= consumedRef.current) {
2117
- continue;
2118
- }
2119
- consumedRef.current = event.sequence;
2120
- if (matchRef.current(event)) {
2121
- onEventRef.current(event);
2122
- }
2123
- }
2124
- }, [sharedFeed, enabled, events, workspaceId, sessionId]);
2125
- useEffect2(() => {
2126
- if (sharedFeed || !enabled || !sessionId) {
2127
- return;
2128
- }
2129
- const controller = new AbortController();
2130
- void (async () => {
2131
- try {
2132
- const session = await client.getSession(workspaceId, sessionId);
2133
- if (controller.signal.aborted) {
2134
- return;
2135
- }
2136
- const stream = client.streamEvents(workspaceId, sessionId, {
2137
- after: session.lastSequence,
2138
- signal: controller.signal
2139
- });
2140
- for await (const event of stream) {
2141
- if (matchRef.current(event)) {
2142
- onEventRef.current(event);
2143
- }
2144
- }
2145
- } catch {
2146
- }
2147
- })();
2148
- return () => {
2149
- controller.abort();
2150
- };
2151
- }, [sharedFeed, enabled, client, workspaceId, sessionId]);
2152
- }
2153
- function useDebouncedCallback(callback, delayMs = 150) {
2154
- const callbackRef = useRef3(callback);
2155
- callbackRef.current = callback;
2156
- const timerRef = useRef3(null);
2157
- useEffect2(() => {
2158
- return () => {
2159
- if (timerRef.current !== null) {
2160
- clearTimeout(timerRef.current);
2161
- }
2162
- };
2163
- }, []);
2164
- return useCallback2(() => {
2165
- if (timerRef.current !== null) {
2166
- clearTimeout(timerRef.current);
2167
- }
2168
- timerRef.current = setTimeout(() => {
2169
- timerRef.current = null;
2170
- callbackRef.current();
2171
- }, delayMs);
2172
- }, [delayMs]);
2173
- }
2174
1852
 
2175
1853
  // src/hooks/use-machines.ts
2176
1854
  var EMPTY = { activeSandboxId: null, activeEpoch: 0, machines: [] };
@@ -2178,9 +1856,18 @@ function useMachines(options = {}) {
2178
1856
  const { client, workspaceId } = useOpenGeni(options);
2179
1857
  const machinesClient = options.machinesClient ?? client;
2180
1858
  const sessionId = options.sessionId;
2181
- const load = useCallback3(async () => {
2182
- return await machinesClient.listMachines(workspaceId, sessionId ? { sessionId } : void 0);
2183
- }, [machinesClient, workspaceId, sessionId]);
1859
+ const identityKey = `${workspaceId}\0${sessionId ?? ""}`;
1860
+ const identityRef = useRef3(identityKey);
1861
+ identityRef.current = identityKey;
1862
+ const load = useCallback2(
1863
+ async (signal) => {
1864
+ return await machinesClient.listMachines(workspaceId, {
1865
+ ...sessionId ? { sessionId } : {},
1866
+ ...signal ? { signal } : {}
1867
+ });
1868
+ },
1869
+ [machinesClient, workspaceId, sessionId]
1870
+ );
2184
1871
  const {
2185
1872
  data: loadedData,
2186
1873
  loading,
@@ -2190,27 +1877,31 @@ function useMachines(options = {}) {
2190
1877
  pollIntervalMs: options.pollIntervalMs,
2191
1878
  enabled: options.enabled
2192
1879
  });
2193
- const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
2194
- const [attachingSandboxId, setAttachingSandboxId] = useState5(null);
1880
+ const { run, mutating, mutationError, clearMutationError } = useMutationRunner(identityKey);
1881
+ const [attachState, setAttachState] = useState4(() => ({ identity: identityKey, sandboxId: null }));
1882
+ const attachingSandboxId = attachState.identity === identityKey ? attachState.sandboxId : null;
2195
1883
  const data = loadedData ?? EMPTY;
2196
1884
  const canAttach = sessionId !== void 0 && (typeof machinesClient.attachMachine === "function" || typeof machinesClient.swapActiveSandbox === "function");
2197
- const attach = useCallback3(
1885
+ const attach = useCallback2(
2198
1886
  async (sandboxId) => {
2199
1887
  if (sessionId === void 0) return false;
2200
1888
  const runSwap = machinesClient.attachMachine ? () => machinesClient.attachMachine(workspaceId, sessionId, sandboxId) : machinesClient.swapActiveSandbox ? () => machinesClient.swapActiveSandbox(workspaceId, sessionId, { target: sandboxId }) : null;
2201
1889
  if (!runSwap) return false;
2202
- setAttachingSandboxId(sandboxId);
1890
+ const ownedIdentity = identityKey;
1891
+ setAttachState({ identity: ownedIdentity, sandboxId });
2203
1892
  const result = await run(async () => {
2204
1893
  await runSwap();
2205
1894
  return true;
2206
1895
  });
2207
- setAttachingSandboxId(null);
2208
- if (result) await refresh();
1896
+ if (identityRef.current === ownedIdentity) {
1897
+ setAttachState({ identity: ownedIdentity, sandboxId: null });
1898
+ if (result) await refresh();
1899
+ }
2209
1900
  return result === true;
2210
1901
  },
2211
- [machinesClient, workspaceId, sessionId, run, refresh]
1902
+ [machinesClient, workspaceId, sessionId, identityKey, run, refresh]
2212
1903
  );
2213
- const fetchSeries = useCallback3(
1904
+ const fetchSeries = useCallback2(
2214
1905
  async (enrollmentId, window2 = "1h") => {
2215
1906
  if (!machinesClient.machineMetricsSeries) return [];
2216
1907
  return await machinesClient.machineMetricsSeries(workspaceId, enrollmentId, { window: window2 });
@@ -2236,21 +1927,6 @@ function useMachines(options = {}) {
2236
1927
 
2237
1928
  export {
2238
1929
  OpenGeniProvider,
2239
- useOpenGeni,
2240
- useOpenGeniClient,
2241
- usePolledValue,
2242
- useMutationRunner,
2243
- useSessionEventTrigger,
2244
- useDebouncedCallback,
2245
- formatRelativeTime,
2246
- formatBytes,
2247
- truncate,
2248
- stringifyPayload,
2249
- tryParseJson,
2250
- CREDIT_EXHAUSTION_MESSAGE,
2251
- isCreditExhaustion,
2252
- humanizeFailureReason,
2253
- cn,
2254
1930
  useMachines,
2255
1931
  connectionStatusForState,
2256
1932
  CONNECTION_STATUS_META,
@@ -2277,4 +1953,4 @@ export {
2277
1953
  EnrollmentDeviceFlow,
2278
1954
  EnrollmentConsent
2279
1955
  };
2280
- //# sourceMappingURL=chunk-TOJR776I.js.map
1956
+ //# sourceMappingURL=chunk-6BXBGJ3B.js.map