@opengeni/react 7.4.0 → 7.5.0-canary.36919050519001
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 +124 -27
- package/dist/artifacts-document.js +3 -2
- package/dist/artifacts-presentation.js +3 -2
- package/dist/artifacts-spreadsheet.js +2 -2
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +516 -1413
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-2E6R2Y4W.js → browser-viewer-Z7S7QEJL.js} +6 -5
- package/dist/chat-interactive-block-UGBTNB3L.js +11 -0
- package/dist/{chunk-Y3QTZN2F.js → chunk-2VFNCX4E.js} +6 -6
- package/dist/{chunk-UDD6DLLT.js → chunk-3IJJEYW4.js} +15 -3
- package/dist/chunk-3IJJEYW4.js.map +1 -0
- package/dist/chunk-4RVYOY4O.js +379 -0
- package/dist/chunk-4RVYOY4O.js.map +1 -0
- package/dist/chunk-5CVXZA3Y.js +292 -0
- package/dist/chunk-5CVXZA3Y.js.map +1 -0
- package/dist/{chunk-5YK2ZDIG.js → chunk-5J4FOW6Q.js} +2 -10
- package/dist/chunk-5J4FOW6Q.js.map +1 -0
- package/dist/{chunk-KVERXCFY.js → chunk-5KYZIQFY.js} +40 -3
- package/dist/chunk-5KYZIQFY.js.map +1 -0
- package/dist/{chunk-KNDIXZN6.js → chunk-7UAMYXJY.js} +25 -18
- package/dist/{chunk-KNDIXZN6.js.map → chunk-7UAMYXJY.js.map} +1 -1
- package/dist/{chunk-SEEVCT36.js → chunk-A4UJB2MM.js} +2 -2
- package/dist/{chunk-X4HIN6TV.js → chunk-AFLPWUQ7.js} +71 -162
- package/dist/chunk-AFLPWUQ7.js.map +1 -0
- package/dist/chunk-B3V6TG77.js +41 -0
- package/dist/chunk-B3V6TG77.js.map +1 -0
- package/dist/{chunk-XEJEOAOV.js → chunk-B7WMS3CS.js} +51 -40
- package/dist/chunk-B7WMS3CS.js.map +1 -0
- package/dist/{chunk-S5JQ4R22.js → chunk-BDKR6BAR.js} +54 -24
- package/dist/chunk-BDKR6BAR.js.map +1 -0
- package/dist/{chunk-4MDLEHXL.js → chunk-DYWBUX5Q.js} +24 -12
- package/dist/chunk-DYWBUX5Q.js.map +1 -0
- package/dist/{chunk-YZ3YYMOM.js → chunk-E3G7SAYZ.js} +252 -624
- package/dist/chunk-E3G7SAYZ.js.map +1 -0
- package/dist/chunk-JT4JW7BC.js +54 -0
- package/dist/chunk-JT4JW7BC.js.map +1 -0
- package/dist/{chunk-5AZSK6MV.js → chunk-LWNLETQT.js} +5 -5
- package/dist/{chunk-USXVQACU.js → chunk-M7LBYP63.js} +5 -4
- package/dist/{chunk-USXVQACU.js.map → chunk-M7LBYP63.js.map} +1 -1
- package/dist/{chunk-L4OSXQ6H.js → chunk-NBOHNRLJ.js} +1289 -1060
- package/dist/chunk-NBOHNRLJ.js.map +1 -0
- package/dist/chunk-RHREZ7BO.js +11 -0
- package/dist/chunk-RHREZ7BO.js.map +1 -0
- package/dist/chunk-SCOTPE2I.js +589 -0
- package/dist/chunk-SCOTPE2I.js.map +1 -0
- package/dist/chunk-WEHMPOXK.js +39 -0
- package/dist/chunk-WEHMPOXK.js.map +1 -0
- package/dist/chunk-WRHIZYZF.js +988 -0
- package/dist/chunk-WRHIZYZF.js.map +1 -0
- package/dist/{chunk-3E2AITTZ.js → chunk-WUISZ3WU.js} +32 -28
- package/dist/{chunk-3E2AITTZ.js.map → chunk-WUISZ3WU.js.map} +1 -1
- package/dist/chunk-XCHWDJ22.js +107 -0
- package/dist/chunk-XCHWDJ22.js.map +1 -0
- package/dist/{chunk-DUEBTL3N.js → chunk-Y2PRMYLC.js} +4 -3
- package/dist/{chunk-DUEBTL3N.js.map → chunk-Y2PRMYLC.js.map} +1 -1
- package/dist/chunk-YBXQDQFX.js +1772 -0
- package/dist/chunk-YBXQDQFX.js.map +1 -0
- package/dist/chunk-YV4L3ILF.js +9 -0
- package/dist/chunk-YV4L3ILF.js.map +1 -0
- package/dist/{chunk-V2ABIVYJ.js → chunk-ZXSEDZP7.js} +70 -6
- package/dist/chunk-ZXSEDZP7.js.map +1 -0
- package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
- package/dist/components/artifacts/artifact-sandbox.d.ts +27 -0
- package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
- package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
- package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
- package/dist/components/artifacts/editable-artifact-view.d.ts +46 -0
- package/dist/components/artifacts/index.d.ts +7 -0
- package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
- package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
- package/dist/components/artifacts/site-view.d.ts +23 -0
- package/dist/components/browser-select-control.d.ts +3 -1
- package/dist/components/code-editor.d.ts +1 -4
- package/dist/components/desktop-viewer.d.ts +1 -1
- package/dist/components/message-timeline.d.ts +20 -3
- package/dist/components/open-geni-links.d.ts +16 -0
- package/dist/components/session-conversation.d.ts +20 -1
- package/dist/components/timeline-pill-geometry.d.ts +8 -0
- package/dist/components/timeline-pill-placement.d.ts +24 -0
- package/dist/components/usage-limit-notice.d.ts +49 -0
- package/dist/components/usage-member-list.d.ts +91 -0
- package/dist/components/usage-meter.d.ts +73 -0
- package/dist/composer.js +5 -5
- package/dist/{computer-viewer-WMJE6OA3.js → computer-viewer-KJWLH3GI.js} +7 -5
- package/dist/connect.js +7 -6
- package/dist/connect.js.map +1 -1
- package/dist/conversation-timeline.d.ts +1 -1
- package/dist/desktop.d.ts +7 -0
- package/dist/desktop.js +26 -0
- package/dist/desktop.js.map +1 -0
- package/dist/editor.d.ts +7 -0
- package/dist/editor.js +21 -0
- package/dist/editor.js.map +1 -0
- package/dist/hooks/latest-question.d.ts +1 -1
- package/dist/hooks/use-composer.d.ts +4 -0
- package/dist/hooks/use-desktop-stream.d.ts +1 -1
- package/dist/hooks/use-usage.d.ts +40 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +460 -1386
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +8 -6
- package/dist/interaction.js.map +1 -1
- package/dist/{latest-question-QB4EUCW5.js → latest-question-IS445HOW.js} +3 -3
- package/dist/lib/format.d.ts +6 -1
- package/dist/lib/workbench-peers.d.ts +36 -0
- package/dist/machines.js +2 -2
- package/dist/{markdown-table-layout-PFUEZ2U4.js → markdown-table-layout-QXSLK4BY.js} +37 -5
- package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
- package/dist/{queue-surface-implementation-IM2HD3SK.js → queue-surface-implementation-2CAQAIHM.js} +2 -2
- package/dist/realtime.js +3 -3
- package/dist/{sandbox-files-PXPIZAFG.js → sandbox-files-AEJTWGT4.js} +8 -5
- package/dist/sandbox-files-AEJTWGT4.js.map +1 -0
- package/dist/session-ui.d.ts +9 -2
- package/dist/session-ui.js +39 -20
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +4 -4
- package/dist/sites.js +4 -289
- package/dist/sites.js.map +1 -1
- package/dist/terminal.d.ts +8 -0
- package/dist/terminal.js +31 -0
- package/dist/terminal.js.map +1 -0
- package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
- package/dist/timeline/genie-loading.d.ts +3 -1
- package/dist/timeline/types.d.ts +8 -1
- package/dist/{timeline-annotation-selection-ODLVF4OI.js → timeline-annotation-selection-DMLMM2TX.js} +6 -1
- package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
- package/dist/timeline-pill-geometry-Z23V243M.js +110 -0
- package/dist/timeline-pill-geometry-Z23V243M.js.map +1 -0
- package/dist/usage/allowance-copy.d.ts +25 -0
- package/dist/usage/summary.d.ts +35 -0
- package/dist/usage.d.ts +13 -0
- package/dist/usage.js +1073 -0
- package/dist/usage.js.map +1 -0
- package/package.json +19 -3
- package/src/artifacts.ts +37 -0
- package/src/components/artifacts/artifact-chrome.tsx +413 -0
- package/src/components/artifacts/artifact-sandbox.tsx +185 -0
- package/src/components/artifacts/chat-interactive-block.tsx +260 -0
- package/src/components/artifacts/deferred-chat-media.tsx +67 -0
- package/src/components/artifacts/editable-artifact-ui.tsx +3 -1
- package/src/components/artifacts/editable-artifact-view.tsx +187 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
- package/src/components/artifacts/editable-document.tsx +34 -36
- package/src/components/artifacts/editable-presentation.tsx +21 -16
- package/src/components/artifacts/index.ts +41 -0
- package/src/components/artifacts/latest-task-runner.ts +49 -0
- package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
- package/src/components/artifacts/site-view.tsx +116 -0
- package/src/components/browser-select-control.tsx +4 -1
- package/src/components/browser-viewer.tsx +81 -22
- package/src/components/code-editor.tsx +13 -16
- package/src/components/computer-viewer.tsx +51 -23
- package/src/components/desktop-viewer.tsx +1 -1
- package/src/components/markdown-table-layout.ts +49 -5
- package/src/components/markdown.tsx +5 -2
- package/src/components/message-timeline.tsx +557 -516
- package/src/components/open-geni-links.tsx +24 -0
- package/src/components/sandbox-terminal.tsx +48 -11
- package/src/components/session-chrome.tsx +22 -3
- package/src/components/session-conversation.tsx +195 -102
- package/src/components/timeline-annotation-selection.tsx +7 -0
- package/src/components/timeline-pill-geometry.ts +169 -0
- package/src/components/timeline-pill-placement.ts +99 -0
- package/src/components/usage-limit-notice.tsx +179 -0
- package/src/components/usage-member-list.tsx +792 -0
- package/src/components/usage-meter.tsx +382 -0
- package/src/conversation-timeline.ts +10 -2
- package/src/desktop.ts +24 -0
- package/src/editor.ts +13 -0
- package/src/hooks/use-available-models.ts +1 -1
- package/src/hooks/use-composer.ts +57 -2
- package/src/hooks/use-desktop-stream.ts +19 -28
- package/src/hooks/use-usage.ts +100 -0
- package/src/index.ts +22 -0
- package/src/lib/format.ts +32 -6
- package/src/lib/workbench-peers.ts +62 -0
- package/src/session-ui.ts +22 -0
- package/src/terminal.ts +26 -0
- package/src/timeline/activity-rail.tsx +5 -3
- package/src/timeline/allowance-exhausted-row.tsx +86 -0
- package/src/timeline/genie-loading.tsx +16 -6
- package/src/timeline/projection.ts +78 -4
- package/src/timeline/types.ts +8 -0
- package/src/usage/allowance-copy.ts +64 -0
- package/src/usage/summary.ts +93 -0
- package/src/usage.ts +50 -0
- package/styles/compiled.css +325 -0
- package/dist/chunk-4MDLEHXL.js.map +0 -1
- package/dist/chunk-5YK2ZDIG.js.map +0 -1
- package/dist/chunk-KVERXCFY.js.map +0 -1
- package/dist/chunk-L4OSXQ6H.js.map +0 -1
- package/dist/chunk-S5JQ4R22.js.map +0 -1
- package/dist/chunk-UDD6DLLT.js.map +0 -1
- package/dist/chunk-UGMKPUEM.js +0 -299
- package/dist/chunk-UGMKPUEM.js.map +0 -1
- package/dist/chunk-V2ABIVYJ.js.map +0 -1
- package/dist/chunk-X4HIN6TV.js.map +0 -1
- package/dist/chunk-XEJEOAOV.js.map +0 -1
- package/dist/chunk-YZ3YYMOM.js.map +0 -1
- package/dist/markdown-table-layout-PFUEZ2U4.js.map +0 -1
- package/dist/timeline-annotation-selection-ODLVF4OI.js.map +0 -1
- /package/dist/{browser-viewer-2E6R2Y4W.js.map → browser-viewer-Z7S7QEJL.js.map} +0 -0
- /package/dist/{computer-viewer-WMJE6OA3.js.map → chat-interactive-block-UGBTNB3L.js.map} +0 -0
- /package/dist/{chunk-Y3QTZN2F.js.map → chunk-2VFNCX4E.js.map} +0 -0
- /package/dist/{chunk-SEEVCT36.js.map → chunk-A4UJB2MM.js.map} +0 -0
- /package/dist/{chunk-5AZSK6MV.js.map → chunk-LWNLETQT.js.map} +0 -0
- /package/dist/{sandbox-files-PXPIZAFG.js.map → computer-viewer-KJWLH3GI.js.map} +0 -0
- /package/dist/{latest-question-QB4EUCW5.js.map → latest-question-IS445HOW.js.map} +0 -0
- /package/dist/{queue-surface-implementation-IM2HD3SK.js.map → queue-surface-implementation-2CAQAIHM.js.map} +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import type { UsageAllowancePeriod, WorkspaceUsageResponse } from "@opengeni/sdk/usage-allowances";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { useUsage, type UsageClientLike } from "../hooks/use-usage";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import {
|
|
7
|
+
allowanceLabels,
|
|
8
|
+
allowanceResetSentence,
|
|
9
|
+
formatAllowanceInstant,
|
|
10
|
+
type AllowanceLabels,
|
|
11
|
+
} from "../usage/allowance-copy";
|
|
12
|
+
import {
|
|
13
|
+
summarizeUsage,
|
|
14
|
+
usagePercentLabel,
|
|
15
|
+
type UsageReading,
|
|
16
|
+
type UsageSummary,
|
|
17
|
+
} from "../usage/summary";
|
|
18
|
+
|
|
19
|
+
export type UsageMeterLabels = AllowanceLabels & {
|
|
20
|
+
/** Heading for the member's own ceiling. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Heading when only the shared workspace pool applies. */
|
|
23
|
+
workspaceTitle: string;
|
|
24
|
+
/** "38% left" — `percent` is preformatted ("38", "<1"). */
|
|
25
|
+
left: (percent: string) => string;
|
|
26
|
+
/** "62% used". */
|
|
27
|
+
used: (percent: string) => string;
|
|
28
|
+
limitReached: string;
|
|
29
|
+
/** Status chip beside a hero meter that is close to its limit. */
|
|
30
|
+
nearLimit: string;
|
|
31
|
+
/** "$13.00 left" for a hero meter with amounts. */
|
|
32
|
+
amountLeft: (amount: string) => string;
|
|
33
|
+
/** No ceiling applies to this member. */
|
|
34
|
+
unlimited: string;
|
|
35
|
+
loading: string;
|
|
36
|
+
error: string;
|
|
37
|
+
retry: string;
|
|
38
|
+
/** "$31.00 of $50.00 used" when the host passes `formatAmount`. */
|
|
39
|
+
amounts: (used: string, limit: string) => string;
|
|
40
|
+
/** The shared pool, under a member's own meter: "Workspace: 9% left". */
|
|
41
|
+
workspaceLine: (value: string) => string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_USAGE_METER_LABELS: UsageMeterLabels = {
|
|
45
|
+
...allowanceLabels(),
|
|
46
|
+
title: "Your usage",
|
|
47
|
+
workspaceTitle: "Workspace usage",
|
|
48
|
+
left: (percent) => `${percent}% left`,
|
|
49
|
+
used: (percent) => `${percent}% used`,
|
|
50
|
+
limitReached: "Limit reached",
|
|
51
|
+
nearLimit: "Near limit",
|
|
52
|
+
amountLeft: (amount) => `${amount} left`,
|
|
53
|
+
unlimited: "No usage limit",
|
|
54
|
+
loading: "Checking usage…",
|
|
55
|
+
error: "Couldn't load usage",
|
|
56
|
+
retry: "Try again",
|
|
57
|
+
amounts: (used, limit) => `${used} of ${limit} used`,
|
|
58
|
+
workspaceLine: (value) => `Workspace: ${value}`,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type UsageMeterBaseProps = {
|
|
62
|
+
/** "left" (default) reads like a battery; "used" reads like spend. */
|
|
63
|
+
measure?: "left" | "used" | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Opt in to amounts. Receives USD micros; return your own unit ("$31.00",
|
|
66
|
+
* "3.1 credits", "1.5× plan"). Without it the meter shows only shares.
|
|
67
|
+
*/
|
|
68
|
+
formatAmount?: ((micros: number) => string) | undefined;
|
|
69
|
+
labels?: Partial<UsageMeterLabels> | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* `compact` is one line for menus and headers; `hero` leads a page with the
|
|
72
|
+
* amount (or share) left in large type. An empty `labels.title` hides the
|
|
73
|
+
* heading when the page already names it.
|
|
74
|
+
*/
|
|
75
|
+
density?: "default" | "compact" | "hero" | undefined;
|
|
76
|
+
/** Show the shared pool under the member's own meter when it is the tighter one. Default true. */
|
|
77
|
+
showWorkspace?: boolean | undefined;
|
|
78
|
+
className?: string | undefined;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type UsageMeterProps = UsageMeterBaseProps & {
|
|
82
|
+
/**
|
|
83
|
+
* A usage response you already loaded (for example from `useUsage`).
|
|
84
|
+
* Omit it to let the meter read `/usage/me` itself.
|
|
85
|
+
*/
|
|
86
|
+
usage?: WorkspaceUsageResponse | null | undefined;
|
|
87
|
+
client?: UsageClientLike | undefined;
|
|
88
|
+
workspaceId?: string | undefined;
|
|
89
|
+
period?: UsageAllowancePeriod | undefined;
|
|
90
|
+
/** Re-read when this changes (see `useUsage`). */
|
|
91
|
+
refreshKey?: unknown;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The signed-in member's own usage: how much of their limit is left and when
|
|
96
|
+
* it resets. Shares only by default; hosts opt in to amounts.
|
|
97
|
+
*/
|
|
98
|
+
export function UsageMeter({
|
|
99
|
+
usage,
|
|
100
|
+
client,
|
|
101
|
+
workspaceId,
|
|
102
|
+
period,
|
|
103
|
+
refreshKey,
|
|
104
|
+
...view
|
|
105
|
+
}: UsageMeterProps) {
|
|
106
|
+
if (usage !== undefined) {
|
|
107
|
+
return (
|
|
108
|
+
<UsageMeterView
|
|
109
|
+
{...view}
|
|
110
|
+
summary={usage ? summarizeUsage(usage) : null}
|
|
111
|
+
loading={usage === null}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
<LoadingUsageMeter
|
|
117
|
+
{...view}
|
|
118
|
+
client={client}
|
|
119
|
+
workspaceId={workspaceId}
|
|
120
|
+
period={period}
|
|
121
|
+
refreshKey={refreshKey}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function LoadingUsageMeter({
|
|
127
|
+
client,
|
|
128
|
+
workspaceId,
|
|
129
|
+
period,
|
|
130
|
+
refreshKey,
|
|
131
|
+
...view
|
|
132
|
+
}: UsageMeterBaseProps &
|
|
133
|
+
Pick<UsageMeterProps, "client" | "workspaceId" | "period" | "refreshKey">) {
|
|
134
|
+
const state = useUsage({ client, workspaceId, period, refreshKey });
|
|
135
|
+
return (
|
|
136
|
+
<UsageMeterView
|
|
137
|
+
{...view}
|
|
138
|
+
summary={state.summary}
|
|
139
|
+
loading={state.loading && !state.summary}
|
|
140
|
+
error={state.summary ? null : state.error}
|
|
141
|
+
onRetry={() => void state.refresh()}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type UsageMeterViewProps = UsageMeterBaseProps & {
|
|
147
|
+
summary: UsageSummary | null;
|
|
148
|
+
loading?: boolean | undefined;
|
|
149
|
+
error?: Error | null | undefined;
|
|
150
|
+
onRetry?: (() => void) | undefined;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const FILL: Record<UsageReading["status"], string> = {
|
|
154
|
+
ok: "bg-og-accent",
|
|
155
|
+
warning: "bg-og-status-waiting",
|
|
156
|
+
exhausted: "bg-og-danger",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
function valueText(
|
|
160
|
+
reading: UsageReading,
|
|
161
|
+
measure: "left" | "used",
|
|
162
|
+
labels: UsageMeterLabels,
|
|
163
|
+
): string {
|
|
164
|
+
if (reading.status === "exhausted") return labels.limitReached;
|
|
165
|
+
return measure === "left"
|
|
166
|
+
? labels.left(usagePercentLabel(Math.max(0, 1 - reading.fraction)))
|
|
167
|
+
: labels.used(usagePercentLabel(reading.fraction));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Presentational meter for a summary you computed (`summarizeUsage`). */
|
|
171
|
+
export function UsageMeterView({
|
|
172
|
+
summary,
|
|
173
|
+
loading = false,
|
|
174
|
+
error = null,
|
|
175
|
+
onRetry,
|
|
176
|
+
measure = "left",
|
|
177
|
+
formatAmount,
|
|
178
|
+
labels: overrides,
|
|
179
|
+
density = "default",
|
|
180
|
+
showWorkspace = true,
|
|
181
|
+
className,
|
|
182
|
+
}: UsageMeterViewProps) {
|
|
183
|
+
const labels = { ...DEFAULT_USAGE_METER_LABELS, ...overrides };
|
|
184
|
+
const compact = density === "compact";
|
|
185
|
+
if (loading || error || !summary) {
|
|
186
|
+
return (
|
|
187
|
+
<div
|
|
188
|
+
className={cn("og-root min-w-0 text-og-sm", className)}
|
|
189
|
+
data-og-usage-meter={error ? "error" : "loading"}
|
|
190
|
+
aria-busy={loading || undefined}
|
|
191
|
+
>
|
|
192
|
+
{error ? (
|
|
193
|
+
<p className="flex flex-wrap items-center gap-x-2 text-og-fg-muted" role="alert">
|
|
194
|
+
{labels.error}
|
|
195
|
+
{onRetry ? (
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
onClick={onRetry}
|
|
199
|
+
className="rounded-og-sm text-og-fg underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-og-accent"
|
|
200
|
+
>
|
|
201
|
+
{labels.retry}
|
|
202
|
+
</button>
|
|
203
|
+
) : null}
|
|
204
|
+
</p>
|
|
205
|
+
) : (
|
|
206
|
+
<span className="text-og-fg-subtle">{labels.loading}</span>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const primary = summary.member ?? summary.workspace;
|
|
212
|
+
if (!primary) {
|
|
213
|
+
return (
|
|
214
|
+
<div
|
|
215
|
+
className={cn("og-root min-w-0 text-og-sm text-og-fg-muted", className)}
|
|
216
|
+
data-og-usage-meter="unlimited"
|
|
217
|
+
>
|
|
218
|
+
{compact ? null : <span className="font-medium text-og-fg">{labels.title} · </span>}
|
|
219
|
+
{labels.unlimited}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const title = summary.member ? labels.title : labels.workspaceTitle;
|
|
224
|
+
const value = valueText(primary, measure, labels);
|
|
225
|
+
// The bar always fills with what's used, so "near the limit" reads as a
|
|
226
|
+
// nearly full bar whichever way the words count.
|
|
227
|
+
const fill = Math.min(1, Math.max(0, primary.fraction));
|
|
228
|
+
const reset = allowanceResetSentence(labels, primary.resetsAt);
|
|
229
|
+
const pool =
|
|
230
|
+
showWorkspace &&
|
|
231
|
+
summary.member &&
|
|
232
|
+
summary.workspace &&
|
|
233
|
+
(summary.binding?.scope === "workspace" || summary.workspace.status !== "ok")
|
|
234
|
+
? summary.workspace
|
|
235
|
+
: null;
|
|
236
|
+
const meterProps = {
|
|
237
|
+
role: "meter" as const,
|
|
238
|
+
"aria-label": title || labels.title,
|
|
239
|
+
"aria-valuemin": 0,
|
|
240
|
+
"aria-valuemax": 100,
|
|
241
|
+
"aria-valuenow": Math.round(fill * 100),
|
|
242
|
+
"aria-valuetext": `${value}. ${reset}${
|
|
243
|
+
pool ? ` ${labels.workspaceLine(valueText(pool, measure, labels))}.` : ""
|
|
244
|
+
}`,
|
|
245
|
+
};
|
|
246
|
+
const bar = (
|
|
247
|
+
<span
|
|
248
|
+
aria-hidden
|
|
249
|
+
className={cn(
|
|
250
|
+
"relative block overflow-hidden rounded-full",
|
|
251
|
+
density === "hero" ? "h-1.5" : "h-1",
|
|
252
|
+
primary.status === "exhausted" ? "bg-og-danger/15" : "bg-og-surface-3",
|
|
253
|
+
compact ? "w-16 shrink-0" : "w-full",
|
|
254
|
+
)}
|
|
255
|
+
>
|
|
256
|
+
<span
|
|
257
|
+
className={cn("absolute inset-y-0 left-0 rounded-full", FILL[primary.status])}
|
|
258
|
+
style={{ width: `${Math.round(fill * 1000) / 10}%` }}
|
|
259
|
+
/>
|
|
260
|
+
</span>
|
|
261
|
+
);
|
|
262
|
+
const valueClass = cn(
|
|
263
|
+
"shrink-0 whitespace-nowrap tabular-nums",
|
|
264
|
+
primary.status === "ok" ? "text-og-fg-muted" : "font-medium",
|
|
265
|
+
primary.status === "warning" && "text-og-status-waiting",
|
|
266
|
+
primary.status === "exhausted" && "text-og-danger",
|
|
267
|
+
);
|
|
268
|
+
if (compact) {
|
|
269
|
+
return (
|
|
270
|
+
<span
|
|
271
|
+
{...meterProps}
|
|
272
|
+
data-og-usage-meter={primary.status}
|
|
273
|
+
title={primary.resetsAt ? `${reset} ${formatAllowanceInstant(primary.resetsAt)}` : reset}
|
|
274
|
+
className={cn("og-root inline-flex min-w-0 items-center gap-2 text-og-xs", className)}
|
|
275
|
+
>
|
|
276
|
+
<span className={valueClass}>{value}</span>
|
|
277
|
+
{bar}
|
|
278
|
+
</span>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const amounts = formatAmount
|
|
282
|
+
? labels.amounts(formatAmount(primary.used), formatAmount(primary.limit))
|
|
283
|
+
: null;
|
|
284
|
+
if (density === "hero") {
|
|
285
|
+
// Lead with what's left, even at zero; the chip names the state.
|
|
286
|
+
const headline = formatAmount
|
|
287
|
+
? labels.amountLeft(formatAmount(primary.remaining))
|
|
288
|
+
: labels.left(usagePercentLabel(Math.max(0, 1 - primary.fraction)));
|
|
289
|
+
return (
|
|
290
|
+
<div
|
|
291
|
+
{...meterProps}
|
|
292
|
+
data-og-usage-meter={primary.status}
|
|
293
|
+
className={cn("og-root flex min-w-0 flex-col gap-3", className)}
|
|
294
|
+
>
|
|
295
|
+
{title ? <span className="text-og-sm font-medium text-og-fg-muted">{title}</span> : null}
|
|
296
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
|
|
297
|
+
<span className="text-2xl leading-8 font-semibold tracking-[-0.5px] text-og-fg tabular-nums">
|
|
298
|
+
{headline}
|
|
299
|
+
</span>
|
|
300
|
+
{primary.status !== "ok" ? (
|
|
301
|
+
<span
|
|
302
|
+
className={cn(
|
|
303
|
+
"inline-flex h-[22px] items-center gap-1.5 rounded-full border px-2 text-og-xs font-medium",
|
|
304
|
+
primary.status === "warning"
|
|
305
|
+
? "border-og-status-waiting/30 text-og-status-waiting"
|
|
306
|
+
: "border-og-danger/30 text-og-danger",
|
|
307
|
+
)}
|
|
308
|
+
>
|
|
309
|
+
<span
|
|
310
|
+
aria-hidden
|
|
311
|
+
className={cn(
|
|
312
|
+
"size-1.5 rounded-full",
|
|
313
|
+
primary.status === "warning" ? "bg-og-status-waiting" : "bg-og-danger",
|
|
314
|
+
)}
|
|
315
|
+
/>
|
|
316
|
+
{primary.status === "warning" ? labels.nearLimit : labels.limitReached}
|
|
317
|
+
</span>
|
|
318
|
+
) : null}
|
|
319
|
+
</div>
|
|
320
|
+
{bar}
|
|
321
|
+
<p className="text-og-xs text-og-fg-muted tabular-nums">
|
|
322
|
+
{[
|
|
323
|
+
amounts ?? labels.used(usagePercentLabel(primary.fraction)),
|
|
324
|
+
amounts ? `${usagePercentLabel(primary.fraction)}%` : null,
|
|
325
|
+
]
|
|
326
|
+
.filter(Boolean)
|
|
327
|
+
.join(" · ")}{" "}
|
|
328
|
+
·{" "}
|
|
329
|
+
<span title={primary.resetsAt ? formatAllowanceInstant(primary.resetsAt) : undefined}>
|
|
330
|
+
{reset}
|
|
331
|
+
</span>
|
|
332
|
+
</p>
|
|
333
|
+
{pool ? <PoolLine reading={pool} measure={measure} labels={labels} /> : null}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return (
|
|
338
|
+
<div
|
|
339
|
+
{...meterProps}
|
|
340
|
+
data-og-usage-meter={primary.status}
|
|
341
|
+
className={cn("og-root flex min-w-0 flex-col gap-2 text-og-sm", className)}
|
|
342
|
+
>
|
|
343
|
+
<div className="flex min-w-0 items-baseline justify-between gap-3">
|
|
344
|
+
<span className="min-w-0 truncate font-medium text-og-fg">{title}</span>
|
|
345
|
+
<span className={valueClass}>{value}</span>
|
|
346
|
+
</div>
|
|
347
|
+
{bar}
|
|
348
|
+
<div className="flex min-w-0 flex-wrap items-baseline justify-between gap-x-3 gap-y-1 text-og-xs text-og-fg-subtle">
|
|
349
|
+
<span title={primary.resetsAt ? formatAllowanceInstant(primary.resetsAt) : undefined}>
|
|
350
|
+
{reset}
|
|
351
|
+
</span>
|
|
352
|
+
{amounts ? <span className="tabular-nums">{amounts}</span> : null}
|
|
353
|
+
</div>
|
|
354
|
+
{pool ? <PoolLine reading={pool} measure={measure} labels={labels} /> : null}
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function PoolLine({
|
|
360
|
+
reading,
|
|
361
|
+
measure,
|
|
362
|
+
labels,
|
|
363
|
+
}: {
|
|
364
|
+
reading: UsageReading;
|
|
365
|
+
measure: "left" | "used";
|
|
366
|
+
labels: UsageMeterLabels;
|
|
367
|
+
}): ReactNode {
|
|
368
|
+
return (
|
|
369
|
+
<p
|
|
370
|
+
className={cn(
|
|
371
|
+
"text-og-xs",
|
|
372
|
+
reading.status === "exhausted"
|
|
373
|
+
? "font-medium text-og-danger"
|
|
374
|
+
: reading.status === "warning"
|
|
375
|
+
? "text-og-status-waiting"
|
|
376
|
+
: "text-og-fg-muted",
|
|
377
|
+
)}
|
|
378
|
+
>
|
|
379
|
+
{labels.workspaceLine(valueText(reading, measure, labels))}
|
|
380
|
+
</p>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
@@ -8,7 +8,10 @@ export function conversationTimeline(
|
|
|
8
8
|
queue: Pick<UseTurnQueueResult, "queue" | "snapshot" | "acceptedSteers">,
|
|
9
9
|
composer: Pick<
|
|
10
10
|
ComposerState,
|
|
11
|
-
|
|
11
|
+
| "optimisticMessages"
|
|
12
|
+
| "retryOptimisticMessage"
|
|
13
|
+
| "restoreOptimisticMessage"
|
|
14
|
+
| "removeOptimisticMessage"
|
|
12
15
|
>,
|
|
13
16
|
): TimelineItem[] {
|
|
14
17
|
const queued = new Set(
|
|
@@ -61,7 +64,12 @@ export function conversationTimeline(
|
|
|
61
64
|
...(message.error ? { error: message.error } : {}),
|
|
62
65
|
...(message.state === "failed"
|
|
63
66
|
? {
|
|
64
|
-
|
|
67
|
+
...(message.retryable !== false && composer.retryOptimisticMessage
|
|
68
|
+
? { onRetry: () => composer.retryOptimisticMessage?.(message.clientEventId) }
|
|
69
|
+
: {}),
|
|
70
|
+
...(message.retryable === false && composer.restoreOptimisticMessage
|
|
71
|
+
? { onEdit: () => composer.restoreOptimisticMessage?.(message.clientEventId) }
|
|
72
|
+
: {}),
|
|
65
73
|
onRemove: () => composer.removeOptimisticMessage?.(message.clientEventId),
|
|
66
74
|
}
|
|
67
75
|
: {}),
|
package/src/desktop.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DesktopRfbFactory, DesktopRfbLike } from "@opengeni/sdk";
|
|
2
|
+
import { registerDesktopViewer } from "./lib/workbench-peers";
|
|
3
|
+
|
|
4
|
+
export { DesktopViewer } from "./components/desktop-viewer";
|
|
5
|
+
export type { DesktopViewerProps } from "./components/desktop-viewer";
|
|
6
|
+
export { useDesktopStream } from "./hooks/use-desktop-stream";
|
|
7
|
+
export type { UseDesktopStreamOptions, UseDesktopStreamResult } from "./hooks/use-desktop-stream";
|
|
8
|
+
export { registerDesktopViewer } from "./lib/workbench-peers";
|
|
9
|
+
|
|
10
|
+
async function loadDesktop(): Promise<DesktopRfbFactory> {
|
|
11
|
+
const mod = (await import("@novnc/novnc")) as unknown as {
|
|
12
|
+
default: new (
|
|
13
|
+
target: HTMLElement,
|
|
14
|
+
url: string,
|
|
15
|
+
options: Parameters<DesktopRfbFactory>[2],
|
|
16
|
+
) => DesktopRfbLike;
|
|
17
|
+
};
|
|
18
|
+
return (target, url, options) => new mod.default(target, url, options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Opt in once on a VNC route; relay-frame viewers need no noVNC peer. */
|
|
22
|
+
export function enableDesktopViewer(): void {
|
|
23
|
+
registerDesktopViewer(loadDesktop);
|
|
24
|
+
}
|
package/src/editor.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { registerCodeEditor, type CodeEditorLanguages } from "./lib/workbench-peers";
|
|
2
|
+
|
|
3
|
+
export { CodeEditor, languageForPath } from "./components/code-editor";
|
|
4
|
+
export type { CodeEditorProps } from "./components/code-editor";
|
|
5
|
+
export { registerCodeEditor } from "./lib/workbench-peers";
|
|
6
|
+
export type { CodeEditorLanguage, CodeEditorLanguages } from "./lib/workbench-peers";
|
|
7
|
+
|
|
8
|
+
const loadEditor = async () => ({ module: await import("@uiw/react-codemirror") });
|
|
9
|
+
|
|
10
|
+
/** Only name installed grammar peers in host-supplied language loaders. */
|
|
11
|
+
export function enableCodeEditor(languages?: CodeEditorLanguages): void {
|
|
12
|
+
registerCodeEditor(languages ? async () => ({ ...(await loadEditor()), languages }) : loadEditor);
|
|
13
|
+
}
|
|
@@ -72,7 +72,7 @@ export function useWorkspaceModelCatalog(
|
|
|
72
72
|
}
|
|
73
73
|
const [catalog, config] = await Promise.all([
|
|
74
74
|
client.getWorkspaceModelCatalog(options.workspaceId, { signal }),
|
|
75
|
-
client.getClientConfig({ signal }),
|
|
75
|
+
client.getClientConfig({ signal, workspaceId: options.workspaceId }),
|
|
76
76
|
]);
|
|
77
77
|
return {
|
|
78
78
|
models: catalog.models,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "@opengeni/sdk";
|
|
16
16
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
17
17
|
import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
|
|
18
|
+
import { composerSubmissionCanRetry, composerSubmissionErrorMessage } from "../lib/format";
|
|
18
19
|
import { useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
|
|
19
20
|
|
|
20
21
|
export type ComposerPolicy = {
|
|
@@ -97,6 +98,8 @@ export type ComposerOptimisticMessage = {
|
|
|
97
98
|
triggerEventId?: string | null | undefined;
|
|
98
99
|
appliedQueueVersion?: number | null | undefined;
|
|
99
100
|
error?: string | undefined;
|
|
101
|
+
/** False for a definitive refusal that requires changing payment or allowance settings. */
|
|
102
|
+
retryable?: boolean | undefined;
|
|
100
103
|
outcomeUnknown?: boolean | undefined;
|
|
101
104
|
};
|
|
102
105
|
|
|
@@ -185,7 +188,14 @@ function restoreOptimisticSendOperations(key: string | null): OptimisticSendOper
|
|
|
185
188
|
error:
|
|
186
189
|
operation.state === "sending"
|
|
187
190
|
? "Delivery was interrupted; retry to reconcile this message."
|
|
188
|
-
: operation.error
|
|
191
|
+
: operation.error
|
|
192
|
+
? composerSubmissionErrorMessage(new Error(operation.error))
|
|
193
|
+
: undefined,
|
|
194
|
+
retryable:
|
|
195
|
+
operation.state === "sending" || operation.outcomeUnknown
|
|
196
|
+
? true
|
|
197
|
+
: (operation.retryable ??
|
|
198
|
+
(operation.error ? composerSubmissionCanRetry(new Error(operation.error)) : true)),
|
|
189
199
|
input: operation.input,
|
|
190
200
|
canRetry: !hasMcpCredentialUpdates,
|
|
191
201
|
}));
|
|
@@ -433,6 +443,8 @@ export type ComposerState = {
|
|
|
433
443
|
/** Locally acknowledged ordinary sends awaiting durable timeline reconciliation. */
|
|
434
444
|
optimisticMessages?: ComposerOptimisticMessage[] | undefined;
|
|
435
445
|
retryOptimisticMessage?: ((clientEventId: string) => void) | undefined;
|
|
446
|
+
/** Move a definitively refused message into an empty composer without resending it. */
|
|
447
|
+
restoreOptimisticMessage?: ((clientEventId: string) => void) | undefined;
|
|
436
448
|
removeOptimisticMessage?: ((clientEventId: string) => void) | undefined;
|
|
437
449
|
/** Supersede current direction with the draft. */
|
|
438
450
|
steer: (text?: string) => Promise<boolean>;
|
|
@@ -1379,7 +1391,8 @@ export function useComposer(
|
|
|
1379
1391
|
? {
|
|
1380
1392
|
...candidate,
|
|
1381
1393
|
state: "failed",
|
|
1382
|
-
error: problem.message,
|
|
1394
|
+
error: outcomeUnknown ? problem.message : composerSubmissionErrorMessage(problem),
|
|
1395
|
+
retryable: outcomeUnknown || composerSubmissionCanRetry(problem),
|
|
1383
1396
|
outcomeUnknown,
|
|
1384
1397
|
}
|
|
1385
1398
|
: candidate,
|
|
@@ -2138,6 +2151,7 @@ export function useComposer(
|
|
|
2138
2151
|
if (operation.outcomeUnknown) {
|
|
2139
2152
|
return { ...operation, state: "sending", error: undefined };
|
|
2140
2153
|
}
|
|
2154
|
+
if (operation.retryable === false) return operation;
|
|
2141
2155
|
const nextClientEventId = generateClientEventId();
|
|
2142
2156
|
const currentPersonalResourceAttachment = resolveSendExtras(
|
|
2143
2157
|
sendExtrasRef.current,
|
|
@@ -2167,6 +2181,7 @@ export function useComposer(
|
|
|
2167
2181
|
occurredAt: new Date().toISOString(),
|
|
2168
2182
|
state: "sending",
|
|
2169
2183
|
error: undefined,
|
|
2184
|
+
retryable: undefined,
|
|
2170
2185
|
outcomeUnknown: false,
|
|
2171
2186
|
canRetry: true,
|
|
2172
2187
|
};
|
|
@@ -2573,6 +2588,42 @@ export function useComposer(
|
|
|
2573
2588
|
);
|
|
2574
2589
|
}, []);
|
|
2575
2590
|
|
|
2591
|
+
const restoreOptimisticMessage = useCallback(
|
|
2592
|
+
(clientEventId: string): void => {
|
|
2593
|
+
if (
|
|
2594
|
+
targetKeyRef.current !== targetKey ||
|
|
2595
|
+
draftLoading ||
|
|
2596
|
+
pendingOperationRef.current ||
|
|
2597
|
+
hasDraftContent()
|
|
2598
|
+
)
|
|
2599
|
+
return;
|
|
2600
|
+
const operation = optimisticSendsRef.current.find(
|
|
2601
|
+
(candidate) => candidate.clientEventId === clientEventId,
|
|
2602
|
+
);
|
|
2603
|
+
if (!operation || operation.state !== "failed" || operation.outcomeUnknown) return;
|
|
2604
|
+
// Restoring is a new draft edit, not a retry. Keep the user's current model
|
|
2605
|
+
// selection and never copy credentials from the old rejected request.
|
|
2606
|
+
localEditRevision.current += 1;
|
|
2607
|
+
valueRef.current = operation.text;
|
|
2608
|
+
restoredResourcesRef.current = [...operation.resources];
|
|
2609
|
+
annotationsRef.current = cloneAnnotations(operation.annotations);
|
|
2610
|
+
setValue(operation.text);
|
|
2611
|
+
setRestoredResources(restoredResourcesRef.current);
|
|
2612
|
+
setAnnotations(annotationsRef.current);
|
|
2613
|
+
setError(null);
|
|
2614
|
+
setOptimisticDraftShadow({
|
|
2615
|
+
text: operation.text,
|
|
2616
|
+
resources: restoredResourcesRef.current,
|
|
2617
|
+
annotations: annotationsRef.current,
|
|
2618
|
+
policy: policyRef.current ?? undefined,
|
|
2619
|
+
});
|
|
2620
|
+
replaceOptimisticSends((current) =>
|
|
2621
|
+
current.filter((candidate) => candidate.clientEventId !== clientEventId),
|
|
2622
|
+
);
|
|
2623
|
+
},
|
|
2624
|
+
[draftLoading, hasDraftContent, replaceOptimisticSends, setOptimisticDraftShadow, targetKey],
|
|
2625
|
+
);
|
|
2626
|
+
|
|
2576
2627
|
const resolveDraftConflict = useCallback(
|
|
2577
2628
|
async (choice: "keep_mine" | "use_remote"): Promise<void> => {
|
|
2578
2629
|
const ownedTargetKey = targetKey;
|
|
@@ -2676,6 +2727,10 @@ export function useComposer(
|
|
|
2676
2727
|
send,
|
|
2677
2728
|
optimisticMessages: visibleOptimisticMessages,
|
|
2678
2729
|
retryOptimisticMessage,
|
|
2730
|
+
restoreOptimisticMessage:
|
|
2731
|
+
!draftLoading && !pendingOperationRef.current && !hasDraftContent()
|
|
2732
|
+
? restoreOptimisticMessage
|
|
2733
|
+
: undefined,
|
|
2679
2734
|
removeOptimisticMessage,
|
|
2680
2735
|
steer,
|
|
2681
2736
|
steering: visibleSteering,
|
|
@@ -6,7 +6,15 @@ import {
|
|
|
6
6
|
type DesktopRfbLike,
|
|
7
7
|
type DesktopStreamCapability,
|
|
8
8
|
} from "@opengeni/sdk";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type RefObject,
|
|
11
|
+
useCallback,
|
|
12
|
+
useEffect,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
useSyncExternalStore,
|
|
16
|
+
} from "react";
|
|
17
|
+
import { desktopViewerPeers } from "../lib/workbench-peers";
|
|
10
18
|
import { type DesktopWebSocketFactory, useRelayFrameStream } from "./use-relay-frame-stream";
|
|
11
19
|
|
|
12
20
|
export type UseDesktopStreamOptions = {
|
|
@@ -17,7 +25,7 @@ export type UseDesktopStreamOptions = {
|
|
|
17
25
|
/** Read-only by default (v1 ruling H). interactive only when cap.mode allows. */
|
|
18
26
|
interactive?: boolean | undefined;
|
|
19
27
|
scaleViewport?: boolean | undefined;
|
|
20
|
-
/** Custom RFB factory
|
|
28
|
+
/** Custom RFB factory. Otherwise call enableDesktopViewer() on the desktop subpath. */
|
|
21
29
|
rfbFactory?: DesktopRfbFactory | undefined;
|
|
22
30
|
/** Additional WebSocket protocols for an authenticated placement proxy. */
|
|
23
31
|
webSocketProtocols?: string[] | undefined;
|
|
@@ -35,29 +43,6 @@ export type UseDesktopStreamResult = {
|
|
|
35
43
|
sendKey?: ((keysym: number, code: string, down?: boolean) => boolean) | undefined;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
|
-
/** Lazy-load @novnc/novnc's RFB as the default factory. Imported inside the
|
|
39
|
-
* connect effect so SSR / non-desktop bundles never pull the DOM-only lib.
|
|
40
|
-
* @novnc/novnc ships no types and a single `export default class RFB`. The
|
|
41
|
-
* specifier is STATIC (`import("@novnc/novnc")`) so Vite can pre-bundle and
|
|
42
|
-
* resolve it — a runtime-string indirection with `@vite-ignore` (the previous
|
|
43
|
-
* approach) hands the browser a bare specifier and throws
|
|
44
|
-
* "Failed to resolve module specifier '@novnc/novnc'". The dynamic form keeps
|
|
45
|
-
* it out of the SSR / non-desktop critical path while staying resolvable. */
|
|
46
|
-
async function defaultRfbFactory(): Promise<DesktopRfbFactory> {
|
|
47
|
-
const mod = (await import("@novnc/novnc")) as unknown as {
|
|
48
|
-
default: new (
|
|
49
|
-
t: HTMLElement,
|
|
50
|
-
u: string,
|
|
51
|
-
o: {
|
|
52
|
-
credentials?: { password?: string | undefined } | undefined;
|
|
53
|
-
wsProtocols?: string[] | undefined;
|
|
54
|
-
},
|
|
55
|
-
) => DesktopRfbLike;
|
|
56
|
-
};
|
|
57
|
-
const RFB = mod.default;
|
|
58
|
-
return (target, url, opts) => new RFB(target, url, opts);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
46
|
/**
|
|
62
47
|
* Drive the noVNC RFB lifecycle from a `DesktopStreamCapability`, using the
|
|
63
48
|
* SDK's `desktop.ts` reducer + `desktopSocketUrl`. SSR-safe: the RFB import and
|
|
@@ -87,6 +72,12 @@ export function useDesktopStream(options: UseDesktopStreamOptions): UseDesktopSt
|
|
|
87
72
|
webSocketProtocols,
|
|
88
73
|
webSocketFactory,
|
|
89
74
|
} = options;
|
|
75
|
+
const peerRevision = useSyncExternalStore(
|
|
76
|
+
desktopViewerPeers.subscribe,
|
|
77
|
+
desktopViewerPeers.revision,
|
|
78
|
+
desktopViewerPeers.revision,
|
|
79
|
+
);
|
|
80
|
+
const factoryRevision = rfbFactory ? 0 : peerRevision;
|
|
90
81
|
|
|
91
82
|
// The self-hosted PNG-frame path. Always invoked (rules of hooks); dormant
|
|
92
83
|
// unless `transport === "relay-frames"`, in which case it owns the surface.
|
|
@@ -188,7 +179,7 @@ export function useDesktopStream(options: UseDesktopStreamOptions): UseDesktopSt
|
|
|
188
179
|
|
|
189
180
|
void (async () => {
|
|
190
181
|
try {
|
|
191
|
-
const factory = rfbFactoryRef.current ?? (await
|
|
182
|
+
const factory = rfbFactoryRef.current ?? (await desktopViewerPeers.load());
|
|
192
183
|
if (disposed) return;
|
|
193
184
|
const socketUrl = desktopSocketUrl({ url });
|
|
194
185
|
setBoth(nextDesktopState(stateRef.current, { type: "negotiated" }));
|
|
@@ -232,11 +223,11 @@ export function useDesktopStream(options: UseDesktopStreamOptions): UseDesktopSt
|
|
|
232
223
|
};
|
|
233
224
|
// ONLY a real transport change reconnects: a fresh url (rotation), a new
|
|
234
225
|
// credential (RFB password OR bearer subprotocol), the transport flipping,
|
|
235
|
-
// or an explicit manual reconnect
|
|
226
|
+
// a newly registered default peer loader, or an explicit manual reconnect
|
|
236
227
|
// (`nonce`). `interactive`/`scaleViewport`/`mode`/`rfbFactory` are read via
|
|
237
228
|
// refs and applied live below — they must never re-open the socket.
|
|
238
229
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
239
|
-
}, [url, token, transport, webSocketProtocolsKey, nonce]);
|
|
230
|
+
}, [url, token, transport, webSocketProtocolsKey, nonce, factoryRevision]);
|
|
240
231
|
|
|
241
232
|
// Apply take-control / return-control to the OPEN connection in place. noVNC
|
|
242
233
|
// honours `viewOnly` live, so flipping it neither blinks the surface nor drops
|