@opengeni/react 0.11.0 → 0.13.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 (85) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-5C7RGAWA.js → chunk-NFYVQWIB.js} +170 -64
  3. package/dist/chunk-NFYVQWIB.js.map +1 -0
  4. package/dist/index.d.ts +791 -145
  5. package/dist/index.js +5447 -2213
  6. package/dist/index.js.map +1 -1
  7. package/dist/{machines-BeZ3bD3t.d.ts → machines-CnlMb7E-.d.ts} +5 -5
  8. package/dist/machines.d.ts +1 -1
  9. package/dist/machines.js +1 -1
  10. package/package.json +17 -12
  11. package/src/approvals.ts +16 -4
  12. package/src/client.ts +32 -5
  13. package/src/commands/index.ts +1 -7
  14. package/src/commands/registry.ts +46 -12
  15. package/src/components/chat-composer.tsx +135 -39
  16. package/src/components/code-editor.tsx +28 -36
  17. package/src/components/command-palette.tsx +26 -5
  18. package/src/components/desktop-viewer.tsx +29 -16
  19. package/src/components/diff-view.tsx +27 -189
  20. package/src/components/enrollment-consent.tsx +28 -10
  21. package/src/components/enrollment-device-flow.tsx +8 -5
  22. package/src/components/file-browser.tsx +190 -56
  23. package/src/components/fleet-tile.tsx +13 -4
  24. package/src/components/machine-card.tsx +16 -4
  25. package/src/components/machine-dock-bar.tsx +20 -8
  26. package/src/components/machine-metrics.tsx +31 -8
  27. package/src/components/machine-status-pill.tsx +26 -5
  28. package/src/components/machines-dashboard.tsx +8 -2
  29. package/src/components/markdown.tsx +39 -9
  30. package/src/components/message-timeline.tsx +633 -109
  31. package/src/components/pierre-diff.tsx +83 -15
  32. package/src/components/pierre-file.tsx +10 -9
  33. package/src/components/sandbox-files.tsx +110 -227
  34. package/src/components/sandbox-terminal.tsx +256 -88
  35. package/src/components/sandbox-workspace.tsx +823 -0
  36. package/src/components/session-status.tsx +28 -2
  37. package/src/components/workbench-changes.tsx +541 -0
  38. package/src/components/workspace-dock.tsx +85 -22
  39. package/src/hooks/internal.ts +5 -2
  40. package/src/hooks/use-available-models.ts +7 -2
  41. package/src/hooks/use-billing-usage.ts +4 -1
  42. package/src/hooks/use-codex-accounts.ts +74 -44
  43. package/src/hooks/use-composer.ts +57 -36
  44. package/src/hooks/use-environments.ts +92 -47
  45. package/src/hooks/use-file-attachments.ts +101 -55
  46. package/src/hooks/use-goal.ts +52 -16
  47. package/src/hooks/use-machine-chip.ts +134 -0
  48. package/src/hooks/use-machines.ts +34 -16
  49. package/src/hooks/use-packs.ts +24 -19
  50. package/src/hooks/use-relay-frame-stream.ts +5 -2
  51. package/src/hooks/use-rigs.ts +335 -0
  52. package/src/hooks/use-sandbox-files.ts +213 -39
  53. package/src/hooks/use-sandbox-git.ts +188 -11
  54. package/src/hooks/use-sandbox-terminal.ts +18 -14
  55. package/src/hooks/use-scheduled-tasks.ts +10 -2
  56. package/src/hooks/use-session-capabilities.ts +77 -20
  57. package/src/hooks/use-session-control.ts +49 -21
  58. package/src/hooks/use-session-events.ts +48 -20
  59. package/src/hooks/use-session-lineage.ts +119 -0
  60. package/src/hooks/use-session.ts +43 -28
  61. package/src/hooks/use-slash-commands.ts +6 -4
  62. package/src/hooks/use-turn-queue.ts +74 -147
  63. package/src/hooks/use-windowed-sections.ts +204 -0
  64. package/src/hooks/use-workspace-capture.ts +233 -0
  65. package/src/hooks/use-workspace-edit.ts +231 -0
  66. package/src/hooks/use-workspace-sessions.ts +48 -5
  67. package/src/hooks/use-workspaces.ts +18 -15
  68. package/src/index.ts +128 -23
  69. package/src/lib/format.ts +44 -6
  70. package/src/lib/xterm-renderer.ts +65 -0
  71. package/src/lib/xterm-theme.ts +224 -18
  72. package/src/machines.ts +13 -3
  73. package/src/provider.tsx +3 -1
  74. package/src/timeline/activity-rail.tsx +213 -54
  75. package/src/timeline/disclosure-context.tsx +12 -2
  76. package/src/timeline/index.ts +21 -2
  77. package/src/timeline/parsers.ts +44 -10
  78. package/src/timeline/projection.ts +435 -84
  79. package/src/timeline/shared.tsx +111 -24
  80. package/src/timeline/tool-diff.tsx +24 -20
  81. package/src/timeline/tool-renderers.tsx +98 -21
  82. package/src/timeline/turn-summary.tsx +76 -23
  83. package/src/timeline/types.ts +96 -12
  84. package/styles/tokens.css +2 -2
  85. package/dist/chunk-5C7RGAWA.js.map +0 -1
