@opengeni/react 0.48.0 → 0.57.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.
- package/README.md +138 -2
- package/dist/artifacts-document.d.ts +3 -0
- package/dist/artifacts-document.js +17 -0
- package/dist/artifacts-document.js.map +1 -0
- package/dist/artifacts-presentation.d.ts +3 -0
- package/dist/artifacts-presentation.js +17 -0
- package/dist/artifacts-presentation.js.map +1 -0
- package/dist/artifacts-spreadsheet.d.ts +3 -0
- package/dist/artifacts-spreadsheet.js +17 -0
- package/dist/artifacts-spreadsheet.js.map +1 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +199 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
- package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
- package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/chunk-7WCLAEFV.js +2828 -0
- package/dist/chunk-7WCLAEFV.js.map +1 -0
- package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/chunk-HUIKIDM5.js +2148 -0
- package/dist/chunk-HUIKIDM5.js.map +1 -0
- package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
- package/dist/chunk-IGC6A2ZI.js.map +1 -0
- package/dist/chunk-JVYQRRC4.js +53 -0
- package/dist/chunk-JVYQRRC4.js.map +1 -0
- package/dist/chunk-KCGXPP5V.js +157 -0
- package/dist/chunk-KCGXPP5V.js.map +1 -0
- 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-PEI6RANI.js +3234 -0
- package/dist/chunk-PEI6RANI.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
- package/dist/chunk-WSK7G5LE.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-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +10 -2
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +7 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/types.d.ts +1 -1
- package/dist/components/approval-surface.d.ts +2 -2
- package/dist/components/artifacts/artifact-surface.d.ts +18 -0
- package/dist/components/artifacts/document-editor.d.ts +172 -0
- package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
- package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
- package/dist/components/artifacts/editable-document.d.ts +24 -0
- package/dist/components/artifacts/editable-presentation.d.ts +24 -0
- package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
- package/dist/components/artifacts/index.d.ts +9 -0
- package/dist/components/artifacts/presentation-editor.d.ts +297 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
- package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +30 -0
- package/dist/components/chat-composer.d.ts +8 -8
- package/dist/components/code-editor.d.ts +25 -3
- package/dist/components/command-palette.d.ts +2 -2
- package/dist/components/composer-transcription-control.d.ts +2 -2
- package/dist/components/composer.d.ts +17 -17
- package/dist/components/computer-viewer.d.ts +24 -0
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/desktop-viewer.d.ts +50 -22
- package/dist/components/diff-view.d.ts +1 -1
- package/dist/components/enrollment-consent.d.ts +1 -1
- package/dist/components/enrollment-device-flow.d.ts +1 -1
- package/dist/components/file-browser.d.ts +2 -2
- package/dist/components/fleet-tile.d.ts +1 -1
- package/dist/components/generated-video-player.d.ts +10 -0
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +2 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/interactive-workbench-viewers.d.ts +2 -0
- package/dist/components/machine-card.d.ts +2 -2
- package/dist/components/machine-dock-bar.d.ts +3 -3
- package/dist/components/machine-health-pill.d.ts +2 -2
- package/dist/components/machine-input-display.d.ts +1 -1
- package/dist/components/machine-metrics.d.ts +2 -2
- package/dist/components/machine-status-pill.d.ts +4 -4
- package/dist/components/machines/health.d.ts +1 -1
- package/dist/components/machines/machine-detail.d.ts +3 -3
- package/dist/components/machines/metric-history-chart.d.ts +1 -1
- package/dist/components/machines/metric-sparkline.d.ts +2 -2
- package/dist/components/machines/series.d.ts +2 -2
- package/dist/components/machines-dashboard.d.ts +2 -2
- package/dist/components/markdown.d.ts +1 -1
- package/dist/components/message-timeline.d.ts +12 -5
- package/dist/components/model-picker.d.ts +2 -2
- package/dist/components/model-policy-picker.d.ts +7 -7
- package/dist/components/pierre-diff.d.ts +1 -1
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/queue-draft-policy.d.ts +1 -1
- package/dist/components/queue-surface-implementation.d.ts +3 -3
- package/dist/components/queue-surface-state.d.ts +4 -4
- package/dist/components/queue-surface.d.ts +5 -5
- package/dist/components/sandbox-files.d.ts +3 -3
- package/dist/components/sandbox-terminal.d.ts +24 -4
- package/dist/components/sandbox-workspace.d.ts +28 -9
- package/dist/components/session-chrome.d.ts +4 -4
- package/dist/components/session-status.d.ts +2 -2
- package/dist/components/timeline-annotation-selection.d.ts +9 -0
- package/dist/components/timeline-annotations-dialog.d.ts +16 -0
- package/dist/components/timeline-annotations.d.ts +13 -0
- package/dist/components/tooltip.d.ts +4 -4
- package/dist/components/user-message-body.d.ts +2 -2
- package/dist/components/workbench-changes.d.ts +2 -2
- package/dist/components/workspace-dock.d.ts +1 -1
- package/dist/composer.d.ts +11 -11
- package/dist/composer.js +7 -7
- package/dist/hooks/internal.d.ts +1 -1
- package/dist/hooks/use-attached-browsers.d.ts +21 -0
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-available-models.d.ts +2 -2
- package/dist/hooks/use-billing-usage.d.ts +1 -1
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
- package/dist/hooks/use-browser-identities.d.ts +30 -0
- package/dist/hooks/use-browser-session.d.ts +29 -0
- package/dist/hooks/use-browser-sessions.d.ts +32 -0
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-codex-accounts.d.ts +2 -2
- package/dist/hooks/use-composer.d.ts +36 -9
- package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
- package/dist/hooks/use-computer-session.d.ts +29 -0
- package/dist/hooks/use-computer-sessions.d.ts +28 -0
- package/dist/hooks/use-desktop-stream.d.ts +5 -1
- package/dist/hooks/use-environments.d.ts +1 -1
- package/dist/hooks/use-file-attachments.d.ts +1 -1
- package/dist/hooks/use-goal.d.ts +2 -2
- package/dist/hooks/use-human-input.d.ts +2 -2
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
- package/dist/hooks/use-machine-chip.d.ts +2 -2
- package/dist/hooks/use-machines.d.ts +4 -4
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-packs.d.ts +7 -2
- package/dist/hooks/use-rigs.d.ts +1 -1
- package/dist/hooks/use-sandbox-files.d.ts +6 -4
- package/dist/hooks/use-sandbox-git.d.ts +6 -5
- package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
- package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
- package/dist/hooks/use-session-capabilities.d.ts +5 -4
- package/dist/hooks/use-session-control.d.ts +1 -1
- package/dist/hooks/use-session-events.d.ts +2 -2
- package/dist/hooks/use-session-lineage.d.ts +1 -1
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-session.d.ts +2 -2
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-slash-commands.d.ts +1 -1
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +11 -2
- package/dist/hooks/use-turn-queue.d.ts +2 -2
- package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
- package/dist/hooks/use-voice-input.d.ts +1 -1
- package/dist/hooks/use-workspace-capture.d.ts +1 -1
- package/dist/hooks/use-workspace-edit.d.ts +1 -1
- package/dist/hooks/use-workspace-sessions.d.ts +1 -1
- package/dist/hooks/use-workspaces.d.ts +1 -1
- package/dist/index.d.ts +162 -155
- package/dist/index.js +1822 -1703
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +33 -0
- package/dist/interaction.js +550 -0
- package/dist/interaction.js.map +1 -0
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +24 -1
- package/dist/lib/sandbox-liveness.d.ts +9 -0
- package/dist/lib/terminal-capability.d.ts +5 -0
- package/dist/lib/xterm-theme.d.ts +1 -1
- package/dist/machines.d.ts +30 -30
- package/dist/machines.js +2 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +6 -5
- package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
- package/dist/realtime/dropdown-menu.d.ts +8 -8
- package/dist/realtime/realtime-control.d.ts +8 -8
- package/dist/realtime.d.ts +4 -4
- package/dist/realtime.js +38 -11
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +28 -2
- package/dist/session-ui.d.ts +14 -14
- package/dist/session-ui.js +5 -4
- package/dist/session.d.ts +28 -28
- package/dist/session.js +10 -10
- package/dist/timeline/activity-rail.d.ts +5 -3
- package/dist/timeline/disclosure-context.d.ts +1 -1
- package/dist/timeline/fleet-decision-projection.d.ts +1 -1
- package/dist/timeline/fleet-decision-row.d.ts +3 -3
- package/dist/timeline/index.d.ts +19 -19
- package/dist/timeline/parsers.d.ts +4 -1
- package/dist/timeline/projection.d.ts +3 -3
- package/dist/timeline/registry.d.ts +8 -2
- package/dist/timeline/screenshot-lightbox.d.ts +2 -2
- package/dist/timeline/shared.d.ts +21 -10
- package/dist/timeline/tool-diff.d.ts +2 -2
- package/dist/timeline/tool-renderers.d.ts +1 -1
- package/dist/timeline/turn-summary.d.ts +3 -3
- package/dist/timeline/types.d.ts +44 -3
- package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
- package/package.json +36 -7
- package/src/artifacts-document.ts +31 -0
- package/src/artifacts-presentation.ts +36 -0
- package/src/artifacts-spreadsheet.ts +21 -0
- package/src/artifacts.ts +89 -0
- package/src/client.ts +157 -1
- package/src/clipboard.ts +2 -0
- package/src/components/artifacts/artifact-surface.tsx +93 -0
- package/src/components/artifacts/document-editor.tsx +2255 -0
- package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
- package/src/components/artifacts/editable-document.tsx +657 -0
- package/src/components/artifacts/editable-presentation.tsx +1020 -0
- package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
- package/src/components/artifacts/index.ts +104 -0
- package/src/components/artifacts/presentation-editor.tsx +3148 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +2702 -0
- package/src/components/chat-composer.tsx +13 -0
- package/src/components/code-editor.tsx +89 -30
- package/src/components/composer-transcription-control.tsx +10 -6
- package/src/components/computer-viewer.tsx +1636 -0
- package/src/components/desktop-viewer.tsx +215 -74
- package/src/components/generated-video-player.tsx +89 -0
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/interactive-workbench-viewers.ts +2 -0
- package/src/components/machine-input-display.ts +3 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/message-timeline.tsx +289 -32
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/queue-surface-implementation.tsx +116 -60
- package/src/components/sandbox-files.tsx +2 -2
- package/src/components/sandbox-terminal.tsx +128 -46
- package/src/components/sandbox-workspace.tsx +256 -90
- package/src/components/session-chrome.tsx +53 -13
- package/src/components/timeline-annotation-selection.tsx +221 -0
- package/src/components/timeline-annotations-dialog.tsx +213 -0
- package/src/components/timeline-annotations.tsx +84 -0
- package/src/components/workbench-changes.tsx +188 -31
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +154 -0
- 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 +425 -0
- package/src/hooks/use-browser-identities.ts +266 -0
- package/src/hooks/use-browser-session.ts +543 -0
- package/src/hooks/use-browser-sessions.ts +286 -0
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-composer.ts +769 -24
- package/src/hooks/use-computer-frame-stream.ts +434 -0
- package/src/hooks/use-computer-session.ts +462 -0
- package/src/hooks/use-computer-sessions.ts +251 -0
- 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 +15 -3
- package/src/hooks/use-machines.ts +33 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-packs.ts +51 -0
- package/src/hooks/use-sandbox-files.ts +98 -55
- package/src/hooks/use-sandbox-git.ts +93 -38
- package/src/hooks/use-sandbox-terminal.ts +3 -2
- package/src/hooks/use-session-capabilities.ts +59 -46
- 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 +359 -47
- package/src/hooks/use-video-artifact-playback.ts +28 -0
- package/src/hooks/use-workspace-edit.ts +2 -1
- package/src/index.ts +18 -0
- package/src/interaction.ts +101 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +47 -4
- package/src/lib/sandbox-liveness.ts +11 -0
- package/src/lib/terminal-capability.ts +10 -0
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +34 -6
- package/src/session-context.ts +156 -1
- package/src/timeline/activity-rail.tsx +25 -3
- package/src/timeline/index.ts +3 -0
- package/src/timeline/parsers.ts +14 -1
- package/src/timeline/projection.ts +269 -14
- package/src/timeline/registry.ts +13 -1
- package/src/timeline/screenshot-lightbox.tsx +19 -9
- package/src/timeline/shared.tsx +56 -8
- package/src/timeline/tool-renderers.tsx +303 -50
- package/src/timeline/types.ts +57 -3
- package/styles/compiled.css +312 -21
- package/styles/index.css +15 -0
- package/styles/responsive.css +13 -1
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-EKZH6IDG.js.map +0 -1
- package/dist/chunk-FWXGSNXT.js.map +0 -1
- package/dist/chunk-LHLYWG5R.js.map +0 -1
- package/dist/chunk-O4VXOBGF.js.map +0 -1
- package/dist/chunk-OL6QVXGF.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-XCNTXWBB.js.map +0 -1
- package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
- package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
- package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { SuperGrokAccount, SuperGrokAccountsResponse } from "@opengeni/sdk";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
|
|
4
|
+
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
5
|
+
import { useMutationRunner, usePolledValue } from "./internal";
|
|
6
|
+
|
|
7
|
+
export type SuperGrokAccountsClientLike = {
|
|
8
|
+
listSuperGrokAccounts: (workspaceId: string) => Promise<SuperGrokAccountsResponse>;
|
|
9
|
+
activateSuperGrokAccount?: (
|
|
10
|
+
workspaceId: string,
|
|
11
|
+
accountId: string,
|
|
12
|
+
) => Promise<{ activated: boolean; accountId: string }>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type UseSuperGrokAccountsOptions = ClientOverride & {
|
|
16
|
+
enabled?: boolean | undefined;
|
|
17
|
+
pollIntervalMs?: number | undefined;
|
|
18
|
+
supergrokClient?: SuperGrokAccountsClientLike | undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type UseSuperGrokAccountsResult = {
|
|
22
|
+
accounts: SuperGrokAccount[];
|
|
23
|
+
activeAccountId: string | null;
|
|
24
|
+
settings: SuperGrokAccountsResponse["settings"];
|
|
25
|
+
loading: boolean;
|
|
26
|
+
refresh: () => Promise<void>;
|
|
27
|
+
activate: (accountId: string) => Promise<boolean>;
|
|
28
|
+
activating: boolean;
|
|
29
|
+
mutationError: Error | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const EMPTY: SuperGrokAccountsResponse = {
|
|
33
|
+
accounts: [],
|
|
34
|
+
activeAccountId: null,
|
|
35
|
+
settings: {
|
|
36
|
+
rotationEnabled: false,
|
|
37
|
+
rotationStrategy: "sharded",
|
|
38
|
+
activeCredentialId: null,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Workspace-visible xAI accounts. Workspace scope is the connection default. */
|
|
43
|
+
export function useSuperGrokAccounts(
|
|
44
|
+
options: UseSuperGrokAccountsOptions = {},
|
|
45
|
+
): UseSuperGrokAccountsResult {
|
|
46
|
+
const { client, workspaceId } = useOpenGeni(options);
|
|
47
|
+
const supergrokClient =
|
|
48
|
+
options.supergrokClient ?? (client as unknown as SuperGrokAccountsClientLike);
|
|
49
|
+
const load = useCallback(
|
|
50
|
+
async () => await supergrokClient.listSuperGrokAccounts(workspaceId),
|
|
51
|
+
[supergrokClient, workspaceId],
|
|
52
|
+
);
|
|
53
|
+
const { data, loading, refresh } = usePolledValue(load, {
|
|
54
|
+
enabled: options.enabled,
|
|
55
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
56
|
+
});
|
|
57
|
+
const { run, mutating: activating, mutationError } = useMutationRunner();
|
|
58
|
+
const activate = useCallback(
|
|
59
|
+
async (accountId: string): Promise<boolean> => {
|
|
60
|
+
if (!supergrokClient.activateSuperGrokAccount) return false;
|
|
61
|
+
const result = await run(async () => {
|
|
62
|
+
await supergrokClient.activateSuperGrokAccount!(workspaceId, accountId);
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
if (result) await refresh();
|
|
66
|
+
return result === true;
|
|
67
|
+
},
|
|
68
|
+
[refresh, run, supergrokClient, workspaceId],
|
|
69
|
+
);
|
|
70
|
+
const state = data ?? EMPTY;
|
|
71
|
+
return {
|
|
72
|
+
accounts: state.accounts,
|
|
73
|
+
activeAccountId: state.activeAccountId,
|
|
74
|
+
settings: state.settings,
|
|
75
|
+
loading,
|
|
76
|
+
refresh,
|
|
77
|
+
activate,
|
|
78
|
+
activating,
|
|
79
|
+
mutationError,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -7,7 +7,16 @@ import {
|
|
|
7
7
|
ttydResizeFrame,
|
|
8
8
|
TtydServerCommand,
|
|
9
9
|
} from "@opengeni/sdk";
|
|
10
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import {
|
|
12
|
+
decodeStreamFrame,
|
|
13
|
+
decodeStreamOpenAck,
|
|
14
|
+
encodeStreamFrame,
|
|
15
|
+
encodeStreamOpen,
|
|
16
|
+
type RelayChannel,
|
|
17
|
+
STREAM_KIND_PTY,
|
|
18
|
+
STREAM_ROLE_CLIENT,
|
|
19
|
+
} from "../lib/relay-wire";
|
|
11
20
|
|
|
12
21
|
/** The ttyd connection lifecycle as surfaced to the component. */
|
|
13
22
|
export type TerminalStreamStatus = "connecting" | "open" | "closed" | "error";
|
|
@@ -17,11 +26,14 @@ export type UseTerminalStreamOptions = {
|
|
|
17
26
|
* The stream connects ONLY when `transport === "pty-ws"` and `url` is set; on a
|
|
18
27
|
* cold box (`transport === "sse-events"` / no url) it stays idle and the caller
|
|
19
28
|
* falls back to the Channel-A read-only firehose. */
|
|
20
|
-
capability: Pick<TerminalCapability, "transport" | "url" | "token"> | null;
|
|
29
|
+
capability: Pick<TerminalCapability, "transport" | "url" | "token" | "expiresAt"> | null;
|
|
21
30
|
/** Called for each OUTPUT payload from ttyd (write verbatim into xterm). */
|
|
22
31
|
onOutput?: ((data: string) => void) | undefined;
|
|
23
32
|
/** Called when ttyd sends a SET_WINDOW_TITLE frame. */
|
|
24
33
|
onTitle?: ((title: string) => void) | undefined;
|
|
34
|
+
/** Requests a fresh negotiated terminal grant after an unexpected transport
|
|
35
|
+
* failure. Intentional disconnects and effect cleanup never call it. */
|
|
36
|
+
onReconnectNeeded?: (() => void) | undefined;
|
|
25
37
|
/** Initial PTY size to seed the ttyd auth frame + first resize. */
|
|
26
38
|
initialCols?: number | undefined;
|
|
27
39
|
initialRows?: number | undefined;
|
|
@@ -31,7 +43,8 @@ export type UseTerminalStreamResult = {
|
|
|
31
43
|
/** True once the ttyd socket is open (and the auth frame has been sent). */
|
|
32
44
|
connected: boolean;
|
|
33
45
|
status: TerminalStreamStatus;
|
|
34
|
-
/** Pipe a keystroke/paste to
|
|
46
|
+
/** Pipe a keystroke/paste to PTY stdin. Input during CONNECTING is bounded and
|
|
47
|
+
* replayed only after ttyd authentication succeeds. */
|
|
35
48
|
write: (data: string) => void;
|
|
36
49
|
/** Tell ttyd the PTY window changed size (on xterm fit/resize). */
|
|
37
50
|
resize: (cols: number, rows: number) => void;
|
|
@@ -39,6 +52,84 @@ export type UseTerminalStreamResult = {
|
|
|
39
52
|
disconnect: () => void;
|
|
40
53
|
};
|
|
41
54
|
|
|
55
|
+
// Keystrokes entered during the websocket handshake are held until ttyd has
|
|
56
|
+
// accepted its auth/resize frames. Bound the queue by UTF-16 code units (a hard
|
|
57
|
+
// <=128 KiB string payload in current JS engines) so a paste cannot turn a slow
|
|
58
|
+
// handshake into unbounded renderer memory. Overflow rejects the whole pending
|
|
59
|
+
// input rather than ever executing a truncated shell command.
|
|
60
|
+
export const MAX_PENDING_TERMINAL_INPUT_CODE_UNITS = 64 * 1024;
|
|
61
|
+
const TERMINAL_CONNECT_EXPIRY_SKEW_MS = 5_000;
|
|
62
|
+
const TERMINAL_RECONNECT_INITIAL_MS = 100;
|
|
63
|
+
const TERMINAL_RECONNECT_MAX_MS = 3_000;
|
|
64
|
+
const RELAY_TAG_OPEN = 1;
|
|
65
|
+
const RELAY_TAG_OPEN_ACK = 2;
|
|
66
|
+
const RELAY_TAG_FRAME = 3;
|
|
67
|
+
|
|
68
|
+
function relayDatagram(tag: number, body: Uint8Array): ArrayBuffer {
|
|
69
|
+
const bytes = new Uint8Array(body.length + 1);
|
|
70
|
+
bytes[0] = tag;
|
|
71
|
+
bytes.set(body, 1);
|
|
72
|
+
return bytes.buffer;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function relayChannelFromUrl(url: string): RelayChannel {
|
|
76
|
+
const endpoint = new URL(url);
|
|
77
|
+
const channel: RelayChannel = {
|
|
78
|
+
channelId: endpoint.searchParams.get("channel") ?? "",
|
|
79
|
+
workspaceId: endpoint.searchParams.get("ws") ?? "",
|
|
80
|
+
agentId: endpoint.searchParams.get("agent") ?? "",
|
|
81
|
+
kind: STREAM_KIND_PTY,
|
|
82
|
+
port: Number(endpoint.searchParams.get("port") ?? "0"),
|
|
83
|
+
};
|
|
84
|
+
if (
|
|
85
|
+
!channel.channelId ||
|
|
86
|
+
!channel.workspaceId ||
|
|
87
|
+
!channel.agentId ||
|
|
88
|
+
!Number.isSafeInteger(channel.port) ||
|
|
89
|
+
channel.port <= 0
|
|
90
|
+
) {
|
|
91
|
+
throw new Error("terminal relay endpoint is incomplete");
|
|
92
|
+
}
|
|
93
|
+
return channel;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sendRelayInput(
|
|
97
|
+
socket: WebSocket,
|
|
98
|
+
channel: RelayChannel,
|
|
99
|
+
sequence: bigint,
|
|
100
|
+
data: string,
|
|
101
|
+
): void {
|
|
102
|
+
socket.send(
|
|
103
|
+
relayDatagram(
|
|
104
|
+
RELAY_TAG_FRAME,
|
|
105
|
+
encodeStreamFrame({
|
|
106
|
+
channelId: channel.channelId,
|
|
107
|
+
seq: sequence.toString(),
|
|
108
|
+
data: new TextEncoder().encode(data),
|
|
109
|
+
producedAtMs: String(Date.now()),
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A bearer must remain valid long enough to finish a new websocket handshake.
|
|
116
|
+
* Existing open sockets are unaffected: this is evaluated only when the
|
|
117
|
+
* credential identity changes and the connection effect attempts a new socket. */
|
|
118
|
+
export function terminalStreamCredentialUsable(
|
|
119
|
+
capability: Pick<TerminalCapability, "transport" | "url" | "expiresAt"> | null | undefined,
|
|
120
|
+
nowMs = Date.now(),
|
|
121
|
+
): boolean {
|
|
122
|
+
if (
|
|
123
|
+
(capability?.transport !== "pty-ws" && capability?.transport !== "relay-pty") ||
|
|
124
|
+
!capability.url
|
|
125
|
+
) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (!capability.expiresAt) return true;
|
|
129
|
+
const expiresAt = Date.parse(capability.expiresAt);
|
|
130
|
+
return Number.isFinite(expiresAt) && expiresAt > nowMs + TERMINAL_CONNECT_EXPIRY_SKEW_MS;
|
|
131
|
+
}
|
|
132
|
+
|
|
42
133
|
/** Decode an inbound ttyd frame's payload (everything after the 1-char command).
|
|
43
134
|
* ttyd may send either a text frame (string) or a binary frame (ArrayBuffer);
|
|
44
135
|
* for binary we slice off the first byte (the command) and utf-8 decode the rest. */
|
|
@@ -52,6 +143,14 @@ function decodeFrame(data: string | ArrayBuffer): { command: string; payload: st
|
|
|
52
143
|
return { command, payload };
|
|
53
144
|
}
|
|
54
145
|
|
|
146
|
+
function closeSocket(socket: WebSocket | null): void {
|
|
147
|
+
try {
|
|
148
|
+
socket?.close();
|
|
149
|
+
} catch {
|
|
150
|
+
// Already closed or rejected by the host WebSocket implementation.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
55
154
|
/**
|
|
56
155
|
* Drive a ttyd PTY-over-websocket connection from a `pty-ws` Terminal capability,
|
|
57
156
|
* symmetric with `use-desktop-stream` (the noVNC-over-tunnel hook). The scoped
|
|
@@ -70,9 +169,21 @@ function decodeFrame(data: string | ArrayBuffer): { command: string; payload: st
|
|
|
70
169
|
* SSR-safe: the socket open lives in `useEffect`, so a server render is a no-op.
|
|
71
170
|
*/
|
|
72
171
|
export function useTerminalStream(options: UseTerminalStreamOptions): UseTerminalStreamResult {
|
|
73
|
-
const { capability, onOutput, onTitle, initialCols, initialRows } = options;
|
|
172
|
+
const { capability, onOutput, onTitle, onReconnectNeeded, initialCols, initialRows } = options;
|
|
74
173
|
const [status, setStatus] = useState<TerminalStreamStatus>("closed");
|
|
174
|
+
const [reconnectGeneration, setReconnectGeneration] = useState(0);
|
|
75
175
|
const wsRef = useRef<WebSocket | null>(null);
|
|
176
|
+
const pendingInputRef = useRef("");
|
|
177
|
+
const socketFailedRef = useRef(false);
|
|
178
|
+
const socketAuthenticatedRef = useRef(false);
|
|
179
|
+
const relayChannelRef = useRef<RelayChannel | null>(null);
|
|
180
|
+
const relayIdentityRef = useRef<string | null>(null);
|
|
181
|
+
const relayInputSequenceRef = useRef(0n);
|
|
182
|
+
const relayOutputSequenceRef = useRef(0n);
|
|
183
|
+
const reconnectAttemptRef = useRef(0);
|
|
184
|
+
const credentialIdentityRef = useRef<string | null>(null);
|
|
185
|
+
const refreshRequestedRef = useRef(false);
|
|
186
|
+
const intentionalCloseRef = useRef<WeakSet<WebSocket>>(new WeakSet());
|
|
76
187
|
// Latest size, so a resize() before the socket opens is replayed on open, and a
|
|
77
188
|
// reconnect seeds the right geometry.
|
|
78
189
|
const sizeRef = useRef<{ cols: number; rows: number }>({
|
|
@@ -83,52 +194,219 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
|
|
|
83
194
|
// render (the parent passes fresh closures each time).
|
|
84
195
|
const onOutputRef = useRef(onOutput);
|
|
85
196
|
const onTitleRef = useRef(onTitle);
|
|
197
|
+
const onReconnectNeededRef = useRef(onReconnectNeeded);
|
|
86
198
|
onOutputRef.current = onOutput;
|
|
87
199
|
onTitleRef.current = onTitle;
|
|
200
|
+
onReconnectNeededRef.current = onReconnectNeeded;
|
|
88
201
|
|
|
89
202
|
const transport = capability?.transport ?? null;
|
|
90
203
|
const url = capability?.url ?? null;
|
|
91
204
|
const token = capability?.token ?? null;
|
|
205
|
+
const expiresAt = capability?.expiresAt ?? null;
|
|
206
|
+
const transportRef = useRef(transport);
|
|
207
|
+
transportRef.current = transport;
|
|
92
208
|
|
|
93
209
|
useEffect(() => {
|
|
94
210
|
// SSR / no WebSocket / not a live pty-ws cell: stay closed; the caller falls
|
|
95
211
|
// back to the Channel-A read-only firehose.
|
|
96
212
|
if (typeof window === "undefined" || typeof WebSocket === "undefined") return;
|
|
97
|
-
|
|
213
|
+
const relay = transport === "relay-pty";
|
|
214
|
+
if (transport !== "pty-ws" && !relay) {
|
|
215
|
+
// A transport downgrade is a real semantic boundary. Do not replay input
|
|
216
|
+
// captured for a PTY if this surface later changes back from firehose mode.
|
|
217
|
+
pendingInputRef.current = "";
|
|
218
|
+
socketAuthenticatedRef.current = false;
|
|
219
|
+
relayIdentityRef.current = null;
|
|
220
|
+
relayInputSequenceRef.current = 0n;
|
|
221
|
+
relayOutputSequenceRef.current = 0n;
|
|
222
|
+
reconnectAttemptRef.current = 0;
|
|
223
|
+
credentialIdentityRef.current = null;
|
|
224
|
+
refreshRequestedRef.current = false;
|
|
225
|
+
setStatus("closed");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const credentialIdentity = `${transport}\u0000${url ?? ""}\u0000${token ?? ""}\u0000${expiresAt ?? ""}`;
|
|
229
|
+
if (credentialIdentityRef.current !== credentialIdentity) {
|
|
230
|
+
credentialIdentityRef.current = credentialIdentity;
|
|
231
|
+
reconnectAttemptRef.current = 0;
|
|
232
|
+
refreshRequestedRef.current = false;
|
|
233
|
+
}
|
|
234
|
+
// A changed/cleared credential is a new connection attempt boundary. It may
|
|
235
|
+
// receive input while its exact grant is pending even if the prior socket
|
|
236
|
+
// failed; the unchanged failed identity remains fenced because this effect
|
|
237
|
+
// does not re-run for a status-only render.
|
|
238
|
+
socketFailedRef.current = false;
|
|
239
|
+
socketAuthenticatedRef.current = false;
|
|
240
|
+
if (!url) {
|
|
241
|
+
// Exact grants arrive asynchronously. Preserve the bounded first input
|
|
242
|
+
// while the descriptor remains pty-ws, but never open without a credential.
|
|
243
|
+
setStatus("closed");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!terminalStreamCredentialUsable({ transport, url, expiresAt })) {
|
|
247
|
+
// A descriptor from an older rolling server may still carry a bearer that
|
|
248
|
+
// aged out before this surface mounted. Ask once for the exact fresh grant.
|
|
98
249
|
setStatus("closed");
|
|
250
|
+
if (!refreshRequestedRef.current) {
|
|
251
|
+
refreshRequestedRef.current = true;
|
|
252
|
+
onReconnectNeededRef.current?.();
|
|
253
|
+
}
|
|
99
254
|
return;
|
|
100
255
|
}
|
|
101
256
|
|
|
102
257
|
let disposed = false;
|
|
258
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
103
259
|
let socket: WebSocket;
|
|
260
|
+
let relayChannel: RelayChannel | null = null;
|
|
261
|
+
const relayOutputDecoder = new TextDecoder();
|
|
104
262
|
setStatus("connecting");
|
|
105
263
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
264
|
+
if (relay) {
|
|
265
|
+
relayChannel = relayChannelFromUrl(url);
|
|
266
|
+
const relayIdentity = [
|
|
267
|
+
relayChannel.workspaceId,
|
|
268
|
+
relayChannel.agentId,
|
|
269
|
+
relayChannel.port,
|
|
270
|
+
relayChannel.channelId,
|
|
271
|
+
].join("\u0000");
|
|
272
|
+
if (relayIdentityRef.current !== relayIdentity) {
|
|
273
|
+
relayIdentityRef.current = relayIdentity;
|
|
274
|
+
relayInputSequenceRef.current = 0n;
|
|
275
|
+
relayOutputSequenceRef.current = 0n;
|
|
276
|
+
}
|
|
277
|
+
relayChannelRef.current = relayChannel;
|
|
278
|
+
socket = new WebSocket(url);
|
|
279
|
+
} else {
|
|
280
|
+
// The ttyd "tty" subprotocol is REQUIRED — ttyd rejects a handshake without
|
|
281
|
+
// it. The scoped token is already in the tunnel `url`.
|
|
282
|
+
socket = new WebSocket(terminalSocketUrl({ url }), TTYD_SUBPROTOCOL);
|
|
283
|
+
}
|
|
109
284
|
} catch {
|
|
285
|
+
socketFailedRef.current = true;
|
|
110
286
|
setStatus("error");
|
|
111
287
|
return;
|
|
112
288
|
}
|
|
113
289
|
socket.binaryType = "arraybuffer";
|
|
114
290
|
wsRef.current = socket;
|
|
115
291
|
|
|
292
|
+
const requestFreshGrant = () => {
|
|
293
|
+
if (disposed || refreshRequestedRef.current || intentionalCloseRef.current.has(socket))
|
|
294
|
+
return;
|
|
295
|
+
refreshRequestedRef.current = true;
|
|
296
|
+
onReconnectNeededRef.current?.();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const scheduleTransportReconnect = () => {
|
|
300
|
+
if (disposed || reconnectTimer !== null || intentionalCloseRef.current.has(socket)) return;
|
|
301
|
+
if (refreshRequestedRef.current) return;
|
|
302
|
+
if (!terminalStreamCredentialUsable({ transport, url, expiresAt })) {
|
|
303
|
+
requestFreshGrant();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const attempt = reconnectAttemptRef.current++;
|
|
307
|
+
const delay = Math.min(
|
|
308
|
+
TERMINAL_RECONNECT_INITIAL_MS * 2 ** Math.min(attempt, 8),
|
|
309
|
+
TERMINAL_RECONNECT_MAX_MS,
|
|
310
|
+
);
|
|
311
|
+
setStatus("connecting");
|
|
312
|
+
reconnectTimer = setTimeout(() => {
|
|
313
|
+
reconnectTimer = null;
|
|
314
|
+
if (!disposed) setReconnectGeneration((generation) => generation + 1);
|
|
315
|
+
}, delay);
|
|
316
|
+
};
|
|
317
|
+
|
|
116
318
|
socket.onopen = () => {
|
|
117
319
|
if (disposed) return;
|
|
320
|
+
if (relay && relayChannel) {
|
|
321
|
+
try {
|
|
322
|
+
socket.send(
|
|
323
|
+
relayDatagram(
|
|
324
|
+
RELAY_TAG_OPEN,
|
|
325
|
+
encodeStreamOpen({
|
|
326
|
+
channel: relayChannel,
|
|
327
|
+
token: token ?? "",
|
|
328
|
+
role: STREAM_ROLE_CLIENT,
|
|
329
|
+
resumeFromSeq: relayOutputSequenceRef.current.toString(),
|
|
330
|
+
}),
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
} catch {
|
|
334
|
+
socketFailedRef.current = false;
|
|
335
|
+
setStatus("connecting");
|
|
336
|
+
closeSocket(socket);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
118
340
|
// ttyd's required first frame: the auth message (empty token — the gate is
|
|
119
341
|
// the tunnel url + scoped stream token, not a ttyd -c credential), seeded
|
|
120
342
|
// with the current PTY geometry. Then an explicit resize to be safe.
|
|
121
343
|
try {
|
|
122
344
|
socket.send(ttydAuthFrame({ columns: sizeRef.current.cols, rows: sizeRef.current.rows }));
|
|
123
345
|
socket.send(ttydResizeFrame(sizeRef.current.cols, sizeRef.current.rows));
|
|
346
|
+
if (pendingInputRef.current.length > 0) {
|
|
347
|
+
// One websocket frame makes the pre-open queue all-or-nothing from this
|
|
348
|
+
// client's perspective; never execute a prefix of a buffered command.
|
|
349
|
+
socket.send(ttydInputFrame(pendingInputRef.current));
|
|
350
|
+
pendingInputRef.current = "";
|
|
351
|
+
}
|
|
352
|
+
socketAuthenticatedRef.current = true;
|
|
124
353
|
} catch {
|
|
125
|
-
|
|
354
|
+
socketFailedRef.current = false;
|
|
355
|
+
setStatus("connecting");
|
|
356
|
+
closeSocket(socket);
|
|
357
|
+
return;
|
|
126
358
|
}
|
|
127
359
|
setStatus("open");
|
|
360
|
+
reconnectAttemptRef.current = 0;
|
|
361
|
+
refreshRequestedRef.current = false;
|
|
128
362
|
};
|
|
129
363
|
|
|
130
364
|
socket.onmessage = (ev: MessageEvent) => {
|
|
131
365
|
if (disposed) return;
|
|
366
|
+
if (relay) {
|
|
367
|
+
if (!(ev.data instanceof ArrayBuffer) || !relayChannel) return;
|
|
368
|
+
const bytes = new Uint8Array(ev.data);
|
|
369
|
+
if (bytes.length < 1) return;
|
|
370
|
+
const tag = bytes[0];
|
|
371
|
+
const body = bytes.subarray(1);
|
|
372
|
+
if (tag === RELAY_TAG_OPEN_ACK) {
|
|
373
|
+
try {
|
|
374
|
+
const ack = decodeStreamOpenAck(body);
|
|
375
|
+
if (!ack.accepted) throw new Error(ack.error?.message ?? "terminal relay rejected");
|
|
376
|
+
reconnectAttemptRef.current = 0;
|
|
377
|
+
refreshRequestedRef.current = false;
|
|
378
|
+
socketAuthenticatedRef.current = true;
|
|
379
|
+
if (pendingInputRef.current.length > 0) {
|
|
380
|
+
sendRelayInput(
|
|
381
|
+
socket,
|
|
382
|
+
relayChannel,
|
|
383
|
+
relayInputSequenceRef.current++,
|
|
384
|
+
pendingInputRef.current,
|
|
385
|
+
);
|
|
386
|
+
pendingInputRef.current = "";
|
|
387
|
+
}
|
|
388
|
+
setStatus("open");
|
|
389
|
+
} catch {
|
|
390
|
+
socketFailedRef.current = true;
|
|
391
|
+
setStatus("error");
|
|
392
|
+
requestFreshGrant();
|
|
393
|
+
closeSocket(socket);
|
|
394
|
+
}
|
|
395
|
+
} else if (tag === RELAY_TAG_FRAME && socketAuthenticatedRef.current) {
|
|
396
|
+
try {
|
|
397
|
+
const frame = decodeStreamFrame(body);
|
|
398
|
+
const sequence = BigInt(frame.seq);
|
|
399
|
+
if (sequence < relayOutputSequenceRef.current) return;
|
|
400
|
+
relayOutputSequenceRef.current = sequence + 1n;
|
|
401
|
+
if (frame.data.length > 0) {
|
|
402
|
+
onOutputRef.current?.(relayOutputDecoder.decode(frame.data, { stream: true }));
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
// Ignore one malformed frame; the live PTY remains usable.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
132
410
|
const { command, payload } = decodeFrame(ev.data as string | ArrayBuffer);
|
|
133
411
|
switch (command) {
|
|
134
412
|
case TtydServerCommand.OUTPUT:
|
|
@@ -144,64 +422,98 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
|
|
|
144
422
|
};
|
|
145
423
|
|
|
146
424
|
socket.onerror = () => {
|
|
147
|
-
if (!disposed)
|
|
425
|
+
if (!disposed) {
|
|
426
|
+
// A network/relay outage is retryable with the same credential and PTY.
|
|
427
|
+
// Keep accepting bounded input while the transport comes back.
|
|
428
|
+
socketFailedRef.current = false;
|
|
429
|
+
setStatus("connecting");
|
|
430
|
+
closeSocket(socket);
|
|
431
|
+
}
|
|
148
432
|
};
|
|
149
433
|
socket.onclose = () => {
|
|
150
|
-
if (
|
|
434
|
+
if (wsRef.current === socket) wsRef.current = null;
|
|
435
|
+
socketAuthenticatedRef.current = false;
|
|
436
|
+
if (!disposed) {
|
|
437
|
+
if (socketFailedRef.current) setStatus("error");
|
|
438
|
+
else scheduleTransportReconnect();
|
|
439
|
+
}
|
|
151
440
|
};
|
|
152
441
|
|
|
153
442
|
return () => {
|
|
154
443
|
disposed = true;
|
|
155
444
|
wsRef.current = null;
|
|
445
|
+
socketAuthenticatedRef.current = false;
|
|
446
|
+
relayChannelRef.current = null;
|
|
447
|
+
if (reconnectTimer !== null) clearTimeout(reconnectTimer);
|
|
156
448
|
// Drop handlers so an in-flight close/error doesn't mutate state post-unmount.
|
|
157
449
|
socket.onopen = null;
|
|
158
450
|
socket.onmessage = null;
|
|
159
451
|
socket.onerror = null;
|
|
160
452
|
socket.onclose = null;
|
|
161
|
-
|
|
162
|
-
socket.close();
|
|
163
|
-
} catch {
|
|
164
|
-
// ignore teardown errors
|
|
165
|
-
}
|
|
453
|
+
closeSocket(socket);
|
|
166
454
|
};
|
|
167
455
|
// A url/token change (rotation) re-runs this effect → close old, open new.
|
|
168
456
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
169
|
-
}, [transport, url, token]);
|
|
457
|
+
}, [transport, url, token, expiresAt, reconnectGeneration]);
|
|
170
458
|
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
459
|
+
const write = useCallback((data: string) => {
|
|
460
|
+
const currentTransport = transportRef.current;
|
|
461
|
+
const ws = wsRef.current;
|
|
462
|
+
if (ws && ws.readyState === WebSocket.OPEN && socketAuthenticatedRef.current) {
|
|
463
|
+
try {
|
|
464
|
+
const relayChannel = relayChannelRef.current;
|
|
465
|
+
if (currentTransport === "relay-pty" && relayChannel) {
|
|
466
|
+
sendRelayInput(ws, relayChannel, relayInputSequenceRef.current++, data);
|
|
467
|
+
} else {
|
|
176
468
|
ws.send(ttydInputFrame(data));
|
|
177
|
-
} catch {
|
|
178
|
-
// socket raced closed — the reconnect effect will re-establish.
|
|
179
469
|
}
|
|
470
|
+
} catch {
|
|
471
|
+
socketFailedRef.current = false;
|
|
472
|
+
setStatus("connecting");
|
|
473
|
+
closeSocket(ws);
|
|
180
474
|
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
475
|
+
} else if (
|
|
476
|
+
(currentTransport === "pty-ws" || currentTransport === "relay-pty") &&
|
|
477
|
+
!socketFailedRef.current
|
|
478
|
+
) {
|
|
479
|
+
const nextSize = pendingInputRef.current.length + data.length;
|
|
480
|
+
if (nextSize > MAX_PENDING_TERMINAL_INPUT_CODE_UNITS) {
|
|
481
|
+
pendingInputRef.current = "";
|
|
482
|
+
socketFailedRef.current = true;
|
|
483
|
+
setStatus("error");
|
|
484
|
+
closeSocket(ws);
|
|
485
|
+
return;
|
|
192
486
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
487
|
+
pendingInputRef.current += data;
|
|
488
|
+
}
|
|
489
|
+
}, []);
|
|
490
|
+
const resize = useCallback((cols: number, rows: number) => {
|
|
491
|
+
if (cols <= 0 || rows <= 0) return;
|
|
492
|
+
sizeRef.current = { cols, rows };
|
|
493
|
+
const ws = wsRef.current;
|
|
494
|
+
if (ws && ws.readyState === WebSocket.OPEN && socketAuthenticatedRef.current) {
|
|
495
|
+
try {
|
|
496
|
+
// Self-hosted relay PTYs carry raw input/output frames. Resize remains
|
|
497
|
+
// an out-of-band control operation; do not send ttyd framing into them.
|
|
498
|
+
if (transportRef.current === "pty-ws") ws.send(ttydResizeFrame(cols, rows));
|
|
499
|
+
} catch {
|
|
500
|
+
// socket raced closed — geometry is replayed on the next open.
|
|
203
501
|
}
|
|
204
|
-
}
|
|
502
|
+
}
|
|
503
|
+
}, []);
|
|
504
|
+
const disconnect = useCallback(() => {
|
|
505
|
+
const ws = wsRef.current;
|
|
506
|
+
wsRef.current = null;
|
|
507
|
+
pendingInputRef.current = "";
|
|
508
|
+
socketFailedRef.current = false;
|
|
509
|
+
socketAuthenticatedRef.current = false;
|
|
510
|
+
relayChannelRef.current = null;
|
|
511
|
+
setStatus("closed");
|
|
512
|
+
if (ws) intentionalCloseRef.current.add(ws);
|
|
513
|
+
closeSocket(ws);
|
|
514
|
+
}, []);
|
|
515
|
+
|
|
516
|
+
return useMemo<UseTerminalStreamResult>(() => {
|
|
205
517
|
return { connected: status === "open", status, write, resize, disconnect };
|
|
206
|
-
}, [status]);
|
|
518
|
+
}, [status, write, resize, disconnect]);
|
|
207
519
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { VideoArtifactPlaybackLoader } from "../timeline";
|
|
3
|
+
import { useOpenGeni, type ClientOverride } from "../session-context";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bind the current public SDK client/workspace to MessageTimeline's renewable
|
|
7
|
+
* video-source boundary. Proxy hosts may omit the method until they support
|
|
8
|
+
* retained video; the failure remains local to the player instead of loading
|
|
9
|
+
* any video bytes through React.
|
|
10
|
+
*/
|
|
11
|
+
export function useVideoArtifactPlaybackLoader(
|
|
12
|
+
options: ClientOverride = {},
|
|
13
|
+
): VideoArtifactPlaybackLoader {
|
|
14
|
+
const { client, workspaceId } = useOpenGeni(options);
|
|
15
|
+
return useMemo(
|
|
16
|
+
() => async (artifactId: string, signal?: AbortSignal) => {
|
|
17
|
+
if (typeof client.createVideoArtifactPlaybackSource !== "function") {
|
|
18
|
+
throw new Error("This OpenGeni host does not expose retained-video playback.");
|
|
19
|
+
}
|
|
20
|
+
return await client.createVideoArtifactPlaybackSource(
|
|
21
|
+
workspaceId,
|
|
22
|
+
artifactId,
|
|
23
|
+
signal ? { signal } : {},
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
[client, workspaceId],
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
3
|
+
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The wake-on-edit state machine. This hook owns the logic and the UI renders
|
|
@@ -73,7 +74,7 @@ export type UseWorkspaceEditResult = {
|
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
function isLive(liveness: string | undefined): boolean {
|
|
76
|
-
return liveness
|
|
77
|
+
return sandboxAcceptsLiveIo(liveness);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/**
|