@opengeni/react 0.52.1 → 1.0.1
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.
- package/README.md +51 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +43 -14
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-ROIRTM37.js → chunk-5UQPNLIO.js} +6 -6
- package/dist/{chunk-JR3QFY6B.js → chunk-6KLX2RH5.js} +49 -2
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/{chunk-FWS3KC3Z.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-FWS3KC3Z.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-HDBG7XGO.js → chunk-C5ZPSSS5.js} +50 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-X4S56Y2H.js → chunk-HA46CYQK.js} +135 -11
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/{chunk-724BW4TW.js → chunk-IGC6A2ZI.js} +2 -2
- package/dist/chunk-LLOLUHVG.js +5937 -0
- package/dist/chunk-LLOLUHVG.js.map +1 -0
- package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
- package/dist/chunk-MKPIAGVL.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
- package/dist/chunk-XBZWOM2K.js.map +1 -0
- package/dist/{chunk-JAGDUCZQ.js → chunk-Y7OEDUJ5.js} +17 -13
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +6 -4
- package/dist/components/artifacts/index.d.ts +1 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +5 -4
- package/dist/components/computer-viewer.d.ts +2 -1
- package/dist/components/desktop-viewer.d.ts +26 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/sandbox-terminal.d.ts +4 -1
- package/dist/components/sandbox-workspace.d.ts +3 -0
- package/dist/composer.js +6 -6
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-identities.d.ts +5 -1
- package/dist/hooks/use-browser-session.d.ts +2 -1
- package/dist/hooks/use-browser-sessions.d.ts +1 -1
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
- package/dist/hooks/use-computer-session.d.ts +2 -1
- package/dist/hooks/use-computer-sessions.d.ts +1 -1
- package/dist/hooks/use-desktop-stream.d.ts +4 -0
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1220 -1545
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +13 -3
- package/dist/interaction.js +520 -6
- package/dist/interaction.js.map +1 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +20 -1
- package/dist/machines.js +3 -3
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +3 -2
- package/dist/realtime/realtime-control.d.ts +6 -2
- package/dist/realtime.js +138 -24
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +10 -2
- package/dist/session-ui.js +3 -3
- package/dist/session.js +11 -11
- package/dist/timeline/types.d.ts +5 -0
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +73 -3
- package/src/components/artifacts/index.ts +5 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +1287 -177
- package/src/components/computer-viewer.tsx +611 -114
- package/src/components/desktop-viewer.tsx +114 -1
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/sandbox-terminal.tsx +24 -7
- package/src/components/sandbox-workspace.tsx +42 -4
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +13 -11
- package/src/hooks/use-auth-runs.ts +278 -0
- package/src/hooks/use-browser-downloads.ts +218 -0
- package/src/hooks/use-browser-frame-stream.ts +168 -59
- package/src/hooks/use-browser-identities.ts +47 -6
- package/src/hooks/use-browser-session.ts +94 -27
- package/src/hooks/use-browser-sessions.ts +19 -14
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-computer-frame-stream.ts +184 -61
- package/src/hooks/use-computer-session.ts +22 -2
- package/src/hooks/use-computer-sessions.ts +19 -13
- package/src/hooks/use-desktop-stream.ts +39 -7
- package/src/hooks/use-interaction-interventions.ts +242 -0
- package/src/hooks/use-interaction-invalidation.ts +66 -0
- package/src/hooks/use-machine-chip.ts +13 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-site-auth-connections.ts +226 -0
- package/src/hooks/use-supergrok-accounts.ts +81 -0
- package/src/hooks/use-terminal-stream.ts +288 -55
- package/src/index.ts +8 -0
- package/src/interaction.ts +30 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +43 -4
- package/src/lib/terminal-capability.ts +1 -1
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +181 -26
- package/src/session-context.ts +66 -1
- package/src/timeline/activity-rail.tsx +6 -0
- package/src/timeline/projection.ts +58 -0
- package/src/timeline/types.ts +2 -0
- package/styles/compiled.css +64 -6
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-6BBWWOOU.js +0 -82
- package/dist/chunk-6BBWWOOU.js.map +0 -1
- package/dist/chunk-7I2KB3OK.js +0 -3895
- package/dist/chunk-7I2KB3OK.js.map +0 -1
- package/dist/chunk-HDBG7XGO.js.map +0 -1
- package/dist/chunk-JAGDUCZQ.js.map +0 -1
- package/dist/chunk-JR3QFY6B.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-X4S56Y2H.js.map +0 -1
- package/dist/interactive-workbench-viewers-KHNOHWQE.js +0 -13
- /package/dist/{chunk-ROIRTM37.js.map → chunk-5UQPNLIO.js.map} +0 -0
- /package/dist/{chunk-724BW4TW.js.map → chunk-IGC6A2ZI.js.map} +0 -0
- /package/dist/{interactive-workbench-viewers-KHNOHWQE.js.map → interactive-workbench-viewers-TOHBXJEE.js.map} +0 -0
|
@@ -4,8 +4,16 @@ import type {
|
|
|
4
4
|
DesktopStreamCapability,
|
|
5
5
|
} from "@opengeni/sdk";
|
|
6
6
|
import { LoaderCircleIcon, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from "lucide-react";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type ClipboardEvent,
|
|
9
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
useEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
} from "react";
|
|
8
15
|
import { cn } from "../lib/cn";
|
|
16
|
+
import { interactionHostPlatform, type InteractionHostPlatform } from "../lib/host-platform";
|
|
9
17
|
import { useDesktopStream } from "../hooks/use-desktop-stream";
|
|
10
18
|
import type { DesktopWebSocketFactory } from "../hooks/use-relay-frame-stream";
|
|
11
19
|
|
|
@@ -24,9 +32,22 @@ export type DesktopViewerProps = {
|
|
|
24
32
|
scaleViewport?: boolean | undefined;
|
|
25
33
|
/** Custom RFB factory (tests / a WebRTC swap). Defaults to lazy @novnc/novnc. */
|
|
26
34
|
rfbFactory?: DesktopRfbFactory | undefined;
|
|
35
|
+
/** Authenticated controller protocols for proxied RFB transports. */
|
|
36
|
+
webSocketProtocols?: string[] | undefined;
|
|
27
37
|
/** Custom socket factory for the self-hosted `relay-frames` transport (tests).
|
|
28
38
|
* Defaults to `new WebSocket(url)`. Mirrors `rfbFactory`. */
|
|
29
39
|
webSocketFactory?: DesktopWebSocketFactory | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Optional host-owned paste path. ComputerSession hosts use their native,
|
|
42
|
+
* target-scoped clipboard action instead of relying on an RFB server to sync
|
|
43
|
+
* ClientCutText into the graphical seat. Return true once the host accepted
|
|
44
|
+
* the text. Without a host authority the viewer leaves paste untouched rather
|
|
45
|
+
* than claiming success for an unreliable display-server clipboard heuristic.
|
|
46
|
+
*/
|
|
47
|
+
onPasteText?: ((text: string) => boolean) | undefined;
|
|
48
|
+
/** OS behind the RFB seat. Used only to translate the host's primary shortcut
|
|
49
|
+
* (Cmd on macOS, Ctrl elsewhere) to the target's primary modifier. */
|
|
50
|
+
targetPlatform?: "linux" | "macos" | "windows" | null | undefined;
|
|
30
51
|
/**
|
|
31
52
|
* @deprecated Desktop acknowledgment is automatic. Retained as an ignored
|
|
32
53
|
* compatibility prop so existing embedders keep compiling.
|
|
@@ -138,7 +159,10 @@ export function DesktopViewer({
|
|
|
138
159
|
showControlToggle = true,
|
|
139
160
|
scaleViewport,
|
|
140
161
|
rfbFactory,
|
|
162
|
+
webSocketProtocols,
|
|
141
163
|
webSocketFactory,
|
|
164
|
+
onPasteText,
|
|
165
|
+
targetPlatform,
|
|
142
166
|
onActivate,
|
|
143
167
|
onAcknowledge,
|
|
144
168
|
watching,
|
|
@@ -273,6 +297,7 @@ export function DesktopViewer({
|
|
|
273
297
|
interactive: inControl,
|
|
274
298
|
...(scaleViewport !== undefined ? { scaleViewport } : {}),
|
|
275
299
|
...(rfbFactory ? { rfbFactory } : {}),
|
|
300
|
+
...(webSocketProtocols ? { webSocketProtocols } : {}),
|
|
276
301
|
...(webSocketFactory ? { webSocketFactory } : {}),
|
|
277
302
|
});
|
|
278
303
|
|
|
@@ -373,6 +398,45 @@ export function DesktopViewer({
|
|
|
373
398
|
stream.reconnect();
|
|
374
399
|
};
|
|
375
400
|
|
|
401
|
+
const paste = (event: ClipboardEvent<HTMLDivElement>) => {
|
|
402
|
+
if (!inControl) return;
|
|
403
|
+
const text = event.clipboardData.getData("text/plain");
|
|
404
|
+
if (!text && !event.clipboardData.types.includes("text/plain")) return;
|
|
405
|
+
if (!onPasteText?.(text)) return;
|
|
406
|
+
event.preventDefault();
|
|
407
|
+
event.stopPropagation();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const primaryShortcut = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
411
|
+
if (!inControl || !targetPlatform) return;
|
|
412
|
+
const hostPlatform = interactionHostPlatform();
|
|
413
|
+
// noVNC maps a Mac Command key to remote Alt before the actual chord key
|
|
414
|
+
// arrives. Suppress that standalone physical modifier; the complete target
|
|
415
|
+
// chord below owns both its down and up events.
|
|
416
|
+
if (isHostPrimaryModifier(event, hostPlatform)) {
|
|
417
|
+
event.stopPropagation();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const chord = desktopPrimaryShortcut(event, hostPlatform, targetPlatform);
|
|
421
|
+
if (!chord) return;
|
|
422
|
+
// Copy/paste must keep the browser's default clipboard dispatch alive, but
|
|
423
|
+
// noVNC must not also send the host modifier into the remote seat.
|
|
424
|
+
if (chord === "clipboard") {
|
|
425
|
+
event.stopPropagation();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const sent = chord.every(({ keysym, code, down }) => stream.sendKey?.(keysym, code, down));
|
|
429
|
+
if (!sent) return;
|
|
430
|
+
event.preventDefault();
|
|
431
|
+
event.stopPropagation();
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const primaryModifierUp = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
435
|
+
if (inControl && isHostPrimaryModifier(event, interactionHostPlatform())) {
|
|
436
|
+
event.stopPropagation();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
376
440
|
// ── Resolve the single UI state (priority order) ────────────────────────────
|
|
377
441
|
let uiState: DesktopUiState;
|
|
378
442
|
if (viewerCapReached) uiState = "viewer_cap";
|
|
@@ -448,6 +512,10 @@ export function DesktopViewer({
|
|
|
448
512
|
data-state={stream.state}
|
|
449
513
|
data-ui-state={uiState}
|
|
450
514
|
data-in-control={inControl ? "true" : undefined}
|
|
515
|
+
data-target-platform={targetPlatform ?? undefined}
|
|
516
|
+
onKeyDownCapture={primaryShortcut}
|
|
517
|
+
onKeyUpCapture={primaryModifierUp}
|
|
518
|
+
onPasteCapture={paste}
|
|
451
519
|
>
|
|
452
520
|
{/* noVNC mount target. It appends a `width:100%;height:100%` `_screen` div
|
|
453
521
|
and AUTOSCALES the 1280x800 framebuffer to fit THIS box. We pin it to
|
|
@@ -524,6 +592,51 @@ export function DesktopViewer({
|
|
|
524
592
|
);
|
|
525
593
|
}
|
|
526
594
|
|
|
595
|
+
type RfbKeyEvent = { keysym: number; code: string; down: boolean };
|
|
596
|
+
|
|
597
|
+
function isHostPrimaryModifier(
|
|
598
|
+
event: Pick<ReactKeyboardEvent<HTMLDivElement>, "key">,
|
|
599
|
+
hostPlatform: InteractionHostPlatform,
|
|
600
|
+
): boolean {
|
|
601
|
+
return event.key === (hostPlatform === "mac" ? "Meta" : "Control");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Translate one host-primary chord into deterministic RFB events for the
|
|
605
|
+
* target OS. noVNC otherwise treats a Mac Command key as remote Alt, which
|
|
606
|
+
* makes ordinary Cmd+A/Z shortcuts silently wrong on Linux sandboxes. */
|
|
607
|
+
export function desktopPrimaryShortcut(
|
|
608
|
+
event: Pick<
|
|
609
|
+
ReactKeyboardEvent<HTMLDivElement>,
|
|
610
|
+
"altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey" | "code"
|
|
611
|
+
>,
|
|
612
|
+
hostPlatform: InteractionHostPlatform,
|
|
613
|
+
targetPlatform: "linux" | "macos" | "windows",
|
|
614
|
+
): RfbKeyEvent[] | "clipboard" | null {
|
|
615
|
+
const primaryHeld = hostPlatform === "mac" ? event.metaKey : event.ctrlKey;
|
|
616
|
+
if (!primaryHeld || [...event.key].length !== 1) return null;
|
|
617
|
+
const key = event.key.toLowerCase();
|
|
618
|
+
if (!event.altKey && (key === "c" || key === "v")) return "clipboard";
|
|
619
|
+
const primary =
|
|
620
|
+
targetPlatform === "macos"
|
|
621
|
+
? { keysym: 0xffeb, code: "MetaLeft" }
|
|
622
|
+
: { keysym: 0xffe3, code: "ControlLeft" };
|
|
623
|
+
const modifiers = [
|
|
624
|
+
primary,
|
|
625
|
+
...(event.altKey ? [{ keysym: 0xffe9, code: "AltLeft" }] : []),
|
|
626
|
+
...(event.shiftKey ? [{ keysym: 0xffe1, code: "ShiftLeft" }] : []),
|
|
627
|
+
];
|
|
628
|
+
const keyEvent = {
|
|
629
|
+
keysym: key.codePointAt(0)!,
|
|
630
|
+
code: event.code || (/^[a-z]$/u.test(key) ? `Key${key.toUpperCase()}` : key),
|
|
631
|
+
};
|
|
632
|
+
return [
|
|
633
|
+
...modifiers.map((modifier) => ({ ...modifier, down: true })),
|
|
634
|
+
{ ...keyEvent, down: true },
|
|
635
|
+
{ ...keyEvent, down: false },
|
|
636
|
+
...modifiers.reverse().map((modifier) => ({ ...modifier, down: false })),
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
|
|
527
640
|
/**
|
|
528
641
|
* The primary WATCHING-state call-to-action: a large, centered pill button
|
|
529
642
|
* overlaid on the desktop inviting the viewer to drive the mouse & keyboard.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { InteractionIntervention } from "@opengeni/sdk/interaction";
|
|
2
|
+
import { CheckIcon, CircleAlertIcon, LoaderCircleIcon, XIcon } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
|
|
5
|
+
export type InteractionInterventionBannerProps = {
|
|
6
|
+
interventions: InteractionIntervention[];
|
|
7
|
+
activeTargetId?: string | null | undefined;
|
|
8
|
+
mutating?: boolean | undefined;
|
|
9
|
+
className?: string | undefined;
|
|
10
|
+
onOpen?: ((intervention: InteractionIntervention) => void) | undefined;
|
|
11
|
+
onResolve: (intervention: InteractionIntervention, outcome: "completed" | "dismissed") => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Compact durable human-action prompt shared by Browser and Computer viewers. */
|
|
15
|
+
export function InteractionInterventionBanner({
|
|
16
|
+
interventions,
|
|
17
|
+
activeTargetId,
|
|
18
|
+
mutating = false,
|
|
19
|
+
className,
|
|
20
|
+
onOpen,
|
|
21
|
+
onResolve,
|
|
22
|
+
}: InteractionInterventionBannerProps) {
|
|
23
|
+
const intervention = interventions[0];
|
|
24
|
+
if (!intervention) return null;
|
|
25
|
+
const targetOpen = activeTargetId === undefined || activeTargetId === intervention.targetId;
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex shrink-0 items-center gap-2 border-b border-og-status-waiting/25 bg-og-status-waiting/[0.07] px-3 py-2",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
role="status"
|
|
33
|
+
>
|
|
34
|
+
<span className="grid size-7 shrink-0 place-items-center rounded-og-sm bg-og-status-waiting/10 text-og-status-waiting">
|
|
35
|
+
<CircleAlertIcon className="size-3.5" />
|
|
36
|
+
</span>
|
|
37
|
+
<span className="min-w-0 flex-1">
|
|
38
|
+
<span className="flex items-center gap-1.5 text-og-control font-medium text-og-fg">
|
|
39
|
+
{interventionTitle(intervention.kind)}
|
|
40
|
+
{interventions.length > 1 ? (
|
|
41
|
+
<span className="rounded-full bg-og-surface-2 px-1.5 py-0.5 text-[10px] font-normal text-og-muted">
|
|
42
|
+
+{interventions.length - 1}
|
|
43
|
+
</span>
|
|
44
|
+
) : null}
|
|
45
|
+
</span>
|
|
46
|
+
<span className="block truncate text-og-xs text-og-muted">{intervention.reason}</span>
|
|
47
|
+
</span>
|
|
48
|
+
{!targetOpen && onOpen ? (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => onOpen(intervention)}
|
|
52
|
+
disabled={mutating}
|
|
53
|
+
className="h-7 shrink-0 rounded-og-sm border border-og-border bg-og-surface-1 px-2.5 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:opacity-50"
|
|
54
|
+
>
|
|
55
|
+
Open
|
|
56
|
+
</button>
|
|
57
|
+
) : (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onClick={() => onResolve(intervention, "completed")}
|
|
61
|
+
disabled={mutating}
|
|
62
|
+
className="inline-flex h-7 shrink-0 items-center gap-1 rounded-og-sm bg-og-accent px-2.5 text-og-control font-medium text-og-accent-fg transition hover:brightness-105 disabled:opacity-50"
|
|
63
|
+
>
|
|
64
|
+
{mutating ? (
|
|
65
|
+
<LoaderCircleIcon className="size-3 animate-spin" />
|
|
66
|
+
) : (
|
|
67
|
+
<CheckIcon className="size-3" />
|
|
68
|
+
)}
|
|
69
|
+
Done
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
onClick={() => onResolve(intervention, "dismissed")}
|
|
75
|
+
disabled={mutating}
|
|
76
|
+
className="grid size-7 shrink-0 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
|
|
77
|
+
aria-label="Cancel request"
|
|
78
|
+
title="Cancel request"
|
|
79
|
+
>
|
|
80
|
+
<XIcon className="size-3.5" />
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function interventionTitle(kind: InteractionIntervention["kind"]): string {
|
|
87
|
+
switch (kind) {
|
|
88
|
+
case "manual_login":
|
|
89
|
+
return "Sign in needed";
|
|
90
|
+
case "mfa":
|
|
91
|
+
return "Verification needed";
|
|
92
|
+
case "external_action":
|
|
93
|
+
return "Action needed";
|
|
94
|
+
case "confirmation":
|
|
95
|
+
return "Confirmation needed";
|
|
96
|
+
case "other":
|
|
97
|
+
return "Agent needs your help";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -95,7 +95,7 @@ export function MachineDetail({
|
|
|
95
95
|
data-remove-machine
|
|
96
96
|
aria-label={`Remove machine ${machine.name}`}
|
|
97
97
|
onClick={() => onRemove(machine)}
|
|
98
|
-
className="inline-flex min-h-9 items-center gap-1.5 rounded-og-sm border border-og-status-failed/40 px-2.5 py-1 font-medium text-og-xs text-og-status-failed transition-colors hover:bg-og-status-failed/10 focus-visible:outline-
|
|
98
|
+
className="inline-flex min-h-9 items-center gap-1.5 rounded-og-sm border border-og-status-failed/40 px-2.5 py-1 font-medium text-og-xs text-og-status-failed transition-colors hover:bg-og-status-failed/10 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-status-failed/50 pointer-coarse:min-h-11"
|
|
99
99
|
>
|
|
100
100
|
<Trash2Icon className="size-3.5" aria-hidden />
|
|
101
101
|
<span className="hidden sm:inline">Remove machine</span>
|
|
@@ -65,6 +65,7 @@ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
|
|
|
65
65
|
billingHints: {
|
|
66
66
|
opengeni_credits: "Will use credits",
|
|
67
67
|
codex_subscription: "ChatGPT / Codex plan",
|
|
68
|
+
supergrok_subscription: "SuperGrok / xAI plan",
|
|
68
69
|
byok: "Billed to your AI Gateway",
|
|
69
70
|
},
|
|
70
71
|
};
|
|
@@ -132,6 +133,17 @@ function ChatGptMark(props: SVGProps<SVGSVGElement>) {
|
|
|
132
133
|
);
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
function XaiMark(props: SVGProps<SVGSVGElement>) {
|
|
137
|
+
return (
|
|
138
|
+
<svg viewBox="0 0 466.04 516.93" fill="currentColor" aria-hidden="true" {...props}>
|
|
139
|
+
<polygon points="0.12 182.71 234.14 516.92 338.15 516.92 104.13 182.71 0.12 182.71" />
|
|
140
|
+
<polygon points="0 516.92 104.08 516.92 156.08 442.67 104.04 368.34 0 516.92" />
|
|
141
|
+
<polygon points="466.04 0 361.96 0 182.1 256.86 234.15 331.18 466.04 0" />
|
|
142
|
+
<polygon points="380.78 516.92 466.04 516.92 466.04 37.16 380.78 158.92 380.78 516.92" />
|
|
143
|
+
</svg>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
export function BillingClassMark(props: {
|
|
136
148
|
billingClass: PickerBillingClass;
|
|
137
149
|
className?: string | undefined;
|
|
@@ -140,6 +152,7 @@ export function BillingClassMark(props: {
|
|
|
140
152
|
const labels: Record<PickerBillingClass, string> = {
|
|
141
153
|
opengeni_credits: "OpenGeni",
|
|
142
154
|
codex_subscription: "Codex",
|
|
155
|
+
supergrok_subscription: "SuperGrok",
|
|
143
156
|
byok: "Bring your own key",
|
|
144
157
|
};
|
|
145
158
|
const label = props["aria-label"] ?? labels[props.billingClass];
|
|
@@ -162,6 +175,8 @@ export function BillingClassMark(props: {
|
|
|
162
175
|
<OpenGeniMark className={mark} />
|
|
163
176
|
) : props.billingClass === "codex_subscription" ? (
|
|
164
177
|
<ChatGptMark className={mark} />
|
|
178
|
+
) : props.billingClass === "supergrok_subscription" ? (
|
|
179
|
+
<XaiMark className={mark} />
|
|
165
180
|
) : (
|
|
166
181
|
<KeyRoundIcon className={mark} aria-hidden />
|
|
167
182
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TerminalCapability } from "@opengeni/sdk";
|
|
2
|
-
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { type TerminalStreamStatus, useTerminalStream } from "../hooks/use-terminal-stream";
|
|
5
5
|
import type { UseSandboxTerminalResult } from "../hooks/use-sandbox-terminal";
|
|
@@ -87,6 +87,8 @@ export type SandboxTerminalProps = {
|
|
|
87
87
|
* the firehose-only default).
|
|
88
88
|
*/
|
|
89
89
|
onActivate?: (() => void) | undefined;
|
|
90
|
+
/** Re-negotiate the terminal capability after an unexpected socket failure. */
|
|
91
|
+
onReconnectNeeded?: (() => void) | undefined;
|
|
90
92
|
className?: string | undefined;
|
|
91
93
|
};
|
|
92
94
|
|
|
@@ -94,6 +96,7 @@ export type SandboxTerminalProps = {
|
|
|
94
96
|
// drive it without a hard type dep on the lazily-imported lib.
|
|
95
97
|
type XtermLike = {
|
|
96
98
|
open: (el: HTMLElement) => void;
|
|
99
|
+
focus: () => void;
|
|
97
100
|
write: (data: string) => void;
|
|
98
101
|
clear: () => void;
|
|
99
102
|
onData: (cb: (data: string) => void) => { dispose: () => void };
|
|
@@ -223,6 +226,7 @@ export function SandboxTerminal({
|
|
|
223
226
|
showHeader,
|
|
224
227
|
shell,
|
|
225
228
|
onActivate,
|
|
229
|
+
onReconnectNeeded,
|
|
226
230
|
className,
|
|
227
231
|
}: SandboxTerminalProps) {
|
|
228
232
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -237,7 +241,8 @@ export function SandboxTerminal({
|
|
|
237
241
|
const [activated, setActivated] = useState(false);
|
|
238
242
|
|
|
239
243
|
// ── Interactive transport switch ─────────────────────────────────────────────
|
|
240
|
-
const ptyDescriptor =
|
|
244
|
+
const ptyDescriptor =
|
|
245
|
+
terminalCapability?.transport === "pty-ws" || terminalCapability?.transport === "relay-pty";
|
|
241
246
|
const ptyAttachable = terminalCanAcquirePty(terminalCapability);
|
|
242
247
|
const {
|
|
243
248
|
status: ptyStatus,
|
|
@@ -251,7 +256,7 @@ export function SandboxTerminal({
|
|
|
251
256
|
capability:
|
|
252
257
|
ptyAttachable && ready
|
|
253
258
|
? {
|
|
254
|
-
transport:
|
|
259
|
+
transport: terminalCapability!.transport,
|
|
255
260
|
url: ptyDescriptor ? terminalCapability!.url : null,
|
|
256
261
|
token: ptyDescriptor ? terminalCapability!.token : null,
|
|
257
262
|
expiresAt: ptyDescriptor ? terminalCapability!.expiresAt : null,
|
|
@@ -260,6 +265,7 @@ export function SandboxTerminal({
|
|
|
260
265
|
onOutput: (data) => {
|
|
261
266
|
termRef.current?.write(data);
|
|
262
267
|
},
|
|
268
|
+
onReconnectNeeded,
|
|
263
269
|
});
|
|
264
270
|
|
|
265
271
|
// A pty-ws capability exclusively owns the screen even while connecting or
|
|
@@ -304,6 +310,16 @@ export function SandboxTerminal({
|
|
|
304
310
|
});
|
|
305
311
|
|
|
306
312
|
function handleActivate() {
|
|
313
|
+
// Capture-phase activation may synchronously negotiate a fresh terminal
|
|
314
|
+
// capability and rerender the shell. Focus xterm before that state change so
|
|
315
|
+
// the user's first keystroke or paste reaches its already-bounded input
|
|
316
|
+
// queue instead of landing on the surrounding dock.
|
|
317
|
+
const term = termRef.current;
|
|
318
|
+
if (term && !readOnly && (ptyAttachable || result.write !== null)) {
|
|
319
|
+
term.options.disableStdin = false;
|
|
320
|
+
term.options.cursorBlink = true;
|
|
321
|
+
}
|
|
322
|
+
term?.focus();
|
|
307
323
|
if (!activated) setActivated(true);
|
|
308
324
|
onActivate?.();
|
|
309
325
|
}
|
|
@@ -451,7 +467,7 @@ export function SandboxTerminal({
|
|
|
451
467
|
|
|
452
468
|
// Sync stdin/cursor at runtime (NOT a remount) when interactivity flips, so the
|
|
453
469
|
// projection→PTY handoff keeps the single Terminal instance + its scrollback.
|
|
454
|
-
|
|
470
|
+
useLayoutEffect(() => {
|
|
455
471
|
const term = termRef.current;
|
|
456
472
|
if (!ready || !term) return;
|
|
457
473
|
term.options.disableStdin = !interactive;
|
|
@@ -509,9 +525,10 @@ export function SandboxTerminal({
|
|
|
509
525
|
}
|
|
510
526
|
}, [ready, result.chunks, ptyMode]);
|
|
511
527
|
|
|
512
|
-
// Wire interactive input when allowed.
|
|
513
|
-
//
|
|
514
|
-
|
|
528
|
+
// Wire interactive input when allowed. `ptyWrite` is stable across socket
|
|
529
|
+
// status changes, so this subscription does not tear down while a grant opens
|
|
530
|
+
// or rotates and cannot create a first-keystroke gap.
|
|
531
|
+
useLayoutEffect(() => {
|
|
515
532
|
const term = termRef.current;
|
|
516
533
|
if (!ready || !term || !interactive) {
|
|
517
534
|
setInputReady(false);
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
useState,
|
|
26
26
|
} from "react";
|
|
27
27
|
import type { SessionEvent } from "@opengeni/sdk";
|
|
28
|
+
import type { InteractionPlacement } from "@opengeni/sdk/interaction";
|
|
28
29
|
import {
|
|
29
30
|
CircleCheckIcon,
|
|
30
31
|
CpuIcon,
|
|
@@ -245,6 +246,9 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
|
|
|
245
246
|
initialTab?: string | null | undefined;
|
|
246
247
|
/** Actually selected dock tab. Inactive data surfaces retain cache but issue no live reads. */
|
|
247
248
|
activeTab?: string | null | undefined;
|
|
249
|
+
/** Whether the dock is visible. Hidden docks retain UI state but must not keep
|
|
250
|
+
* expensive browser/computer media streams alive. */
|
|
251
|
+
workspaceVisible?: boolean | undefined;
|
|
248
252
|
/** Host-routed notifications (mutation errors, desktop-consent failures). The
|
|
249
253
|
* package never imports a toast library — the host decides how to surface. */
|
|
250
254
|
onNotify?: ((notification: WorkspaceNotification) => void) | undefined;
|
|
@@ -301,7 +305,7 @@ function emptyWarmIntents(sessionId: string): SessionWarmIntents {
|
|
|
301
305
|
export function useSandboxWorkspaceTabs(
|
|
302
306
|
options: UseSandboxWorkspaceTabsOptions,
|
|
303
307
|
): UseSandboxWorkspaceTabsResult {
|
|
304
|
-
const { client, workspaceId } = useOpenGeni(options);
|
|
308
|
+
const { client, workspaceId, workspaceInteractionEvent } = useOpenGeni(options);
|
|
305
309
|
const {
|
|
306
310
|
sessionId,
|
|
307
311
|
events,
|
|
@@ -317,6 +321,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
317
321
|
} = options;
|
|
318
322
|
const initialTab = options.initialTab ?? null;
|
|
319
323
|
const activeTab = options.activeTab ?? initialTab;
|
|
324
|
+
const workspaceVisible = options.workspaceVisible ?? true;
|
|
320
325
|
const requestedSurfaces = options.surfaces ?? WORKBENCH_SURFACES;
|
|
321
326
|
const surfaceSet = new Set<SandboxWorkspaceSurface>(requestedSurfaces);
|
|
322
327
|
const changesEnabled = surfaceSet.has(WORKBENCH_TAB_CHANGES);
|
|
@@ -325,12 +330,16 @@ export function useSandboxWorkspaceTabs(
|
|
|
325
330
|
const browserEnabled = surfaceSet.has(WORKBENCH_TAB_BROWSER);
|
|
326
331
|
const desktopEnabled = surfaceSet.has(WORKBENCH_TAB_DESKTOP);
|
|
327
332
|
const createLinkedComputer = useCallback(
|
|
328
|
-
async (name: string) => {
|
|
333
|
+
async (name: string, placement?: InteractionPlacement) => {
|
|
329
334
|
const response = await client.createComputerSession(workspaceId, {
|
|
330
335
|
operationId: crypto.randomUUID(),
|
|
331
336
|
sessionId,
|
|
332
337
|
name,
|
|
338
|
+
...(placement ? { placement } : {}),
|
|
333
339
|
});
|
|
340
|
+
if (response.operation.state !== "completed" || response.session.lifecycle !== "active") {
|
|
341
|
+
throw new Error(response.operation.error?.message ?? "The computer could not be opened.");
|
|
342
|
+
}
|
|
334
343
|
return {
|
|
335
344
|
id: response.session.id,
|
|
336
345
|
placement: response.session.placement,
|
|
@@ -444,6 +453,17 @@ export function useSandboxWorkspaceTabs(
|
|
|
444
453
|
provisioningRef.current = { sessionId, provisioning };
|
|
445
454
|
}, [sessionId, provisioning, renegotiate]);
|
|
446
455
|
|
|
456
|
+
// BrowserSession and ComputerSession lifecycles use the same placement lease
|
|
457
|
+
// as files/terminal/desktop, but are workspace resources rather than session
|
|
458
|
+
// events. Re-read the capability document on their canonical workspace
|
|
459
|
+
// invalidation so the one machine chip cannot remain "Offline" beside a live
|
|
460
|
+
// browser/computer (or remain "Live" after the final interaction holder ends).
|
|
461
|
+
// This is a read-only negotiation: it never acquires a holder or wakes a box.
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
if (workspaceInteractionEvent?.workspaceId !== workspaceId) return;
|
|
464
|
+
renegotiate();
|
|
465
|
+
}, [workspaceId, workspaceInteractionEvent, renegotiate]);
|
|
466
|
+
|
|
447
467
|
// The cold-paint data source: the latest turn-end capture, fetched with a single
|
|
448
468
|
// api round-trip on mount (no machine). Feeds the Files tree + the Changes/Git
|
|
449
469
|
// diff immediately; a warm box reconciles live without discarding the capture
|
|
@@ -748,6 +768,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
748
768
|
result={terminal}
|
|
749
769
|
terminalCapability={capabilities?.Terminal ?? null}
|
|
750
770
|
onActivate={() => requestWarmIntent("warmTerminal")}
|
|
771
|
+
onReconnectNeeded={caps.renegotiate}
|
|
751
772
|
showHeader
|
|
752
773
|
shell={capabilities?.Terminal.shell ?? undefined}
|
|
753
774
|
liveness={liveness}
|
|
@@ -772,6 +793,15 @@ export function useSandboxWorkspaceTabs(
|
|
|
772
793
|
client={client}
|
|
773
794
|
workspaceId={workspaceId}
|
|
774
795
|
sessionId={sessionId}
|
|
796
|
+
// WorkspaceDock mounts a heavy surface only after its first visit,
|
|
797
|
+
// then keeps it mounted behind `hidden` when another tab is
|
|
798
|
+
// selected. Keep that already-visited surface enabled while the
|
|
799
|
+
// dock itself remains open: tearing down its controller + media
|
|
800
|
+
// socket on every Browser↔Computer tab switch turned an ordinary
|
|
801
|
+
// click into a multi-second cold reconnect. This does not eagerly
|
|
802
|
+
// start BrowserViewer—the lazy panel is still unmounted until the
|
|
803
|
+
// user/agent first visits it.
|
|
804
|
+
enabled={workspaceVisible}
|
|
775
805
|
onNotify={onNotify}
|
|
776
806
|
{...(desktopEnabled ? { createLinkedComputer } : {})}
|
|
777
807
|
{...(onOpenComputerSession ? { onOpenComputer: onOpenComputerSession } : {})}
|
|
@@ -796,6 +826,11 @@ export function useSandboxWorkspaceTabs(
|
|
|
796
826
|
client={client}
|
|
797
827
|
workspaceId={workspaceId}
|
|
798
828
|
sessionId={sessionId}
|
|
829
|
+
// Same visited-surface lifetime as Browser above. A live noVNC
|
|
830
|
+
// connection survives tab switches and ResizeObserver re-fits its
|
|
831
|
+
// canvas when this hidden panel becomes visible again, making the
|
|
832
|
+
// second and later Computer opens local-machine fast.
|
|
833
|
+
enabled={workspaceVisible}
|
|
799
834
|
onNotify={onNotify}
|
|
800
835
|
requestedComputerSessionId={requestedComputerSessionId}
|
|
801
836
|
requestedComputerRequestId={requestedComputerRequestId}
|
|
@@ -828,6 +863,8 @@ export function useSandboxWorkspaceTabs(
|
|
|
828
863
|
onNotify,
|
|
829
864
|
browserWebSocketFactory,
|
|
830
865
|
computerWebSocketFactory,
|
|
866
|
+
workspaceVisible,
|
|
867
|
+
resolvedActiveTab,
|
|
831
868
|
client,
|
|
832
869
|
workspaceId,
|
|
833
870
|
liveness,
|
|
@@ -979,6 +1016,7 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
979
1016
|
...(surfaces ? { surfaces } : {}),
|
|
980
1017
|
...(initialTab ? { initialTab } : {}),
|
|
981
1018
|
activeTab: activeTabHint,
|
|
1019
|
+
workspaceVisible: collapsed !== true,
|
|
982
1020
|
...(onNotify ? { onNotify } : {}),
|
|
983
1021
|
...(browserWebSocketFactory ? { browserWebSocketFactory } : {}),
|
|
984
1022
|
...(computerWebSocketFactory ? { computerWebSocketFactory } : {}),
|
|
@@ -1057,13 +1095,13 @@ function MachineStateChip({ chip }: { chip: MachineChip }) {
|
|
|
1057
1095
|
<div
|
|
1058
1096
|
role="status"
|
|
1059
1097
|
aria-label={`Machine: ${chip.label}`}
|
|
1060
|
-
className="inline-flex min-h-7 items-center gap-1.5 px-2 py-1 text-og-xs font-medium text-og-fg-muted max-[1023px]:min-h-11 pointer-coarse:min-h-11"
|
|
1098
|
+
className="inline-flex min-h-7 min-w-0 items-center gap-1.5 px-2 py-1 text-og-xs font-medium text-og-fg-muted max-[1023px]:min-h-11 pointer-coarse:min-h-11"
|
|
1061
1099
|
>
|
|
1062
1100
|
<span
|
|
1063
1101
|
className={cn("size-1.5 shrink-0 rounded-full", chipDotClass(chip.state))}
|
|
1064
1102
|
aria-hidden
|
|
1065
1103
|
/>
|
|
1066
|
-
<span className="max-w-[11rem] truncate">{chip.label}</span>
|
|
1104
|
+
<span className="min-w-0 max-w-[11rem] truncate">{chip.label}</span>
|
|
1067
1105
|
</div>
|
|
1068
1106
|
);
|
|
1069
1107
|
}
|
|
@@ -536,6 +536,8 @@ function DockChrome({
|
|
|
536
536
|
/** Right-aligned chrome controls (maximize / collapse, or the overlay close). */
|
|
537
537
|
controls: ReactNode;
|
|
538
538
|
}) {
|
|
539
|
+
const chromeRef = useRef<HTMLDivElement | null>(null);
|
|
540
|
+
const [compact, setCompact] = useState(false);
|
|
539
541
|
const active = tabs.find((t) => t.id === current) ?? tabs[0];
|
|
540
542
|
const tabsetId = useId();
|
|
541
543
|
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
@@ -544,6 +546,31 @@ function DockChrome({
|
|
|
544
546
|
const [tabOverflow, setTabOverflow] = useState({ start: false, end: false });
|
|
545
547
|
const activeId = active?.id ?? "";
|
|
546
548
|
const activeTabIndex = tabs.findIndex((tab) => tab.id === activeId);
|
|
549
|
+
|
|
550
|
+
// A desktop dock can be much narrower than the viewport after the user drags
|
|
551
|
+
// its separator. Viewport breakpoints therefore cannot protect the tab strip:
|
|
552
|
+
// the status + controls used to squeeze it to zero width, leaving visible but
|
|
553
|
+
// unclickable ARIA tabs. At narrow *dock* widths, give status/controls their
|
|
554
|
+
// own row and the horizontally scrollable tabs the complete second row.
|
|
555
|
+
useDockLayoutEffect(() => {
|
|
556
|
+
const node = chromeRef.current;
|
|
557
|
+
if (!node) return;
|
|
558
|
+
const update = () => {
|
|
559
|
+
const desktop =
|
|
560
|
+
typeof window.matchMedia !== "function" || window.matchMedia("(min-width: 1024px)").matches;
|
|
561
|
+
const next = desktop && node.getBoundingClientRect().width < 420;
|
|
562
|
+
setCompact((previous) => (previous === next ? previous : next));
|
|
563
|
+
};
|
|
564
|
+
update();
|
|
565
|
+
if (typeof ResizeObserver === "undefined") {
|
|
566
|
+
window.addEventListener("resize", update);
|
|
567
|
+
return () => window.removeEventListener("resize", update);
|
|
568
|
+
}
|
|
569
|
+
const observer = new ResizeObserver(update);
|
|
570
|
+
observer.observe(node);
|
|
571
|
+
return () => observer.disconnect();
|
|
572
|
+
}, []);
|
|
573
|
+
|
|
547
574
|
const keepActiveTabVisible = useCallback(() => {
|
|
548
575
|
const selected = activeTabIndex >= 0 ? tabRefs.current[activeTabIndex] : null;
|
|
549
576
|
selected?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
@@ -631,8 +658,22 @@ function DockChrome({
|
|
|
631
658
|
|
|
632
659
|
return (
|
|
633
660
|
<>
|
|
634
|
-
<div
|
|
635
|
-
|
|
661
|
+
<div
|
|
662
|
+
ref={chromeRef}
|
|
663
|
+
data-dock-chrome
|
|
664
|
+
data-compact={compact ? "true" : undefined}
|
|
665
|
+
className={cn(
|
|
666
|
+
"grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0 min-[640px]:max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto_auto]",
|
|
667
|
+
compact && "gap-y-1",
|
|
668
|
+
)}
|
|
669
|
+
style={compact ? { gridTemplateColumns: "minmax(0, 1fr) auto" } : undefined}
|
|
670
|
+
>
|
|
671
|
+
<div
|
|
672
|
+
className={cn(
|
|
673
|
+
"flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11 min-[640px]:max-[1023px]:col-start-1 min-[640px]:max-[1023px]:row-start-1",
|
|
674
|
+
compact && "hidden",
|
|
675
|
+
)}
|
|
676
|
+
>
|
|
636
677
|
{leading ?? (
|
|
637
678
|
<span className="hidden truncate px-1 text-og-sm font-semibold text-og-fg max-[1023px]:inline">
|
|
638
679
|
Workspace
|
|
@@ -648,7 +689,10 @@ function DockChrome({
|
|
|
648
689
|
absorbing any unusually long host-injected labels. */}
|
|
649
690
|
<div
|
|
650
691
|
ref={tabListRef}
|
|
651
|
-
className=
|
|
692
|
+
className={cn(
|
|
693
|
+
"flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-[1023px]:col-span-3 max-[1023px]:row-start-2 max-[1023px]:w-full min-[640px]:max-[1023px]:col-span-1 min-[640px]:max-[1023px]:col-start-2 min-[640px]:max-[1023px]:row-start-1",
|
|
694
|
+
compact && "col-span-2 col-start-1 row-start-2 w-full",
|
|
695
|
+
)}
|
|
652
696
|
role="tablist"
|
|
653
697
|
aria-orientation="horizontal"
|
|
654
698
|
style={{
|
|
@@ -683,10 +727,20 @@ function DockChrome({
|
|
|
683
727
|
</button>
|
|
684
728
|
))}
|
|
685
729
|
</div>
|
|
686
|
-
<div
|
|
730
|
+
<div
|
|
731
|
+
className={cn(
|
|
732
|
+
"flex min-w-0 shrink-0 items-center justify-self-end min-[640px]:max-[1023px]:col-start-3 min-[640px]:max-[1023px]:row-start-1",
|
|
733
|
+
compact && "col-start-1 row-start-1 justify-self-start",
|
|
734
|
+
)}
|
|
735
|
+
>
|
|
687
736
|
{accessory}
|
|
688
737
|
</div>
|
|
689
|
-
<div
|
|
738
|
+
<div
|
|
739
|
+
className={cn(
|
|
740
|
+
"flex shrink-0 items-center gap-0.5 text-og-fg-subtle min-[640px]:max-[1023px]:col-start-4 min-[640px]:max-[1023px]:row-start-1",
|
|
741
|
+
compact && "col-start-2 row-start-1",
|
|
742
|
+
)}
|
|
743
|
+
>
|
|
690
744
|
{controls}
|
|
691
745
|
</div>
|
|
692
746
|
</div>
|