@@ -21,7 +21,10 @@ import type { MachinesResponse, MachineView, MetricSample } from "../types/machi
21
21
  */
22
22
  export type MachinesClientLike = {
23
23
  /** GET /v1/workspaces/:ws/machines — the dashboard list + active pointer. */
24
- listMachines: (workspaceId: string, options?: { sessionId?: string }) => Promise<MachinesResponse>;
24
+ listMachines: (
25
+ workspaceId: string,
26
+ options?: { sessionId?: string },
27
+ ) => Promise<MachinesResponse>;
25
28
  /** GET .../machines/:enrollmentId/metrics/series — the downsampled history. */
26
29
  machineMetricsSeries?: (
27
30
  workspaceId: string,
@@ -69,7 +72,10 @@ export type UseMachinesResult = {
69
72
  /** Whether the host wired an attach/swap path (drives the card affordance). */
70
73
  canAttach: boolean;
71
74
  /** Fetch a downsampled metric series for one enrolled machine. */
72
- fetchSeries: (enrollmentId: string, window?: "15m" | "1h" | "6h" | "24h") => Promise<MetricSample[]>;
75
+ fetchSeries: (
76
+ enrollmentId: string,
77
+ window?: "15m" | "1h" | "6h" | "24h",
78
+ ) => Promise<MetricSample[]>;
73
79
  attaching: boolean;
74
80
  /** The sandbox id of the in-flight attach (for per-card spinner gating). */
75
81
  attachingSandboxId: string | null;
@@ -89,19 +95,28 @@ const EMPTY: MachinesResponse = { activeSandboxId: null, activeEpoch: 0, machine
89
95
  */
90
96
  export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult {
91
97
  const { client, workspaceId } = useOpenGeni(options);
92
- const machinesClient = (options.machinesClient ?? (client as unknown as MachinesClientLike)) satisfies MachinesClientLike;
98
+ const machinesClient = (options.machinesClient ??
99
+ (client as unknown as MachinesClientLike)) satisfies MachinesClientLike;
93
100
  const sessionId = options.sessionId;
94
101
 
95
102
  const load = useCallback(async () => {
96
103
  return await machinesClient.listMachines(workspaceId, sessionId ? { sessionId } : undefined);
97
104
  }, [machinesClient, workspaceId, sessionId]);
98
105
 
99
- const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled });
100
- const mutation = useMutationRunner();
106
+ const {
107
+ data: loadedData,
108
+ loading,
109
+ error,
110
+ refresh,
111
+ } = usePolledValue(load, {
112
+ pollIntervalMs: options.pollIntervalMs,
113
+ enabled: options.enabled,
114
+ });
115
+ const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
101
116
  // The sandbox id of the in-flight attach (drives the per-card spinner).
102
117
  const [attachingSandboxId, setAttachingSandboxId] = useState<string | null>(null);
103
118
 
104
- const data = state.data ?? EMPTY;
119
+ const data = loadedData ?? EMPTY;
105
120
  // The swap is session-scoped: a host adapter (`attachMachine`) wins; otherwise
106
121
  // the default `swapActiveSandbox` path is wired whenever a sessionId is in
107
122
  // scope. Either way attach needs a sessionId to point at.
@@ -120,19 +135,22 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
120
135
  : null;
121
136
  if (!runSwap) return false;
122
137
  setAttachingSandboxId(sandboxId);
123
- const result = await mutation.run(async () => {
138
+ const result = await run(async () => {
124
139
  await runSwap();
125
140
  return true;
126
141
  });
127
142
  setAttachingSandboxId(null);
128
- if (result) await state.refresh();
143
+ if (result) await refresh();
129
144
  return result === true;
130
145
  },
131
- [machinesClient, workspaceId, sessionId, mutation.run, state.refresh],
146
+ [machinesClient, workspaceId, sessionId, run, refresh],
132
147
  );
133
148
 
134
149
  const fetchSeries = useCallback(
135
- async (enrollmentId: string, window: "15m" | "1h" | "6h" | "24h" = "1h"): Promise<MetricSample[]> => {
150
+ async (
151
+ enrollmentId: string,
152
+ window: "15m" | "1h" | "6h" | "24h" = "1h",
153
+ ): Promise<MetricSample[]> => {
136
154
  if (!machinesClient.machineMetricsSeries) return [];
137
155
  return await machinesClient.machineMetricsSeries(workspaceId, enrollmentId, { window });
138
156
  },
@@ -143,15 +161,15 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
143
161
  machines: data.machines,
144
162
  activeSandboxId: data.activeSandboxId,
145
163
  activeEpoch: data.activeEpoch,
146
- loading: state.loading,
147
- error: state.error,
148
- refresh: state.refresh,
164
+ loading,
165
+ error,
166
+ refresh,
149
167
  attach,
150
168
  canAttach,
151
169
  fetchSeries,
152
- attaching: mutation.mutating,
170
+ attaching: mutating,
153
171
  attachingSandboxId,
154
- mutationError: mutation.mutationError,
155
- clearMutationError: mutation.clearMutationError,
172
+ mutationError,
173
+ clearMutationError,
156
174
  };
157
175
  }
@@ -9,6 +9,8 @@ import { useCallback } from "react";
9
9
  import { useOpenGeni, type ClientOverride } from "../provider";
10
10
  import { useMutationRunner, usePolledValue } from "./internal";
11
11
 
12
+ const EMPTY_INSTALLATIONS: PackInstallation[] = [];
13
+
12
14
  export type UsePacksOptions = ClientOverride & {
13
15
  pollIntervalMs?: number | undefined;
14
16
  enabled?: boolean | undefined;
@@ -38,46 +40,49 @@ export type UsePacksResult = {
38
40
  export function usePacks(options: UsePacksOptions = {}): UsePacksResult {
39
41
  const { client, workspaceId } = useOpenGeni(options);
40
42
  const load = useCallback(async () => await client.listPacks(workspaceId), [client, workspaceId]);
41
- const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled });
42
- const mutation = useMutationRunner();
43
+ const { data, loading, error, refresh } = usePolledValue(load, {
44
+ pollIntervalMs: options.pollIntervalMs,
45
+ enabled: options.enabled,
46
+ });
47
+ const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
43
48
 
44
49
  const register = useCallback(
45
50
  async (manifest: RegisterCapabilityPackRequest): Promise<WorkspaceRegisteredPack | null> => {
46
- const result = await mutation.run(() => client.registerPack(workspaceId, manifest));
51
+ const result = await run(() => client.registerPack(workspaceId, manifest));
47
52
  if (result) {
48
- await state.refresh();
53
+ await refresh();
49
54
  }
50
55
  return result;
51
56
  },
52
- [client, workspaceId, mutation.run, state.refresh],
57
+ [client, workspaceId, run, refresh],
53
58
  );
54
59
 
55
60
  const enable = useCallback(
56
61
  async (packId: string, request: EnablePackRequest = {}): Promise<PackInstallation | null> => {
57
- const result = await mutation.run(() => client.enablePack(workspaceId, packId, request));
62
+ const result = await run(() => client.enablePack(workspaceId, packId, request));
58
63
  if (result) {
59
- await state.refresh();
64
+ await refresh();
60
65
  }
61
66
  return result;
62
67
  },
63
- [client, workspaceId, mutation.run, state.refresh],
68
+ [client, workspaceId, run, refresh],
64
69
  );
65
70
 
66
71
  const remove = useCallback(
67
72
  async (packId: string): Promise<boolean> => {
68
- const result = await mutation.run(async () => {
73
+ const result = await run(async () => {
69
74
  await client.deletePack(workspaceId, packId);
70
75
  return true;
71
76
  });
72
77
  if (result) {
73
- await state.refresh();
78
+ await refresh();
74
79
  }
75
80
  return result === true;
76
81
  },
77
- [client, workspaceId, mutation.run, state.refresh],
82
+ [client, workspaceId, run, refresh],
78
83
  );
79
84
 
80
- const installations = state.data?.installations ?? [];
85
+ const installations = data?.installations ?? EMPTY_INSTALLATIONS;
81
86
  const installationFor = useCallback(
82
87
  (packId: string): PackInstallation | null =>
83
88
  installations.find((installation) => installation.packId === packId) ?? null,
@@ -85,17 +90,17 @@ export function usePacks(options: UsePacksOptions = {}): UsePacksResult {
85
90
  );
86
91
 
87
92
  return {
88
- packs: state.data?.packs ?? [],
93
+ packs: data?.packs ?? [],
89
94
  installations,
90
95
  installationFor,
91
- loading: state.loading,
92
- error: state.error,
93
- refresh: state.refresh,
96
+ loading,
97
+ error,
98
+ refresh,
94
99
  register,
95
100
  enable,
96
101
  remove,
97
- mutating: mutation.mutating,
98
- mutationError: mutation.mutationError,
99
- clearMutationError: mutation.clearMutationError,
102
+ mutating,
103
+ mutationError,
104
+ clearMutationError,
100
105
  };
101
106
  }
@@ -85,7 +85,9 @@ function defaultWebSocketFactory(url: string): DesktopWebSocketLike {
85
85
  * decoded at a time; a frame arriving mid-decode replaces the pending one
86
86
  * (latest-wins), so a slow decode can never build an unbounded queue.
87
87
  */
88
- export function useRelayFrameStream(options: UseRelayFrameStreamOptions): UseRelayFrameStreamResult {
88
+ export function useRelayFrameStream(
89
+ options: UseRelayFrameStreamOptions,
90
+ ): UseRelayFrameStreamResult {
89
91
  const { capability, containerRef, webSocketFactory } = options;
90
92
  const [state, setState] = useState<DesktopConnectionState>("idle");
91
93
  const [error, setError] = useState<Error | null>(null);
@@ -183,7 +185,8 @@ export function useRelayFrameStream(options: UseRelayFrameStreamOptions): UseRel
183
185
  if (decoding) return;
184
186
  decoding = true;
185
187
  try {
186
- while (!disposed && pending) {
188
+ while (pending) {
189
+ if (disposed) break;
187
190
  const data = pending;
188
191
  pending = null;
189
192
  let bmp: ImageBitmap;
@@ -0,0 +1,335 @@
1
+ import type {
2
+ CreateRigRequest,
3
+ ProposeRigChangeRequest,
4
+ Rig,
5
+ RigChange,
6
+ RigVersion,
7
+ UpdateRigRequest,
8
+ } from "@opengeni/sdk";
9
+ import { useCallback } from "react";
10
+ import { useOpenGeni, type ClientOverride } from "../provider";
11
+ import { useMutationRunner, usePolledValue } from "./internal";
12
+
13
+ export type UseRigsOptions = ClientOverride & {
14
+ pollIntervalMs?: number | undefined;
15
+ enabled?: boolean | undefined;
16
+ };
17
+
18
+ export type UseRigsResult = {
19
+ rigs: Rig[];
20
+ loading: boolean;
21
+ error: Error | null;
22
+ refresh: () => Promise<void>;
23
+ create: (request: CreateRigRequest) => Promise<Rig | null>;
24
+ update: (rigId: string, request: UpdateRigRequest) => Promise<Rig | null>;
25
+ remove: (rigId: string) => Promise<boolean>;
26
+ listVersions: (rigId: string) => Promise<RigVersion[] | null>;
27
+ activateVersion: (rigId: string, versionId: string) => Promise<RigVersion | null>;
28
+ listChanges: (rigId: string) => Promise<RigChange[] | null>;
29
+ proposeChange: (rigId: string, request: ProposeRigChangeRequest) => Promise<RigChange | null>;
30
+ mutating: boolean;
31
+ mutationError: Error | null;
32
+ clearMutationError: () => void;
33
+ };
34
+
35
+ /**
36
+ * Rigs (workspace-scoped, versioned sandbox machine definitions). The list
37
+ * polls; version/change reads are on-demand (they are per-rig detail, not part
38
+ * of the list surface).
39
+ */
40
+ export function useRigs(options: UseRigsOptions = {}): UseRigsResult {
41
+ const { client, workspaceId } = useOpenGeni(options);
42
+ const load = useCallback(async () => await client.listRigs(workspaceId), [client, workspaceId]);
43
+ const { data, loading, error, refresh } = usePolledValue(load, {
44
+ pollIntervalMs: options.pollIntervalMs,
45
+ enabled: options.enabled,
46
+ });
47
+ const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
48
+
49
+ const create = useCallback(
50
+ async (request: CreateRigRequest): Promise<Rig | null> => {
51
+ const result = await run(() => client.createRig(workspaceId, request));
52
+ if (result) {
53
+ await refresh();
54
+ }
55
+ return result;
56
+ },
57
+ [client, workspaceId, run, refresh],
58
+ );
59
+
60
+ const update = useCallback(
61
+ async (rigId: string, request: UpdateRigRequest): Promise<Rig | null> => {
62
+ const result = await run(() => client.updateRig(workspaceId, rigId, request));
63
+ if (result) {
64
+ await refresh();
65
+ }
66
+ return result;
67
+ },
68
+ [client, workspaceId, run, refresh],
69
+ );
70
+
71
+ const remove = useCallback(
72
+ async (rigId: string): Promise<boolean> => {
73
+ const result = await run(async () => {
74
+ await client.deleteRig(workspaceId, rigId);
75
+ return true;
76
+ });
77
+ if (result) {
78
+ await refresh();
79
+ }
80
+ return result === true;
81
+ },
82
+ [client, workspaceId, run, refresh],
83
+ );
84
+
85
+ const listVersions = useCallback(
86
+ async (rigId: string): Promise<RigVersion[] | null> => {
87
+ return await run(() => client.listRigVersions(workspaceId, rigId));
88
+ },
89
+ [client, workspaceId, run],
90
+ );
91
+
92
+ const activateVersion = useCallback(
93
+ async (rigId: string, versionId: string): Promise<RigVersion | null> => {
94
+ const result = await run(() => client.activateRigVersion(workspaceId, rigId, versionId));
95
+ if (result) {
96
+ await refresh();
97
+ }
98
+ return result;
99
+ },
100
+ [client, workspaceId, run, refresh],
101
+ );
102
+
103
+ const listChanges = useCallback(
104
+ async (rigId: string): Promise<RigChange[] | null> => {
105
+ return await run(() => client.listRigChanges(workspaceId, rigId));
106
+ },
107
+ [client, workspaceId, run],
108
+ );
109
+
110
+ const proposeChange = useCallback(
111
+ async (rigId: string, request: ProposeRigChangeRequest): Promise<RigChange | null> => {
112
+ const result = await run(() => client.proposeRigChange(workspaceId, rigId, request));
113
+ if (result) {
114
+ await refresh();
115
+ }
116
+ return result;
117
+ },
118
+ [client, workspaceId, run, refresh],
119
+ );
120
+
121
+ return {
122
+ rigs: data ?? [],
123
+ loading,
124
+ error,
125
+ refresh,
126
+ create,
127
+ update,
128
+ remove,
129
+ listVersions,
130
+ activateVersion,
131
+ listChanges,
132
+ proposeChange,
133
+ mutating,
134
+ mutationError,
135
+ clearMutationError,
136
+ };
137
+ }
138
+
139
+ export type UseRigOptions = ClientOverride & {
140
+ pollIntervalMs?: number | undefined;
141
+ enabled?: boolean | undefined;
142
+ };
143
+
144
+ export type UseRigResult = {
145
+ rig: Rig | null;
146
+ loading: boolean;
147
+ error: Error | null;
148
+ refresh: () => Promise<void>;
149
+ update: (request: UpdateRigRequest) => Promise<Rig | null>;
150
+ remove: () => Promise<boolean>;
151
+ activateVersion: (versionId: string) => Promise<RigVersion | null>;
152
+ proposeChange: (request: ProposeRigChangeRequest) => Promise<RigChange | null>;
153
+ /** Re-run verification for a change (asynchronous — poll for the outcome). */
154
+ verifyChange: (changeId: string) => Promise<RigChange | null>;
155
+ /** Promote a verified definition_edit into a new active version (rigs:manage). */
156
+ promoteChange: (changeId: string) => Promise<RigVersion | null>;
157
+ /** Re-verify the active version's checks in a clean throwaway sandbox. */
158
+ verify: () => Promise<{ ok: boolean; versionId: string } | null>;
159
+ mutating: boolean;
160
+ mutationError: Error | null;
161
+ clearMutationError: () => void;
162
+ };
163
+
164
+ /**
165
+ * A single rig (its active version + counts), polled so verification and
166
+ * promotion state stay live. Owns the rig's write actions; the versions and
167
+ * changes lists are separate polled reads (`useRigVersions`/`useRigChanges`).
168
+ */
169
+ export function useRig(rigId: string, options: UseRigOptions = {}): UseRigResult {
170
+ const { client, workspaceId } = useOpenGeni(options);
171
+ const load = useCallback(
172
+ async () => await client.getRig(workspaceId, rigId),
173
+ [client, workspaceId, rigId],
174
+ );
175
+ const { data, loading, error, refresh } = usePolledValue(load, {
176
+ pollIntervalMs: options.pollIntervalMs,
177
+ enabled: options.enabled,
178
+ });
179
+ const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
180
+
181
+ const update = useCallback(
182
+ async (request: UpdateRigRequest): Promise<Rig | null> => {
183
+ const result = await run(() => client.updateRig(workspaceId, rigId, request));
184
+ if (result) {
185
+ await refresh();
186
+ }
187
+ return result;
188
+ },
189
+ [client, workspaceId, rigId, run, refresh],
190
+ );
191
+
192
+ const remove = useCallback(async (): Promise<boolean> => {
193
+ const result = await run(async () => {
194
+ await client.deleteRig(workspaceId, rigId);
195
+ return true;
196
+ });
197
+ return result === true;
198
+ }, [client, workspaceId, rigId, run]);
199
+
200
+ const activateVersion = useCallback(
201
+ async (versionId: string): Promise<RigVersion | null> => {
202
+ const result = await run(() => client.activateRigVersion(workspaceId, rigId, versionId));
203
+ if (result) {
204
+ await refresh();
205
+ }
206
+ return result;
207
+ },
208
+ [client, workspaceId, rigId, run, refresh],
209
+ );
210
+
211
+ const proposeChange = useCallback(
212
+ async (request: ProposeRigChangeRequest): Promise<RigChange | null> => {
213
+ const result = await run(() => client.proposeRigChange(workspaceId, rigId, request));
214
+ if (result) {
215
+ await refresh();
216
+ }
217
+ return result;
218
+ },
219
+ [client, workspaceId, rigId, run, refresh],
220
+ );
221
+
222
+ const verifyChange = useCallback(
223
+ async (changeId: string): Promise<RigChange | null> => {
224
+ const result = await run(() => client.verifyRigChange(workspaceId, rigId, changeId));
225
+ if (result) {
226
+ await refresh();
227
+ }
228
+ return result;
229
+ },
230
+ [client, workspaceId, rigId, run, refresh],
231
+ );
232
+
233
+ const promoteChange = useCallback(
234
+ async (changeId: string): Promise<RigVersion | null> => {
235
+ const result = await run(() => client.promoteRigChange(workspaceId, rigId, changeId));
236
+ if (result) {
237
+ await refresh();
238
+ }
239
+ return result;
240
+ },
241
+ [client, workspaceId, rigId, run, refresh],
242
+ );
243
+
244
+ const verify = useCallback(async (): Promise<{ ok: boolean; versionId: string } | null> => {
245
+ return await run(() => client.verifyRig(workspaceId, rigId));
246
+ }, [client, workspaceId, rigId, run]);
247
+
248
+ return {
249
+ rig: data,
250
+ loading,
251
+ error,
252
+ refresh,
253
+ update,
254
+ remove,
255
+ activateVersion,
256
+ proposeChange,
257
+ verifyChange,
258
+ promoteChange,
259
+ verify,
260
+ mutating,
261
+ mutationError,
262
+ clearMutationError,
263
+ };
264
+ }
265
+
266
+ export type UseRigVersionsOptions = ClientOverride & {
267
+ pollIntervalMs?: number | undefined;
268
+ enabled?: boolean | undefined;
269
+ };
270
+
271
+ export type UseRigVersionsResult = {
272
+ versions: RigVersion[];
273
+ loading: boolean;
274
+ error: Error | null;
275
+ refresh: () => Promise<void>;
276
+ };
277
+
278
+ /** A rig's append-only version history, newest-first (polled). */
279
+ export function useRigVersions(
280
+ rigId: string,
281
+ options: UseRigVersionsOptions = {},
282
+ ): UseRigVersionsResult {
283
+ const { client, workspaceId } = useOpenGeni(options);
284
+ const load = useCallback(
285
+ async () => await client.listRigVersions(workspaceId, rigId),
286
+ [client, workspaceId, rigId],
287
+ );
288
+ const state = usePolledValue(load, {
289
+ pollIntervalMs: options.pollIntervalMs,
290
+ enabled: options.enabled,
291
+ });
292
+ return {
293
+ versions: state.data ?? [],
294
+ loading: state.loading,
295
+ error: state.error,
296
+ refresh: state.refresh,
297
+ };
298
+ }
299
+
300
+ export type UseRigChangesOptions = ClientOverride & {
301
+ pollIntervalMs?: number | undefined;
302
+ enabled?: boolean | undefined;
303
+ };
304
+
305
+ export type UseRigChangesResult = {
306
+ changes: RigChange[];
307
+ loading: boolean;
308
+ error: Error | null;
309
+ refresh: () => Promise<void>;
310
+ };
311
+
312
+ /**
313
+ * A rig's change queue (polled). The default poll cadence lets a change move
314
+ * through verifying → merged/rejected without a manual refresh.
315
+ */
316
+ export function useRigChanges(
317
+ rigId: string,
318
+ options: UseRigChangesOptions = {},
319
+ ): UseRigChangesResult {
320
+ const { client, workspaceId } = useOpenGeni(options);
321
+ const load = useCallback(
322
+ async () => await client.listRigChanges(workspaceId, rigId),
323
+ [client, workspaceId, rigId],
324
+ );
325
+ const state = usePolledValue(load, {
326
+ pollIntervalMs: options.pollIntervalMs,
327
+ enabled: options.enabled,
328
+ });
329
+ return {
330
+ changes: state.data ?? [],
331
+ loading: state.loading,
332
+ error: state.error,
333
+ refresh: state.refresh,
334
+ };
335
+ }