@opengeni/react 1.0.1 → 2.2.0-canary.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 (174) hide show
  1. package/README.md +33 -10
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
  8. package/dist/chunk-5RZN3DD5.js.map +1 -0
  9. package/dist/chunk-5YK2ZDIG.js +116 -0
  10. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  11. package/dist/chunk-H5YGYNXS.js +329 -0
  12. package/dist/chunk-H5YGYNXS.js.map +1 -0
  13. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  14. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  15. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  16. package/dist/chunk-JOAVA2TW.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
  24. package/dist/chunk-UOLJWT5Y.js.map +1 -0
  25. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  26. package/dist/chunk-W6NFJXSA.js.map +1 -0
  27. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  28. package/dist/chunk-X4A66EWZ.js.map +1 -0
  29. package/dist/chunk-X4JAFD5R.js +3907 -0
  30. package/dist/chunk-X4JAFD5R.js.map +1 -0
  31. package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
  32. package/dist/chunk-XSA2QAKP.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +6 -3
  83. package/dist/realtime.js +36 -14
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +48 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +91 -55
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +162 -17
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +53 -17
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +476 -23
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +75 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
@@ -1,9 +1,13 @@
1
1
  import {
2
+ AlertTriangleIcon,
2
3
  ArrowRightIcon,
3
4
  CpuIcon,
5
+ CircleCheckIcon,
6
+ DownloadIcon,
4
7
  LaptopIcon,
5
8
  MonitorIcon,
6
9
  MonitorOffIcon,
10
+ LoaderCircleIcon,
7
11
  ServerIcon,
8
12
  UsersIcon,
9
13
  } from "lucide-react";
