@athenaintel/react 0.38.0 → 0.39.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 (70) hide show
  1. package/README.md +18 -2
  2. package/dist/agent-runtime/src/messages.cjs +27 -1
  3. package/dist/agent-runtime/src/messages.cjs.map +1 -1
  4. package/dist/agent-runtime/src/messages.js +27 -1
  5. package/dist/agent-runtime/src/messages.js.map +1 -1
  6. package/dist/agent-runtime/src/queue.cjs +4 -0
  7. package/dist/agent-runtime/src/queue.cjs.map +1 -1
  8. package/dist/agent-runtime/src/queue.js +4 -0
  9. package/dist/agent-runtime/src/queue.js.map +1 -1
  10. package/dist/agent-runtime/src/use-deep-agent-config.cjs +2 -1
  11. package/dist/agent-runtime/src/use-deep-agent-config.cjs.map +1 -1
  12. package/dist/agent-runtime/src/use-deep-agent-config.js +2 -1
  13. package/dist/agent-runtime/src/use-deep-agent-config.js.map +1 -1
  14. package/dist/chat/AthenaChat.cjs +1 -1
  15. package/dist/chat/AthenaChat.cjs.map +1 -1
  16. package/dist/chat/AthenaChat.js +1 -1
  17. package/dist/chat/AthenaChat.js.map +1 -1
  18. package/dist/chat/StatewireQueuedMessages.cjs +18 -0
  19. package/dist/chat/StatewireQueuedMessages.cjs.map +1 -1
  20. package/dist/chat/StatewireQueuedMessages.d.ts +19 -1
  21. package/dist/chat/StatewireQueuedMessages.js +20 -2
  22. package/dist/chat/StatewireQueuedMessages.js.map +1 -1
  23. package/dist/chat-ui/thread/statewire-history-edge.cjs +13 -0
  24. package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
  25. package/dist/chat-ui/thread/statewire-history-edge.js +13 -0
  26. package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
  27. package/dist/diagnostics/errors.cjs +5 -0
  28. package/dist/diagnostics/errors.cjs.map +1 -1
  29. package/dist/diagnostics/errors.d.ts +5 -0
  30. package/dist/diagnostics/errors.js +5 -0
  31. package/dist/diagnostics/errors.js.map +1 -1
  32. package/dist/index.cjs +14 -0
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.ts +6 -0
  35. package/dist/index.js +14 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/provider/AthenaProvider.cjs +5 -2
  38. package/dist/provider/AthenaProvider.cjs.map +1 -1
  39. package/dist/provider/AthenaProvider.js +5 -2
  40. package/dist/provider/AthenaProvider.js.map +1 -1
  41. package/dist/provider/statewire-history-context.cjs +48 -0
  42. package/dist/provider/statewire-history-context.cjs.map +1 -0
  43. package/dist/provider/statewire-history-context.d.ts +36 -0
  44. package/dist/provider/statewire-history-context.js +48 -0
  45. package/dist/provider/statewire-history-context.js.map +1 -0
  46. package/dist/react/package.json.cjs +1 -1
  47. package/dist/react/package.json.js +1 -1
  48. package/dist/styles.css +1 -1
  49. package/dist/workbench/ComputerLiveDesktop.cjs +287 -0
  50. package/dist/workbench/ComputerLiveDesktop.cjs.map +1 -0
  51. package/dist/workbench/ComputerLiveDesktop.d.ts +27 -0
  52. package/dist/workbench/ComputerLiveDesktop.js +287 -0
  53. package/dist/workbench/ComputerLiveDesktop.js.map +1 -0
  54. package/dist/workbench/SessionWorkbench.cjs +2 -2
  55. package/dist/workbench/SessionWorkbench.cjs.map +1 -1
  56. package/dist/workbench/SessionWorkbench.js +2 -2
  57. package/dist/workbench/SessionWorkbench.js.map +1 -1
  58. package/dist/workbench/live-desktop-api.cjs +139 -0
  59. package/dist/workbench/live-desktop-api.cjs.map +1 -0
  60. package/dist/workbench/live-desktop-api.d.ts +69 -0
  61. package/dist/workbench/live-desktop-api.js +139 -0
  62. package/dist/workbench/live-desktop-api.js.map +1 -0
  63. package/dist/workbench/session-workbench-context.cjs.map +1 -1
  64. package/dist/workbench/session-workbench-context.d.ts +7 -0
  65. package/dist/workbench/session-workbench-context.js.map +1 -1
  66. package/dist/workbench/session-workbench-panes.cjs +38 -2
  67. package/dist/workbench/session-workbench-panes.cjs.map +1 -1
  68. package/dist/workbench/session-workbench-panes.js +38 -2
  69. package/dist/workbench/session-workbench-panes.js.map +1 -1
  70. package/package.json +1 -1
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const reactQuery = require("@tanstack/react-query");
5
+ const lucideReact = require("lucide-react");
6
+ const react = require("react");
7
+ const useAthenaRestQuery = require("../lib/use-athena-rest-query.cjs");
8
+ const AthenaContext = require("../provider/AthenaContext.cjs");
9
+ const liveDesktopApi = require("./live-desktop-api.cjs");
10
+ const sessionWorkbenchContext = require("./session-workbench-context.cjs");
11
+ const useSessionWorkspace = require("./use-session-workspace.cjs");
12
+ const cnfast = require("cnfast");
13
+ const LIVE_DESKTOP_READY_TIMEOUT_MS = 3e4;
14
+ const RUNNING_STATES = /* @__PURE__ */ new Set(["started", "running", "ready"]);
15
+ const BUTTON_CLASS = "inline-flex shrink-0 items-center gap-1 rounded-md border border-border bg-background px-2 py-1 font-medium text-xs transition-colors hover:bg-muted outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:opacity-60";
16
+ const Placeholder = ({
17
+ icon,
18
+ children,
19
+ action
20
+ }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-2 p-4 text-center", children: [
21
+ icon,
22
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-72 text-muted-foreground text-xs leading-snug", children }),
23
+ action
24
+ ] });
25
+ const ComputerLiveDesktop = (props) => /* @__PURE__ */ jsxRuntime.jsx(ComputerLiveDesktopView, { ...props }, props.assetId);
26
+ const ComputerLiveDesktopView = ({
27
+ assetId,
28
+ className,
29
+ readyTimeoutMs = LIVE_DESKTOP_READY_TIMEOUT_MS
30
+ }) => {
31
+ var _a, _b, _c;
32
+ const config = AthenaContext.useAthenaConfig();
33
+ const { backendUrl, apiKey, getToken } = config;
34
+ const queryClient = useAthenaRestQuery.useAthenaQueryClient();
35
+ const status = useSessionWorkspace.useAthenaComputerStatus({ assetId });
36
+ const state = ((_a = status.data) == null ? void 0 : _a.state.toLowerCase()) ?? null;
37
+ const running = state !== null && RUNNING_STATES.has(state) && ((_b = status.data) == null ? void 0 : _b.explicitStop) !== "stopping";
38
+ const visible = ((_c = react.useContext(sessionWorkbenchContext.SessionWorkbenchContext)) == null ? void 0 : _c.isVisible) ?? true;
39
+ const [viewer, setViewer] = react.useState(null);
40
+ const [notice, setNotice] = react.useState(null);
41
+ const [switchError, setSwitchError] = react.useState(null);
42
+ const frameRef = react.useRef(null);
43
+ const viewerRef = react.useRef(null);
44
+ viewerRef.current = viewer;
45
+ const session = reactQuery.useMutation(
46
+ {
47
+ mutationFn: ({ mode, start }) => liveDesktopApi.createComputerLiveDesktopSession({
48
+ backendUrl,
49
+ // `config.token` is read at request time: the provider serves the
50
+ // latest token through a getter.
51
+ auth: { apiKey, token: config.token, getToken },
52
+ assetId,
53
+ mode,
54
+ start,
55
+ embedOrigin: typeof window === "undefined" ? null : window.location.origin
56
+ }),
57
+ onSuccess: (answer) => {
58
+ setSwitchError(null);
59
+ if (answer.status !== "ready") {
60
+ setViewer(null);
61
+ setNotice({ kind: answer.status, text: answer.detail });
62
+ return;
63
+ }
64
+ let url;
65
+ try {
66
+ url = liveDesktopApi.liveDesktopViewerUrl({ backendUrl, viewerPath: answer.viewerPath });
67
+ } catch {
68
+ setViewer(null);
69
+ setNotice({
70
+ kind: "failed",
71
+ text: "The computer’s desktop answered with a page this app will not open."
72
+ });
73
+ return;
74
+ }
75
+ setViewer({ url, mode: answer.mode, display: answer.display, state: "connecting" });
76
+ setNotice(null);
77
+ },
78
+ onError: (error) => {
79
+ if (viewerRef.current) {
80
+ setSwitchError(error.message);
81
+ return;
82
+ }
83
+ setNotice({ kind: "failed", text: error.message });
84
+ }
85
+ },
86
+ queryClient
87
+ );
88
+ const { mutate, reset } = session;
89
+ const opened = react.useRef(false);
90
+ const disconnectedByPerson = react.useRef(false);
91
+ react.useEffect(() => {
92
+ if (running && visible) return;
93
+ reset();
94
+ setViewer(null);
95
+ setSwitchError(null);
96
+ opened.current = false;
97
+ }, [running, visible, reset]);
98
+ react.useEffect(() => {
99
+ if (!running || !visible || opened.current || disconnectedByPerson.current) return;
100
+ opened.current = true;
101
+ mutate({ mode: liveDesktopApi.LiveDesktopMode.WATCH, start: false });
102
+ }, [running, visible, mutate]);
103
+ const viewerUrl = (viewer == null ? void 0 : viewer.url) ?? null;
104
+ const viewerState = (viewer == null ? void 0 : viewer.state) ?? null;
105
+ react.useEffect(() => {
106
+ if (!viewerUrl) return void 0;
107
+ const origin = new URL(viewerUrl).origin;
108
+ const end = (text) => {
109
+ setViewer((current) => (current == null ? void 0 : current.url) === viewerUrl ? null : current);
110
+ setNotice({ kind: "ended", text });
111
+ };
112
+ const onMessage = (event) => {
113
+ var _a2;
114
+ if (event.origin !== origin || event.source !== ((_a2 = frameRef.current) == null ? void 0 : _a2.contentWindow)) return;
115
+ const message = liveDesktopApi.readLiveDesktopViewerMessage(event.data);
116
+ if (!message) return;
117
+ if (message.status === "ready") {
118
+ setViewer(
119
+ (current) => (current == null ? void 0 : current.url) === viewerUrl ? { ...current, state: "connected" } : current
120
+ );
121
+ } else if (message.status === "error") {
122
+ end(message.detail);
123
+ } else if (message.status === "closed") {
124
+ const final = liveDesktopApi.finalLiveDesktopCloseNotice(message.code);
125
+ if (final) end(final);
126
+ }
127
+ };
128
+ window.addEventListener("message", onMessage);
129
+ return () => window.removeEventListener("message", onMessage);
130
+ }, [viewerUrl]);
131
+ react.useEffect(() => {
132
+ if (!viewerUrl || viewerState !== "connecting") return void 0;
133
+ const timer = setTimeout(() => {
134
+ setViewer((current) => (current == null ? void 0 : current.url) === viewerUrl ? null : current);
135
+ setNotice({
136
+ kind: "ended",
137
+ text: "The desktop did not start streaming here. Reconnect to try again."
138
+ });
139
+ }, readyTimeoutMs);
140
+ return () => clearTimeout(timer);
141
+ }, [viewerUrl, viewerState, readyTimeoutMs]);
142
+ const watch = (start = false) => {
143
+ disconnectedByPerson.current = false;
144
+ opened.current = true;
145
+ mutate({ mode: liveDesktopApi.LiveDesktopMode.WATCH, start });
146
+ };
147
+ const control = () => mutate({ mode: liveDesktopApi.LiveDesktopMode.CONTROL, start: false });
148
+ const disconnect = () => {
149
+ disconnectedByPerson.current = true;
150
+ reset();
151
+ setViewer(null);
152
+ setNotice(null);
153
+ setSwitchError(null);
154
+ };
155
+ const aspect = (viewer == null ? void 0 : viewer.display) ? `${viewer.display.width} / ${viewer.display.height}` : "16 / 10";
156
+ let body;
157
+ if (!running) {
158
+ body = /* @__PURE__ */ jsxRuntime.jsx(Placeholder, { icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorOffIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }), children: status.isPending ? "Checking the computer…" : "The computer is not running. Its desktop shows here once it is." });
159
+ } else if (viewer) {
160
+ body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
161
+ /* @__PURE__ */ jsxRuntime.jsx(
162
+ "iframe",
163
+ {
164
+ ref: frameRef,
165
+ src: viewer.url,
166
+ title: "Live desktop",
167
+ className: "h-full w-full border-0 bg-black",
168
+ sandbox: "allow-scripts allow-same-origin allow-pointer-lock",
169
+ allow: "fullscreen; keyboard-map; autoplay; clipboard-write",
170
+ referrerPolicy: "no-referrer",
171
+ "data-live-desktop-frame": ""
172
+ },
173
+ viewer.url
174
+ ),
175
+ viewer.state === "connecting" || session.isPending ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/40", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LoaderIcon, { className: "size-5 animate-spin text-white", "aria-label": "Connecting" }) }) : null,
176
+ switchError ? /* @__PURE__ */ jsxRuntime.jsxs(
177
+ "div",
178
+ {
179
+ role: "alert",
180
+ className: "absolute inset-x-2 bottom-2 flex items-start gap-2 rounded-md border border-red-200 bg-red-50 px-2 py-1.5 text-red-900 text-xs",
181
+ children: [
182
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": true }),
183
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1", children: switchError }),
184
+ /* @__PURE__ */ jsxRuntime.jsx(
185
+ "button",
186
+ {
187
+ type: "button",
188
+ onClick: () => setSwitchError(null),
189
+ "aria-label": "Dismiss",
190
+ className: "shrink-0 rounded p-0.5 hover:bg-red-100",
191
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XIcon, { className: "size-3", "aria-hidden": true })
192
+ }
193
+ )
194
+ ]
195
+ }
196
+ ) : null
197
+ ] });
198
+ } else if (session.isPending) {
199
+ body = /* @__PURE__ */ jsxRuntime.jsx(Placeholder, { icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LoaderIcon, { className: "size-5 animate-spin text-muted-foreground", "aria-hidden": true }), children: "Connecting to the desktop…" });
200
+ } else if ((notice == null ? void 0 : notice.kind) === "display_not_running") {
201
+ body = /* @__PURE__ */ jsxRuntime.jsx(
202
+ Placeholder,
203
+ {
204
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorOffIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }),
205
+ action: /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => watch(true), className: BUTTON_CLASS, children: [
206
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PlayIcon, { className: "size-3.5", "aria-hidden": true }),
207
+ "Start the display"
208
+ ] }),
209
+ children: notice.text
210
+ }
211
+ );
212
+ } else if ((notice == null ? void 0 : notice.kind) === "unsupported") {
213
+ body = /* @__PURE__ */ jsxRuntime.jsx(Placeholder, { icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorOffIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }), children: notice.text });
214
+ } else if (notice) {
215
+ body = /* @__PURE__ */ jsxRuntime.jsx(
216
+ Placeholder,
217
+ {
218
+ icon: notice.kind === "failed" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "size-5 text-red-600", "aria-hidden": true }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorOffIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }),
219
+ action: /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => watch(), className: BUTTON_CLASS, children: [
220
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCwIcon, { className: "size-3.5", "aria-hidden": true }),
221
+ notice.kind === "failed" ? "Try again" : "Reconnect"
222
+ ] }),
223
+ children: notice.text
224
+ }
225
+ );
226
+ } else {
227
+ body = /* @__PURE__ */ jsxRuntime.jsx(
228
+ Placeholder,
229
+ {
230
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }),
231
+ action: /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => watch(), className: BUTTON_CLASS, children: [
232
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeIcon, { className: "size-3.5", "aria-hidden": true }),
233
+ "Watch live"
234
+ ] }),
235
+ children: "Watch the computer’s screen as the agent works."
236
+ }
237
+ );
238
+ }
239
+ const liveMode = running && viewer ? viewer.mode : null;
240
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Live desktop", className: cnfast.cn("flex flex-col gap-1.5", className), children: [
241
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
242
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-medium text-[11px] text-muted-foreground uppercase tracking-wide", children: "Live desktop" }),
243
+ liveMode ? /* @__PURE__ */ jsxRuntime.jsx(
244
+ "span",
245
+ {
246
+ "data-mode": liveMode,
247
+ className: cnfast.cn(
248
+ "rounded-full px-1.5 py-px text-[10px]",
249
+ liveMode === liveDesktopApi.LiveDesktopMode.CONTROL ? "bg-amber-100 text-amber-900" : "bg-muted text-muted-foreground"
250
+ ),
251
+ children: liveMode === liveDesktopApi.LiveDesktopMode.CONTROL ? "In control" : "Watching"
252
+ }
253
+ ) : null,
254
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1" }),
255
+ liveMode === liveDesktopApi.LiveDesktopMode.WATCH ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: control, disabled: session.isPending, className: BUTTON_CLASS, children: [
256
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MousePointer2Icon, { className: "size-3.5", "aria-hidden": true }),
257
+ "Take control"
258
+ ] }) : null,
259
+ liveMode === liveDesktopApi.LiveDesktopMode.CONTROL ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => watch(), disabled: session.isPending, className: BUTTON_CLASS, children: [
260
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeIcon, { className: "size-3.5", "aria-hidden": true }),
261
+ "Stop controlling"
262
+ ] }) : null,
263
+ liveMode ? /* @__PURE__ */ jsxRuntime.jsx(
264
+ "button",
265
+ {
266
+ type: "button",
267
+ onClick: disconnect,
268
+ "aria-label": "Disconnect from the desktop",
269
+ title: "Disconnect",
270
+ className: cnfast.cn(BUTTON_CLASS, "px-1.5"),
271
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XIcon, { className: "size-3.5", "aria-hidden": true })
272
+ }
273
+ ) : null
274
+ ] }),
275
+ /* @__PURE__ */ jsxRuntime.jsx(
276
+ "div",
277
+ {
278
+ className: "relative w-full overflow-hidden rounded-lg border border-border bg-muted/40",
279
+ style: { aspectRatio: aspect },
280
+ children: body
281
+ }
282
+ )
283
+ ] });
284
+ };
285
+ exports.ComputerLiveDesktop = ComputerLiveDesktop;
286
+ exports.LIVE_DESKTOP_READY_TIMEOUT_MS = LIVE_DESKTOP_READY_TIMEOUT_MS;
287
+ //# sourceMappingURL=ComputerLiveDesktop.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComputerLiveDesktop.cjs","sources":["../../src/workbench/ComputerLiveDesktop.tsx"],"sourcesContent":["\"use client\";\n\nimport { useMutation } from '@tanstack/react-query';\nimport {\n AlertCircleIcon,\n EyeIcon,\n LoaderIcon,\n MonitorOffIcon,\n MousePointer2Icon,\n PlayIcon,\n RefreshCwIcon,\n XIcon,\n} from 'lucide-react';\nimport { type FC, type ReactNode, useContext, useEffect, useRef, useState } from 'react';\nimport { useAthenaQueryClient } from '../lib/use-athena-rest-query';\nimport { cn } from '../lib/utils';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n createComputerLiveDesktopSession,\n finalLiveDesktopCloseNotice,\n LiveDesktopMode,\n type LiveDesktopSession,\n liveDesktopViewerUrl,\n readLiveDesktopViewerMessage,\n} from './live-desktop-api';\nimport { SessionWorkbenchContext } from './session-workbench-context';\nimport { useAthenaComputerStatus } from './use-session-workspace';\n\n/** How long a viewer may take to show video before the pane calls it failed. */\nexport const LIVE_DESKTOP_READY_TIMEOUT_MS = 30_000;\n\nconst RUNNING_STATES: ReadonlySet<string> = new Set(['started', 'running', 'ready']);\n\nconst BUTTON_CLASS =\n 'inline-flex shrink-0 items-center gap-1 rounded-md border border-border bg-background px-2 py-1 font-medium text-xs transition-colors hover:bg-muted outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:opacity-60';\n\n/** The viewer page the pane frames: one per ticket, its own messages move it on. */\ninterface MountedViewer {\n url: string;\n mode: LiveDesktopMode;\n display: { width: number; height: number } | null;\n /** `connected` once the page reports video (`ready`); never on a socket alone. */\n state: 'connecting' | 'connected';\n}\n\n/** Why the pane shows no viewer, when it has something to say. */\ntype Notice =\n | { kind: 'display_not_running'; text: string }\n | { kind: 'unsupported'; text: string }\n | { kind: 'ended'; text: string }\n | { kind: 'failed'; text: string };\n\ninterface SessionRequest {\n mode: LiveDesktopMode;\n start: boolean;\n}\n\nconst Placeholder: FC<{ icon?: ReactNode; children: ReactNode; action?: ReactNode }> = ({\n icon,\n children,\n action,\n}) => (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-2 p-4 text-center\">\n {icon}\n <p className=\"max-w-72 text-muted-foreground text-xs leading-snug\">{children}</p>\n {action}\n </div>\n);\n\nexport interface ComputerLiveDesktopProps {\n assetId: string;\n className?: string;\n /** How long a viewer may take to show video ({@link LIVE_DESKTOP_READY_TIMEOUT_MS}). */\n readyTimeoutMs?: number;\n}\n\n/**\n * The computer's screen, live — watch it, or take control and use it — framed\n * from agora's Selkies viewer (agora `web/api/computer/live_desktop_viewer.py`).\n *\n * Opens watching as soon as it mounts on a running computer, and ends when it\n * unmounts, its workbench collapses to the rail, or the computer stops (an open\n * view keeps the computer awake, up to agora's 30-minute cap). Needs EDIT on\n * the computer, in its own workspace, and an app the computer serves on Athena\n * (agora binds the viewer to this page's origin). A computer without a display\n * (one not made from the Browser template) says so instead.\n *\n * The frame runs Selkies' page from the guest, sandboxed as Olympus frames it:\n * scripts on agora's origin, no popups, forms or top navigation. Its messages\n * only move this pane between connecting, connected and ended. Each computer\n * is its own instance (keyed by `assetId`), so a switch never shows or drives\n * the previous computer's screen.\n */\nexport const ComputerLiveDesktop: FC<ComputerLiveDesktopProps> = (props) => (\n <ComputerLiveDesktopView key={props.assetId} {...props} />\n);\n\nconst ComputerLiveDesktopView: FC<ComputerLiveDesktopProps> = ({\n assetId,\n className,\n readyTimeoutMs = LIVE_DESKTOP_READY_TIMEOUT_MS,\n}) => {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const queryClient = useAthenaQueryClient();\n const status = useAthenaComputerStatus({ assetId });\n const state = status.data?.state.toLowerCase() ?? null;\n const running =\n state !== null && RUNNING_STATES.has(state) && status.data?.explicitStop !== 'stopping';\n // Hidden in a collapsed workbench, the pane stops its stream.\n const visible = useContext(SessionWorkbenchContext)?.isVisible ?? true;\n\n const [viewer, setViewer] = useState<MountedViewer | null>(null);\n const [notice, setNotice] = useState<Notice | null>(null);\n // A mode switch that failed while a viewer stayed on screen.\n const [switchError, setSwitchError] = useState<string | null>(null);\n const frameRef = useRef<HTMLIFrameElement>(null);\n const viewerRef = useRef<MountedViewer | null>(null);\n viewerRef.current = viewer;\n\n const session = useMutation<LiveDesktopSession, Error, SessionRequest>(\n {\n mutationFn: ({ mode, start }) =>\n createComputerLiveDesktopSession({\n backendUrl,\n // `config.token` is read at request time: the provider serves the\n // latest token through a getter.\n auth: { apiKey, token: config.token, getToken },\n assetId,\n mode,\n start,\n embedOrigin: typeof window === 'undefined' ? null : window.location.origin,\n }),\n onSuccess: (answer) => {\n setSwitchError(null);\n if (answer.status !== 'ready') {\n setViewer(null);\n setNotice({ kind: answer.status, text: answer.detail });\n return;\n }\n let url: string;\n try {\n url = liveDesktopViewerUrl({ backendUrl, viewerPath: answer.viewerPath });\n } catch {\n setViewer(null);\n setNotice({\n kind: 'failed',\n text: 'The computer’s desktop answered with a page this app will not open.',\n });\n return;\n }\n // The new page replaces the one on screen only now that it exists.\n setViewer({ url, mode: answer.mode, display: answer.display, state: 'connecting' });\n setNotice(null);\n },\n onError: (error) => {\n // A failed switch leaves the working viewer where it is.\n if (viewerRef.current) {\n setSwitchError(error.message);\n return;\n }\n setNotice({ kind: 'failed', text: error.message });\n },\n },\n queryClient,\n );\n const { mutate, reset } = session;\n\n const opened = useRef(false);\n const disconnectedByPerson = useRef(false);\n\n // Hidden, or the computer stopped: drop the view. Its ticket is spent, so\n // showing again (or the computer starting again) asks for a fresh one.\n useEffect(() => {\n if (running && visible) return;\n reset();\n setViewer(null);\n setSwitchError(null);\n opened.current = false;\n }, [running, visible, reset]);\n // Watch once the computer is known to be running; never wake one.\n useEffect(() => {\n if (!running || !visible || opened.current || disconnectedByPerson.current) return;\n opened.current = true;\n mutate({ mode: LiveDesktopMode.WATCH, start: false });\n }, [running, visible, mutate]);\n\n // The viewer page's own messages move it on; a page agora ended takes the\n // frame down (an unseen stream would keep the computer awake).\n const viewerUrl = viewer?.url ?? null;\n const viewerState = viewer?.state ?? null;\n useEffect(() => {\n if (!viewerUrl) return undefined;\n const origin = new URL(viewerUrl).origin;\n const end = (text: string) => {\n setViewer((current) => (current?.url === viewerUrl ? null : current));\n setNotice({ kind: 'ended', text });\n };\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== origin || event.source !== frameRef.current?.contentWindow) return;\n const message = readLiveDesktopViewerMessage(event.data);\n if (!message) return;\n if (message.status === 'ready') {\n setViewer((current) =>\n current?.url === viewerUrl ? { ...current, state: 'connected' } : current,\n );\n } else if (message.status === 'error') {\n end(message.detail);\n } else if (message.status === 'closed') {\n const final = finalLiveDesktopCloseNotice(message.code);\n if (final) end(final);\n }\n };\n window.addEventListener('message', onMessage);\n return () => window.removeEventListener('message', onMessage);\n }, [viewerUrl]);\n // No video within the timeout takes the frame down too.\n useEffect(() => {\n if (!viewerUrl || viewerState !== 'connecting') return undefined;\n const timer = setTimeout(() => {\n setViewer((current) => (current?.url === viewerUrl ? null : current));\n setNotice({\n kind: 'ended',\n text: 'The desktop did not start streaming here. Reconnect to try again.',\n });\n }, readyTimeoutMs);\n return () => clearTimeout(timer);\n }, [viewerUrl, viewerState, readyTimeoutMs]);\n\n const watch = (start = false) => {\n disconnectedByPerson.current = false;\n opened.current = true;\n mutate({ mode: LiveDesktopMode.WATCH, start });\n };\n const control = () => mutate({ mode: LiveDesktopMode.CONTROL, start: false });\n const disconnect = () => {\n disconnectedByPerson.current = true;\n reset();\n setViewer(null);\n setNotice(null);\n setSwitchError(null);\n };\n\n const aspect = viewer?.display ? `${viewer.display.width} / ${viewer.display.height}` : '16 / 10';\n let body: ReactNode;\n if (!running) {\n body = (\n <Placeholder icon={<MonitorOffIcon className=\"size-5 text-muted-foreground/70\" aria-hidden />}>\n {status.isPending\n ? 'Checking the computer…'\n : 'The computer is not running. Its desktop shows here once it is.'}\n </Placeholder>\n );\n } else if (viewer) {\n body = (\n <>\n <iframe\n ref={frameRef}\n key={viewer.url}\n src={viewer.url}\n title=\"Live desktop\"\n className=\"h-full w-full border-0 bg-black\"\n sandbox=\"allow-scripts allow-same-origin allow-pointer-lock\"\n allow=\"fullscreen; keyboard-map; autoplay; clipboard-write\"\n referrerPolicy=\"no-referrer\"\n data-live-desktop-frame=\"\"\n />\n {viewer.state === 'connecting' || session.isPending ? (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center bg-black/40\">\n <LoaderIcon className=\"size-5 animate-spin text-white\" aria-label=\"Connecting\" />\n </div>\n ) : null}\n {switchError ? (\n <div\n role=\"alert\"\n className=\"absolute inset-x-2 bottom-2 flex items-start gap-2 rounded-md border border-red-200 bg-red-50 px-2 py-1.5 text-red-900 text-xs\"\n >\n <AlertCircleIcon className=\"mt-0.5 size-3.5 shrink-0\" aria-hidden />\n <span className=\"min-w-0 flex-1\">{switchError}</span>\n <button\n type=\"button\"\n onClick={() => setSwitchError(null)}\n aria-label=\"Dismiss\"\n className=\"shrink-0 rounded p-0.5 hover:bg-red-100\"\n >\n <XIcon className=\"size-3\" aria-hidden />\n </button>\n </div>\n ) : null}\n </>\n );\n } else if (session.isPending) {\n body = (\n <Placeholder icon={<LoaderIcon className=\"size-5 animate-spin text-muted-foreground\" aria-hidden />}>\n Connecting to the desktop…\n </Placeholder>\n );\n } else if (notice?.kind === 'display_not_running') {\n body = (\n <Placeholder\n icon={<MonitorOffIcon className=\"size-5 text-muted-foreground/70\" aria-hidden />}\n action={\n <button type=\"button\" onClick={() => watch(true)} className={BUTTON_CLASS}>\n <PlayIcon className=\"size-3.5\" aria-hidden />\n Start the display\n </button>\n }\n >\n {notice.text}\n </Placeholder>\n );\n } else if (notice?.kind === 'unsupported') {\n body = (\n <Placeholder icon={<MonitorOffIcon className=\"size-5 text-muted-foreground/70\" aria-hidden />}>\n {notice.text}\n </Placeholder>\n );\n } else if (notice) {\n body = (\n <Placeholder\n icon={\n notice.kind === 'failed' ? (\n <AlertCircleIcon className=\"size-5 text-red-600\" aria-hidden />\n ) : (\n <MonitorOffIcon className=\"size-5 text-muted-foreground/70\" aria-hidden />\n )\n }\n action={\n <button type=\"button\" onClick={() => watch()} className={BUTTON_CLASS}>\n <RefreshCwIcon className=\"size-3.5\" aria-hidden />\n {notice.kind === 'failed' ? 'Try again' : 'Reconnect'}\n </button>\n }\n >\n {notice.text}\n </Placeholder>\n );\n } else {\n body = (\n <Placeholder\n icon={<EyeIcon className=\"size-5 text-muted-foreground/70\" aria-hidden />}\n action={\n <button type=\"button\" onClick={() => watch()} className={BUTTON_CLASS}>\n <EyeIcon className=\"size-3.5\" aria-hidden />\n Watch live\n </button>\n }\n >\n Watch the computer’s screen as the agent works.\n </Placeholder>\n );\n }\n\n const liveMode = running && viewer ? viewer.mode : null;\n return (\n <section aria-label=\"Live desktop\" className={cn('flex flex-col gap-1.5', className)}>\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-medium text-[11px] text-muted-foreground uppercase tracking-wide\">\n Live desktop\n </h3>\n {liveMode ? (\n <span\n data-mode={liveMode}\n className={cn(\n 'rounded-full px-1.5 py-px text-[10px]',\n liveMode === LiveDesktopMode.CONTROL\n ? 'bg-amber-100 text-amber-900'\n : 'bg-muted text-muted-foreground',\n )}\n >\n {liveMode === LiveDesktopMode.CONTROL ? 'In control' : 'Watching'}\n </span>\n ) : null}\n <div className=\"min-w-0 flex-1\" />\n {liveMode === LiveDesktopMode.WATCH ? (\n <button type=\"button\" onClick={control} disabled={session.isPending} className={BUTTON_CLASS}>\n <MousePointer2Icon className=\"size-3.5\" aria-hidden />\n Take control\n </button>\n ) : null}\n {liveMode === LiveDesktopMode.CONTROL ? (\n <button type=\"button\" onClick={() => watch()} disabled={session.isPending} className={BUTTON_CLASS}>\n <EyeIcon className=\"size-3.5\" aria-hidden />\n Stop controlling\n </button>\n ) : null}\n {liveMode ? (\n <button\n type=\"button\"\n onClick={disconnect}\n aria-label=\"Disconnect from the desktop\"\n title=\"Disconnect\"\n className={cn(BUTTON_CLASS, 'px-1.5')}\n >\n <XIcon className=\"size-3.5\" aria-hidden />\n </button>\n ) : null}\n </div>\n {/* The remote screen's aspect is a runtime value no static utility expresses. */}\n <div\n className=\"relative w-full overflow-hidden rounded-lg border border-border bg-muted/40\"\n style={{ aspectRatio: aspect }}\n >\n {body}\n </div>\n </section>\n );\n};\n"],"names":["jsxs","jsx","useAthenaConfig","useAthenaQueryClient","useAthenaComputerStatus","useContext","SessionWorkbenchContext","useState","useRef","useMutation","createComputerLiveDesktopSession","liveDesktopViewerUrl","useEffect","LiveDesktopMode","_a","readLiveDesktopViewerMessage","finalLiveDesktopCloseNotice","MonitorOffIcon","Fragment","LoaderIcon","AlertCircleIcon","XIcon","PlayIcon","RefreshCwIcon","EyeIcon","cn","MousePointer2Icon"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { type FC } from 'react';
2
+ /** How long a viewer may take to show video before the pane calls it failed. */
3
+ export declare const LIVE_DESKTOP_READY_TIMEOUT_MS = 30000;
4
+ export interface ComputerLiveDesktopProps {
5
+ assetId: string;
6
+ className?: string;
7
+ /** How long a viewer may take to show video ({@link LIVE_DESKTOP_READY_TIMEOUT_MS}). */
8
+ readyTimeoutMs?: number;
9
+ }
10
+ /**
11
+ * The computer's screen, live — watch it, or take control and use it — framed
12
+ * from agora's Selkies viewer (agora `web/api/computer/live_desktop_viewer.py`).
13
+ *
14
+ * Opens watching as soon as it mounts on a running computer, and ends when it
15
+ * unmounts, its workbench collapses to the rail, or the computer stops (an open
16
+ * view keeps the computer awake, up to agora's 30-minute cap). Needs EDIT on
17
+ * the computer, in its own workspace, and an app the computer serves on Athena
18
+ * (agora binds the viewer to this page's origin). A computer without a display
19
+ * (one not made from the Browser template) says so instead.
20
+ *
21
+ * The frame runs Selkies' page from the guest, sandboxed as Olympus frames it:
22
+ * scripts on agora's origin, no popups, forms or top navigation. Its messages
23
+ * only move this pane between connecting, connected and ended. Each computer
24
+ * is its own instance (keyed by `assetId`), so a switch never shows or drives
25
+ * the previous computer's screen.
26
+ */
27
+ export declare const ComputerLiveDesktop: FC<ComputerLiveDesktopProps>;
@@ -0,0 +1,287 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { useMutation } from "@tanstack/react-query";
3
+ import { MonitorOffIcon, LoaderIcon, AlertCircleIcon, XIcon, PlayIcon, RefreshCwIcon, MousePointer2Icon, EyeIcon } from "lucide-react";
4
+ import { useContext, useState, useRef, useEffect } from "react";
5
+ import { useAthenaQueryClient } from "../lib/use-athena-rest-query.js";
6
+ import { useAthenaConfig } from "../provider/AthenaContext.js";
7
+ import { liveDesktopViewerUrl, createComputerLiveDesktopSession, LiveDesktopMode, readLiveDesktopViewerMessage, finalLiveDesktopCloseNotice } from "./live-desktop-api.js";
8
+ import { SessionWorkbenchContext } from "./session-workbench-context.js";
9
+ import { useAthenaComputerStatus } from "./use-session-workspace.js";
10
+ import { cn } from "cnfast";
11
+ const LIVE_DESKTOP_READY_TIMEOUT_MS = 3e4;
12
+ const RUNNING_STATES = /* @__PURE__ */ new Set(["started", "running", "ready"]);
13
+ const BUTTON_CLASS = "inline-flex shrink-0 items-center gap-1 rounded-md border border-border bg-background px-2 py-1 font-medium text-xs transition-colors hover:bg-muted outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:opacity-60";
14
+ const Placeholder = ({
15
+ icon,
16
+ children,
17
+ action
18
+ }) => /* @__PURE__ */ jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-2 p-4 text-center", children: [
19
+ icon,
20
+ /* @__PURE__ */ jsx("p", { className: "max-w-72 text-muted-foreground text-xs leading-snug", children }),
21
+ action
22
+ ] });
23
+ const ComputerLiveDesktop = (props) => /* @__PURE__ */ jsx(ComputerLiveDesktopView, { ...props }, props.assetId);
24
+ const ComputerLiveDesktopView = ({
25
+ assetId,
26
+ className,
27
+ readyTimeoutMs = LIVE_DESKTOP_READY_TIMEOUT_MS
28
+ }) => {
29
+ var _a, _b, _c;
30
+ const config = useAthenaConfig();
31
+ const { backendUrl, apiKey, getToken } = config;
32
+ const queryClient = useAthenaQueryClient();
33
+ const status = useAthenaComputerStatus({ assetId });
34
+ const state = ((_a = status.data) == null ? void 0 : _a.state.toLowerCase()) ?? null;
35
+ const running = state !== null && RUNNING_STATES.has(state) && ((_b = status.data) == null ? void 0 : _b.explicitStop) !== "stopping";
36
+ const visible = ((_c = useContext(SessionWorkbenchContext)) == null ? void 0 : _c.isVisible) ?? true;
37
+ const [viewer, setViewer] = useState(null);
38
+ const [notice, setNotice] = useState(null);
39
+ const [switchError, setSwitchError] = useState(null);
40
+ const frameRef = useRef(null);
41
+ const viewerRef = useRef(null);
42
+ viewerRef.current = viewer;
43
+ const session = useMutation(
44
+ {
45
+ mutationFn: ({ mode, start }) => createComputerLiveDesktopSession({
46
+ backendUrl,
47
+ // `config.token` is read at request time: the provider serves the
48
+ // latest token through a getter.
49
+ auth: { apiKey, token: config.token, getToken },
50
+ assetId,
51
+ mode,
52
+ start,
53
+ embedOrigin: typeof window === "undefined" ? null : window.location.origin
54
+ }),
55
+ onSuccess: (answer) => {
56
+ setSwitchError(null);
57
+ if (answer.status !== "ready") {
58
+ setViewer(null);
59
+ setNotice({ kind: answer.status, text: answer.detail });
60
+ return;
61
+ }
62
+ let url;
63
+ try {
64
+ url = liveDesktopViewerUrl({ backendUrl, viewerPath: answer.viewerPath });
65
+ } catch {
66
+ setViewer(null);
67
+ setNotice({
68
+ kind: "failed",
69
+ text: "The computer’s desktop answered with a page this app will not open."
70
+ });
71
+ return;
72
+ }
73
+ setViewer({ url, mode: answer.mode, display: answer.display, state: "connecting" });
74
+ setNotice(null);
75
+ },
76
+ onError: (error) => {
77
+ if (viewerRef.current) {
78
+ setSwitchError(error.message);
79
+ return;
80
+ }
81
+ setNotice({ kind: "failed", text: error.message });
82
+ }
83
+ },
84
+ queryClient
85
+ );
86
+ const { mutate, reset } = session;
87
+ const opened = useRef(false);
88
+ const disconnectedByPerson = useRef(false);
89
+ useEffect(() => {
90
+ if (running && visible) return;
91
+ reset();
92
+ setViewer(null);
93
+ setSwitchError(null);
94
+ opened.current = false;
95
+ }, [running, visible, reset]);
96
+ useEffect(() => {
97
+ if (!running || !visible || opened.current || disconnectedByPerson.current) return;
98
+ opened.current = true;
99
+ mutate({ mode: LiveDesktopMode.WATCH, start: false });
100
+ }, [running, visible, mutate]);
101
+ const viewerUrl = (viewer == null ? void 0 : viewer.url) ?? null;
102
+ const viewerState = (viewer == null ? void 0 : viewer.state) ?? null;
103
+ useEffect(() => {
104
+ if (!viewerUrl) return void 0;
105
+ const origin = new URL(viewerUrl).origin;
106
+ const end = (text) => {
107
+ setViewer((current) => (current == null ? void 0 : current.url) === viewerUrl ? null : current);
108
+ setNotice({ kind: "ended", text });
109
+ };
110
+ const onMessage = (event) => {
111
+ var _a2;
112
+ if (event.origin !== origin || event.source !== ((_a2 = frameRef.current) == null ? void 0 : _a2.contentWindow)) return;
113
+ const message = readLiveDesktopViewerMessage(event.data);
114
+ if (!message) return;
115
+ if (message.status === "ready") {
116
+ setViewer(
117
+ (current) => (current == null ? void 0 : current.url) === viewerUrl ? { ...current, state: "connected" } : current
118
+ );
119
+ } else if (message.status === "error") {
120
+ end(message.detail);
121
+ } else if (message.status === "closed") {
122
+ const final = finalLiveDesktopCloseNotice(message.code);
123
+ if (final) end(final);
124
+ }
125
+ };
126
+ window.addEventListener("message", onMessage);
127
+ return () => window.removeEventListener("message", onMessage);
128
+ }, [viewerUrl]);
129
+ useEffect(() => {
130
+ if (!viewerUrl || viewerState !== "connecting") return void 0;
131
+ const timer = setTimeout(() => {
132
+ setViewer((current) => (current == null ? void 0 : current.url) === viewerUrl ? null : current);
133
+ setNotice({
134
+ kind: "ended",
135
+ text: "The desktop did not start streaming here. Reconnect to try again."
136
+ });
137
+ }, readyTimeoutMs);
138
+ return () => clearTimeout(timer);
139
+ }, [viewerUrl, viewerState, readyTimeoutMs]);
140
+ const watch = (start = false) => {
141
+ disconnectedByPerson.current = false;
142
+ opened.current = true;
143
+ mutate({ mode: LiveDesktopMode.WATCH, start });
144
+ };
145
+ const control = () => mutate({ mode: LiveDesktopMode.CONTROL, start: false });
146
+ const disconnect = () => {
147
+ disconnectedByPerson.current = true;
148
+ reset();
149
+ setViewer(null);
150
+ setNotice(null);
151
+ setSwitchError(null);
152
+ };
153
+ const aspect = (viewer == null ? void 0 : viewer.display) ? `${viewer.display.width} / ${viewer.display.height}` : "16 / 10";
154
+ let body;
155
+ if (!running) {
156
+ body = /* @__PURE__ */ jsx(Placeholder, { icon: /* @__PURE__ */ jsx(MonitorOffIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }), children: status.isPending ? "Checking the computer…" : "The computer is not running. Its desktop shows here once it is." });
157
+ } else if (viewer) {
158
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
159
+ /* @__PURE__ */ jsx(
160
+ "iframe",
161
+ {
162
+ ref: frameRef,
163
+ src: viewer.url,
164
+ title: "Live desktop",
165
+ className: "h-full w-full border-0 bg-black",
166
+ sandbox: "allow-scripts allow-same-origin allow-pointer-lock",
167
+ allow: "fullscreen; keyboard-map; autoplay; clipboard-write",
168
+ referrerPolicy: "no-referrer",
169
+ "data-live-desktop-frame": ""
170
+ },
171
+ viewer.url
172
+ ),
173
+ viewer.state === "connecting" || session.isPending ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/40", children: /* @__PURE__ */ jsx(LoaderIcon, { className: "size-5 animate-spin text-white", "aria-label": "Connecting" }) }) : null,
174
+ switchError ? /* @__PURE__ */ jsxs(
175
+ "div",
176
+ {
177
+ role: "alert",
178
+ className: "absolute inset-x-2 bottom-2 flex items-start gap-2 rounded-md border border-red-200 bg-red-50 px-2 py-1.5 text-red-900 text-xs",
179
+ children: [
180
+ /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": true }),
181
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", children: switchError }),
182
+ /* @__PURE__ */ jsx(
183
+ "button",
184
+ {
185
+ type: "button",
186
+ onClick: () => setSwitchError(null),
187
+ "aria-label": "Dismiss",
188
+ className: "shrink-0 rounded p-0.5 hover:bg-red-100",
189
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-3", "aria-hidden": true })
190
+ }
191
+ )
192
+ ]
193
+ }
194
+ ) : null
195
+ ] });
196
+ } else if (session.isPending) {
197
+ body = /* @__PURE__ */ jsx(Placeholder, { icon: /* @__PURE__ */ jsx(LoaderIcon, { className: "size-5 animate-spin text-muted-foreground", "aria-hidden": true }), children: "Connecting to the desktop…" });
198
+ } else if ((notice == null ? void 0 : notice.kind) === "display_not_running") {
199
+ body = /* @__PURE__ */ jsx(
200
+ Placeholder,
201
+ {
202
+ icon: /* @__PURE__ */ jsx(MonitorOffIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }),
203
+ action: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => watch(true), className: BUTTON_CLASS, children: [
204
+ /* @__PURE__ */ jsx(PlayIcon, { className: "size-3.5", "aria-hidden": true }),
205
+ "Start the display"
206
+ ] }),
207
+ children: notice.text
208
+ }
209
+ );
210
+ } else if ((notice == null ? void 0 : notice.kind) === "unsupported") {
211
+ body = /* @__PURE__ */ jsx(Placeholder, { icon: /* @__PURE__ */ jsx(MonitorOffIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }), children: notice.text });
212
+ } else if (notice) {
213
+ body = /* @__PURE__ */ jsx(
214
+ Placeholder,
215
+ {
216
+ icon: notice.kind === "failed" ? /* @__PURE__ */ jsx(AlertCircleIcon, { className: "size-5 text-red-600", "aria-hidden": true }) : /* @__PURE__ */ jsx(MonitorOffIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }),
217
+ action: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => watch(), className: BUTTON_CLASS, children: [
218
+ /* @__PURE__ */ jsx(RefreshCwIcon, { className: "size-3.5", "aria-hidden": true }),
219
+ notice.kind === "failed" ? "Try again" : "Reconnect"
220
+ ] }),
221
+ children: notice.text
222
+ }
223
+ );
224
+ } else {
225
+ body = /* @__PURE__ */ jsx(
226
+ Placeholder,
227
+ {
228
+ icon: /* @__PURE__ */ jsx(EyeIcon, { className: "size-5 text-muted-foreground/70", "aria-hidden": true }),
229
+ action: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => watch(), className: BUTTON_CLASS, children: [
230
+ /* @__PURE__ */ jsx(EyeIcon, { className: "size-3.5", "aria-hidden": true }),
231
+ "Watch live"
232
+ ] }),
233
+ children: "Watch the computer’s screen as the agent works."
234
+ }
235
+ );
236
+ }
237
+ const liveMode = running && viewer ? viewer.mode : null;
238
+ return /* @__PURE__ */ jsxs("section", { "aria-label": "Live desktop", className: cn("flex flex-col gap-1.5", className), children: [
239
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
240
+ /* @__PURE__ */ jsx("h3", { className: "font-medium text-[11px] text-muted-foreground uppercase tracking-wide", children: "Live desktop" }),
241
+ liveMode ? /* @__PURE__ */ jsx(
242
+ "span",
243
+ {
244
+ "data-mode": liveMode,
245
+ className: cn(
246
+ "rounded-full px-1.5 py-px text-[10px]",
247
+ liveMode === LiveDesktopMode.CONTROL ? "bg-amber-100 text-amber-900" : "bg-muted text-muted-foreground"
248
+ ),
249
+ children: liveMode === LiveDesktopMode.CONTROL ? "In control" : "Watching"
250
+ }
251
+ ) : null,
252
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1" }),
253
+ liveMode === LiveDesktopMode.WATCH ? /* @__PURE__ */ jsxs("button", { type: "button", onClick: control, disabled: session.isPending, className: BUTTON_CLASS, children: [
254
+ /* @__PURE__ */ jsx(MousePointer2Icon, { className: "size-3.5", "aria-hidden": true }),
255
+ "Take control"
256
+ ] }) : null,
257
+ liveMode === LiveDesktopMode.CONTROL ? /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => watch(), disabled: session.isPending, className: BUTTON_CLASS, children: [
258
+ /* @__PURE__ */ jsx(EyeIcon, { className: "size-3.5", "aria-hidden": true }),
259
+ "Stop controlling"
260
+ ] }) : null,
261
+ liveMode ? /* @__PURE__ */ jsx(
262
+ "button",
263
+ {
264
+ type: "button",
265
+ onClick: disconnect,
266
+ "aria-label": "Disconnect from the desktop",
267
+ title: "Disconnect",
268
+ className: cn(BUTTON_CLASS, "px-1.5"),
269
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-3.5", "aria-hidden": true })
270
+ }
271
+ ) : null
272
+ ] }),
273
+ /* @__PURE__ */ jsx(
274
+ "div",
275
+ {
276
+ className: "relative w-full overflow-hidden rounded-lg border border-border bg-muted/40",
277
+ style: { aspectRatio: aspect },
278
+ children: body
279
+ }
280
+ )
281
+ ] });
282
+ };
283
+ export {
284
+ ComputerLiveDesktop,
285
+ LIVE_DESKTOP_READY_TIMEOUT_MS
286
+ };
287
+ //# sourceMappingURL=ComputerLiveDesktop.js.map