@@ -20,6 +24,9 @@ export type MachineCardProps = {
20
24
  onAttach?: ((machine: MachineView) => void) | undefined;
21
25
  /** Whether an attach/swap is currently in flight (disables the affordance). */
22
26
  attaching?: boolean | undefined;
27
+ /** Start/retry the exact signed Connected Machine agent update. */
28
+ onUpdateAgent?: ((machine: MachineView) => void) | undefined;
29
+ updatingAgent?: boolean | undefined;
23
30
  /** Short recent history (e.g. 15m) — drives the card's CPU trend + preview. */
24
31
  series?: MetricSample[] | undefined;
25
32
  /** Open the full per-machine telemetry detail. Makes the whole card actionable. */
@@ -45,6 +52,8 @@ export function MachineCard({
45
52
  machine,
46
53
  onAttach,
47
54
  attaching,
55
+ onUpdateAgent,
56
+ updatingAgent,
48
57
  series,
49
58
  onOpenDetail,
50
59
  now,
@@ -59,6 +68,43 @@ export function MachineCard({
59
68
  : null;
60
69
  const cpuPts = (series ?? []).map((s) => ({ t: new Date(s.sampledAt).getTime(), v: s.cpuPct }));
61
70
  const openable = Boolean(onOpenDetail);
71
+ const runtime = machine.runtime;
72
+ const updateRunning = runtime?.versionState === "updating";
73
+ const dispatchConfirmationStalled =
74
+ runtime?.update?.status === "requested" &&
75
+ clockNow - new Date(runtime.update.updatedAt).getTime() >= 30_000;
76
+ const canUpdate =
77
+ Boolean(machine.enrollmentId && onUpdateAgent) &&
78
+ !offline &&
79
+ (runtime?.versionState === "outdated" ||
80
+ runtime?.versionState === "update_failed" ||
81
+ dispatchConfirmationStalled);
82
+ const updateLabel = (() => {
83
+ const update = runtime?.update;
84
+ if (!update) return null;
85
+ switch (update.status) {
86
+ case "requested":
87
+ return dispatchConfirmationStalled ? "Agent confirmation delayed" : "Confirming with agent";
88
+ case "accepted":
89
+ return "Update accepted";
90
+ case "waiting_for_idle":
91
+ return "Waiting for current work";
92
+ case "downloading":
93
+ case "verifying":
94
+ case "applying":
95
+ return `${update.status}…`;
96
+ case "restarting":
97
+ return "Restarting agent";
98
+ case "succeeded":
99
+ return `Updated · v${update.targetVersion}`;
100
+ case "failed": {
101
+ const detail = update.errorCode?.replaceAll("_", " ");
102
+ return update.rolledBack
103
+ ? `Update rolled back safely${detail ? ` · ${detail}` : ""}`
104
+ : `Update failed${detail ? ` · ${detail}` : ""}`;
105
+ }
106
+ }
107
+ })();
62
108
 
63
109
  return (
64
110
  <div
@@ -108,6 +154,10 @@ export function MachineCard({
108
154
  ) : null}
109
155
  </div>
110
156
  <div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-og-xs text-og-fg-subtle">
157
+ <span data-machine-scope={machine.scope} className="capitalize">
158
+ {machine.scope === "user" ? "personal" : machine.scope}
159
+ </span>
160
+ <span aria-hidden>·</span>
111
161
  <span className="capitalize">
112
162
  {machine.isSessionGroup ? "session sandbox" : machine.kind}
113
163
  </span>
@@ -146,6 +196,67 @@ export function MachineCard({
146
196
  </p>
147
197
  ) : null}
148
198
 
199
+ {machine.connectionAuthority.duplicateRunnerDeniedCount > 0 ? (
200
+ <p
201
+ data-runner-conflict
202
+ className="flex items-center gap-1.5 rounded-og-md border border-og-status-failed/30 bg-og-status-failed/5 px-2.5 py-1.5 text-og-xs text-og-status-failed"
203
+ >
204
+ <AlertTriangleIcon className="size-3.5 shrink-0" aria-hidden />
205
+ Competing agent process blocked. Open diagnostics for details.
206
+ </p>
207
+ ) : null}
208
+
209
+ {runtime ? (
210
+ <div
211
+ data-agent-runtime
212
+ className="flex items-center justify-between gap-3 rounded-og-md border border-og-border/70 bg-og-surface-2/35 px-2.5 py-2 text-og-xs"
213
+ >
214
+ <div className="min-w-0">
215
+ <div className="flex items-center gap-1.5 text-og-fg-muted">
216
+ {runtime.versionState === "current" ? (
217
+ <CircleCheckIcon className="size-3.5 text-og-status-idle" aria-hidden />
218
+ ) : updateRunning ? (
219
+ <LoaderCircleIcon className="size-3.5 animate-og-spin text-og-accent" aria-hidden />
220
+ ) : (
221
+ <DownloadIcon className="size-3.5" aria-hidden />
222
+ )}
223
+ <span className="truncate">
224
+ Agent{" "}
225
+ {runtime.installedVersion ? `v${runtime.installedVersion}` : "version unknown"}
226
+ </span>
227
+ </div>
228
+ <p className="mt-0.5 truncate text-og-fg-subtle">
229
+ {updateLabel ??
230
+ (runtime.versionState === "current"
231
+ ? `Current · ${runtime.updateChannel ?? "channel unknown"}`
232
+ : runtime.desiredVersion
233
+ ? `Promoted v${runtime.desiredVersion}`
234
+ : "No promoted version configured")}
235
+ </p>
236
+ </div>
237
+ {canUpdate ? (
238
+ <button
239
+ type="button"
240
+ data-update-agent
241
+ disabled={updatingAgent}
242
+ onClick={(event) => {
243
+ event.stopPropagation();
244
+ onUpdateAgent?.(machine);
245
+ }}
246
+ className="shrink-0 rounded-og-sm border border-og-border px-2 py-1 font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:min-h-11"
247
+ >
248
+ {updatingAgent
249
+ ? "Starting…"
250
+ : dispatchConfirmationStalled
251
+ ? "Retry delivery"
252
+ : runtime.versionState === "update_failed"
253
+ ? "Retry"
254
+ : "Update"}
255
+ </button>
256
+ ) : null}
257
+ </div>
258
+ ) : null}
259
+
149
260
  <MachineMetrics metrics={machine.metrics} />
150
261
 
151
262
  {/* CPU trend — previews the history and invites opening the detail. */}
@@ -7,6 +7,11 @@ export const MACHINE_INPUT_META: Record<MachineInputMember["kind"], string> = {
7
7
  agent_steer_instruction: "Agent direction",
8
8
  child_terminal_result: "Agent finished",
9
9
  media_generation_result: "Video update",
10
+ child_requires_action: "Agent needs input",
11
+ child_requires_action_resolved: "Agent unblocked",
12
+ child_paused: "Agent paused",
13
+ child_waiting_capacity: "Agent waiting for capacity",
14
+ child_progress: "Agent progress",
10
15
  };
11
16
 
12
17
  /**
@@ -35,6 +40,16 @@ export function machineInputBatchLabel(members: readonly MachineInputMember[]):
35
40
  return n === 1 ? "Agent direction" : `${n} agent directions`;
36
41
  case "media_generation_result":
37
42
  return n === 1 ? "Video ready" : `${n} video updates`;
43
+ case "child_requires_action":
44
+ return n === 1 ? "Agent needs input" : `${n} agents need input`;
45
+ case "child_requires_action_resolved":
46
+ return n === 1 ? "Agent unblocked" : `${n} agents unblocked`;
47
+ case "child_paused":
48
+ return n === 1 ? "Agent paused" : `${n} agents paused`;
49
+ case "child_waiting_capacity":
50
+ return n === 1 ? "Agent waiting for capacity" : `${n} agents waiting for capacity`;
51
+ case "child_progress":
52
+ return n === 1 ? "Agent progress" : `${n} agent progress notes`;
38
53
  }
39
54
  }
40
55
  const parts = [...counts.entries()].map(([kind, count]) => {
@@ -7,6 +7,7 @@
7
7
  // `og-*` token system — no new visual language, just the missing depth.
8
8
  // ----------------------------------------------------------------------------
9
9
  import {
10
+ AlertTriangleIcon,
10
11
  ArrowLeftIcon,
11
12
  CpuIcon,
12
13
  LaptopIcon,
@@ -21,6 +22,8 @@ import { deriveHealth, HEALTH_TOKEN, healthPulses } from "./health";
21
22
  import { MetricHistoryChart } from "./metric-history-chart";
22
23
  import { MetricSparkline } from "./metric-sparkline";
23
24
  import { METRICS, METRIC_WINDOWS, pointsFor, WINDOW_LABEL, type MetricWindow } from "./series";
25
+ import { MachineOperationPolicyEditor } from "./machine-operation-policy";
26
+ import type { UpdateMachineOperationPolicyRequest } from "@opengeni/sdk";
24
27
 
25
28
  export type MachineDetailProps = {
26
29
  machine: MachineView;
@@ -32,6 +35,10 @@ export type MachineDetailProps = {
32
35
  onBack?: (() => void) | undefined;
33
36
  /** Open the workspace-admin removal confirmation for a self-hosted machine. */
34
37
  onRemove?: ((machine: MachineView) => void) | undefined;
38
+ onUpdateOperationPolicy?:
39
+ | ((machine: MachineView, request: UpdateMachineOperationPolicyRequest) => Promise<unknown>)
40
+ | undefined;
41
+ updatingOperationPolicy?: boolean | undefined;
35
42
  now?: number | undefined;
36
43
  className?: string | undefined;
37
44
  };
@@ -50,6 +57,8 @@ export function MachineDetail({
50
57
  loadingSeries,
51
58
  onBack,
52
59
  onRemove,
60
+ onUpdateOperationPolicy,
61
+ updatingOperationPolicy,
53
62
  now = Date.now(),
54
63
  className,
55
64
  }: MachineDetailProps) {
@@ -57,6 +66,10 @@ export function MachineDetail({
57
66
  const tone = HEALTH_TOKEN[health.level];
58
67
  const latest = machine.metrics;
59
68
  const sampledAgo = latest ? formatRelativeTime(latest.sampledAt, new Date(now)) : null;
69
+ const authority = machine.connectionAuthority;
70
+ const duplicateDeniedAgo = authority.duplicateRunnerDeniedAt
71
+ ? formatRelativeTime(authority.duplicateRunnerDeniedAt, new Date(now))
72
+ : null;
60
73
 
61
74
  // Only render metrics that have a value (GPU is dropped on headless boxes).
62
75
  const visible = METRICS.filter((m) => !(m.key === "gpu" && latest?.gpuUtilPct == null));
@@ -206,8 +219,42 @@ export function MachineDetail({
206
219
  })}
207
220
  </div>
208
221
  )}
222
+
223
+ {authority.state !== "not_applicable" ? (
224
+ <div
225
+ data-connection-authority={authority.state}
226
+ className="flex flex-wrap items-center justify-between gap-2 rounded-og-md border border-og-border bg-og-surface-1/75 px-3 py-2 text-og-xs"
227
+ >
228
+ <span className="text-og-fg-muted">
229
+ Runner {authority.state.replace("_", " ")} · generation {authority.generation}
230
+ {authority.supersededCount > 0
231
+ ? ` · ${authority.supersededCount} earlier ${authority.supersededCount === 1 ? "runner" : "runners"} fenced`
232
+ : ""}
233
+ </span>
234
+ {authority.duplicateRunnerDeniedCount > 0 ? (
235
+ <span className="inline-flex items-center gap-1.5 text-og-status-failed">
236
+ <AlertTriangleIcon className="size-3.5" aria-hidden />
237
+ Blocked {authority.duplicateRunnerDeniedCount} competing runner
238
+ {authority.duplicateRunnerDeniedCount === 1 ? "" : "s"}
239
+ {duplicateDeniedAgo
240
+ ? ` · ${duplicateDeniedAgo === "now" ? "now" : `${duplicateDeniedAgo} ago`}`
241
+ : ""}
242
+ </span>
243
+ ) : null}
244
+ </div>
245
+ ) : null}
209
246
  </div>
210
247
 
248
+ {onUpdateOperationPolicy && machine.enrollmentId && machine.operationPolicy ? (
249
+ <MachineOperationPolicyEditor
250
+ policy={machine.operationPolicy}
251
+ memorySupported={machine.runtime?.capabilities.operationResourcePolicy === true}
252
+ cpuSupported={machine.runtime?.capabilities.operationCpuQuota === true}
253
+ saving={updatingOperationPolicy}
254
+ onSave={(request) => onUpdateOperationPolicy(machine, request)}
255
+ />
256
+ ) : null}
257
+
211
258
  {/* ── history charts ──────────────────────────────────────────────── */}
212
259
  <div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
213
260
  {visible.map((m) => {
@@ -0,0 +1,179 @@
1
+ import { AlertTriangleIcon, Loader2Icon } from "lucide-react";
2
+ import { useEffect, useState, type FormEvent } from "react";
3
+ import type { MachineOperationPolicy, UpdateMachineOperationPolicyRequest } from "@opengeni/sdk";
4
+
5
+ export type MachineOperationPolicyEditorProps = {
6
+ policy: MachineOperationPolicy;
7
+ memorySupported: boolean;
8
+ cpuSupported: boolean;
9
+ saving?: boolean | undefined;
10
+ onSave: (request: UpdateMachineOperationPolicyRequest) => Promise<unknown> | unknown;
11
+ };
12
+
13
+ function fieldValue(value: number | null): string {
14
+ return value === null ? "" : String(value);
15
+ }
16
+
17
+ function parseByteCount(label: string, value: string): number | null {
18
+ const trimmed = value.trim();
19
+ if (!trimmed) return null;
20
+ if (!/^\d+$/.test(trimmed)) {
21
+ throw new Error(`${label} must be a whole number of bytes or blank`);
22
+ }
23
+ const parsed = Number(trimmed);
24
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
25
+ throw new Error(`${label} must be a positive safe-integer byte count or blank`);
26
+ }
27
+ return parsed;
28
+ }
29
+
30
+ function parseCpuMillicores(value: string): number | null {
31
+ const trimmed = value.trim();
32
+ if (!trimmed) return null;
33
+ if (!/^\d+$/.test(trimmed)) {
34
+ throw new Error("CPU maximum must be a whole number of millicores or blank");
35
+ }
36
+ const parsed = Number(trimmed);
37
+ if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > 0xffff_ffff) {
38
+ throw new Error("CPU maximum must be between 1 and 4294967295 millicores or blank");
39
+ }
40
+ return parsed;
41
+ }
42
+
43
+ /** Exact editor for the optional per-enrollment command resource policy. Blank
44
+ * values remain genuinely unrestricted; CPU is a period-independent ratio. */
45
+ export function MachineOperationPolicyEditor({
46
+ policy,
47
+ memorySupported,
48
+ cpuSupported,
49
+ saving = false,
50
+ onSave,
51
+ }: MachineOperationPolicyEditorProps) {
52
+ const [memoryMaxBytes, setMemoryMaxBytes] = useState(fieldValue(policy.memoryMaxBytes));
53
+ const [memoryHighBytes, setMemoryHighBytes] = useState(fieldValue(policy.memoryHighBytes));
54
+ const [cpuMaxMillicores, setCpuMaxMillicores] = useState(fieldValue(policy.cpuMaxMillicores));
55
+ const [validationError, setValidationError] = useState<string | null>(null);
56
+
57
+ useEffect(() => {
58
+ setMemoryMaxBytes(fieldValue(policy.memoryMaxBytes));
59
+ setMemoryHighBytes(fieldValue(policy.memoryHighBytes));
60
+ setCpuMaxMillicores(fieldValue(policy.cpuMaxMillicores));
61
+ setValidationError(null);
62
+ }, [policy.memoryMaxBytes, policy.memoryHighBytes, policy.cpuMaxMillicores, policy.revision]);
63
+
64
+ const submit = async (event: FormEvent<HTMLFormElement>) => {
65
+ event.preventDefault();
66
+ try {
67
+ const max = parseByteCount("Hard ceiling", memoryMaxBytes);
68
+ const high = parseByteCount("Reclaim threshold", memoryHighBytes);
69
+ if (max !== null && high !== null && high > max) {
70
+ throw new Error("Reclaim threshold cannot exceed the hard ceiling");
71
+ }
72
+ const cpuMax = parseCpuMillicores(cpuMaxMillicores);
73
+ setValidationError(null);
74
+ await onSave({
75
+ memoryMaxBytes: max,
76
+ memoryHighBytes: high,
77
+ cpuMaxMillicores: cpuMax,
78
+ expectedRevision: policy.revision,
79
+ });
80
+ } catch (error) {
81
+ setValidationError(error instanceof Error ? error.message : String(error));
82
+ }
83
+ };
84
+
85
+ const configured =
86
+ policy.memoryMaxBytes !== null ||
87
+ policy.memoryHighBytes !== null ||
88
+ policy.cpuMaxMillicores !== null;
89
+ const memoryConfigured = policy.memoryMaxBytes !== null || policy.memoryHighBytes !== null;
90
+ const unsupportedMemory = memoryConfigured && !memorySupported;
91
+ const unsupportedCpu = policy.cpuMaxMillicores !== null && (!memorySupported || !cpuSupported);
92
+
93
+ return (
94
+ <section
95
+ data-machine-operation-policy
96
+ className="rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-og-sm"
97
+ >
98
+ <div className="flex flex-col gap-1">
99
+ <h2 className="text-og-sm font-semibold text-og-fg">Per-command resource policy</h2>
100
+ <p className="text-og-xs leading-relaxed text-og-fg-muted">
101
+ Optional limits applied separately to each newly admitted exec and Git command. Running
102
+ commands keep their admitted policy. Blank means unrestricted. The runner or host may
103
+ apply a stricter explicit policy; browser, computer-use, and terminal capabilities remain
104
+ unchanged.
105
+ </p>
106
+ </div>
107
+
108
+ {configured && (unsupportedMemory || unsupportedCpu) ? (
109
+ <div className="mt-3 flex gap-2 rounded-og-md border border-og-status-failed/40 bg-og-status-failed/5 p-3 text-og-xs text-og-status-failed">
110
+ <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" aria-hidden />
111
+ <span>
112
+ {unsupportedCpu
113
+ ? "This runner cannot enforce the configured CPU limit."
114
+ : "This runner cannot enforce the configured memory limit."}{" "}
115
+ Command execution fails closed until the current runner, OS, and service configuration
116
+ can enforce it, or the unsupported limit is cleared. You can still save limits now for
117
+ later enforcement.
118
+ </span>
119
+ </div>
120
+ ) : null}
121
+
122
+ <form className="mt-4 grid gap-3 sm:grid-cols-2" onSubmit={submit}>
123
+ <label className="flex flex-col gap-1.5 text-og-xs text-og-fg-muted">
124
+ Hard ceiling (bytes)
125
+ <input
126
+ inputMode="numeric"
127
+ pattern="[0-9]*"
128
+ value={memoryMaxBytes}
129
+ onChange={(event) => setMemoryMaxBytes(event.target.value)}
130
+ placeholder="Unlimited"
131
+ disabled={saving}
132
+ className="min-h-9 rounded-og-sm border border-og-border bg-og-bg px-2.5 font-og-mono text-og-sm text-og-fg outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/50 disabled:opacity-60"
133
+ />
134
+ </label>
135
+ <label className="flex flex-col gap-1.5 text-og-xs text-og-fg-muted">
136
+ Reclaim threshold (bytes)
137
+ <input
138
+ inputMode="numeric"
139
+ pattern="[0-9]*"
140
+ value={memoryHighBytes}
141
+ onChange={(event) => setMemoryHighBytes(event.target.value)}
142
+ placeholder="Unlimited"
143
+ disabled={saving}
144
+ className="min-h-9 rounded-og-sm border border-og-border bg-og-bg px-2.5 font-og-mono text-og-sm text-og-fg outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/50 disabled:opacity-60"
145
+ />
146
+ </label>
147
+ <label className="flex flex-col gap-1.5 text-og-xs text-og-fg-muted">
148
+ CPU maximum (millicores)
149
+ <input
150
+ inputMode="numeric"
151
+ pattern="[0-9]*"
152
+ value={cpuMaxMillicores}
153
+ onChange={(event) => setCpuMaxMillicores(event.target.value)}
154
+ placeholder="Unlimited"
155
+ disabled={saving}
156
+ className="min-h-9 rounded-og-sm border border-og-border bg-og-bg px-2.5 font-og-mono text-og-sm text-og-fg outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/50 disabled:opacity-60"
157
+ />
158
+ <span>1000 millicores = one CPU core. Blank is unlimited.</span>
159
+ </label>
160
+ <div className="flex min-h-9 items-center justify-between gap-3 sm:col-span-2">
161
+ <span
162
+ role={validationError ? "alert" : undefined}
163
+ className="text-og-xs text-og-status-failed"
164
+ >
165
+ {validationError}
166
+ </span>
167
+ <button
168
+ type="submit"
169
+ disabled={saving}
170
+ className="inline-flex min-h-9 items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-xs font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong disabled:cursor-not-allowed disabled:opacity-60"
171
+ >
172
+ {saving ? <Loader2Icon className="size-3.5 animate-spin" aria-hidden /> : null}
173
+ Save policy
174
+ </button>
175
+ </div>
176
+ </form>
177
+ </section>
178
+ );
179
+ }
@@ -14,6 +14,8 @@ export type MachinesDashboardProps = {
14
14
  onAttach?: ((machine: MachineView) => void) | undefined;
15
15
  /** The sandbox id currently being attached/swapped to (disables that card). */
16
16
  attachingSandboxId?: string | null | undefined;
17
+ onUpdateAgent?: ((machine: MachineView) => void) | undefined;
18
+ updatingEnrollmentId?: string | null | undefined;
17
19
  /** Short recent history per machine (keyed by sandboxId) — drives card sparklines. */
18
20
  seriesByMachine?: Record<string, MetricSample[]> | undefined;
19
21
  /** Open the per-machine telemetry detail (makes each card actionable). */
@@ -116,6 +118,8 @@ export function MachinesDashboard({
116
118
  error,
117
119
  onAttach,
118
120
  attachingSandboxId,
121
+ onUpdateAgent,
122
+ updatingEnrollmentId,
119
123
  seriesByMachine,
120
124
  onOpenDetail,
121
125
  now,
@@ -172,6 +176,8 @@ export function MachinesDashboard({
172
176
  }}
173
177
  onAttach={onAttach}
174
178
  attaching={attachingSandboxId === machine.sandboxId}
179
+ onUpdateAgent={onUpdateAgent}
180
+ updatingAgent={updatingEnrollmentId === machine.enrollmentId}
175
181
  series={seriesByMachine?.[machine.sandboxId]}
176
182
  onOpenDetail={onOpenDetail}
177
183
  now={now}