@opengeni/react 4.0.2 → 5.0.4-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +42 -0
  2. package/dist/artifacts.js +24 -285
  3. package/dist/artifacts.js.map +1 -1
  4. package/dist/chunk-BRFMGY4L.js +274 -0
  5. package/dist/chunk-BRFMGY4L.js.map +1 -0
  6. package/dist/chunk-IWIDV7SB.js +1 -0
  7. package/dist/chunk-IWIDV7SB.js.map +1 -0
  8. package/dist/{chunk-CQG7YUJB.js → chunk-SCZ6SUBW.js} +1177 -1021
  9. package/dist/chunk-SCZ6SUBW.js.map +1 -0
  10. package/dist/clipboard.js +1 -0
  11. package/dist/components/markdown-table-layout.d.ts +4 -1
  12. package/dist/components/message-timeline.d.ts +7 -3
  13. package/dist/connect-accounts.d.ts +10 -0
  14. package/dist/connect-chooser.d.ts +11 -0
  15. package/dist/connect-panel.d.ts +10 -0
  16. package/dist/connect-setup.d.ts +12 -0
  17. package/dist/connect.d.ts +30 -0
  18. package/dist/connect.js +831 -0
  19. package/dist/connect.js.map +1 -0
  20. package/dist/device-authorization.d.ts +15 -0
  21. package/dist/identity-link-accounts.d.ts +11 -0
  22. package/dist/identity-link-consent.d.ts +18 -0
  23. package/dist/index.js +523 -663
  24. package/dist/index.js.map +1 -1
  25. package/dist/{markdown-table-layout-3Z3AWBY7.js → markdown-table-layout-PFUEZ2U4.js} +7 -4
  26. package/dist/{markdown-table-layout-3Z3AWBY7.js.map → markdown-table-layout-PFUEZ2U4.js.map} +1 -1
  27. package/dist/session-ui.d.ts +2 -0
  28. package/dist/session-ui.js +3 -1
  29. package/dist/sites-ui.d.ts +33 -0
  30. package/dist/sites.d.ts +3 -0
  31. package/dist/sites.js +288 -0
  32. package/dist/sites.js.map +1 -0
  33. package/package.json +16 -2
  34. package/src/components/markdown-table-layout.ts +6 -3
  35. package/src/components/markdown.tsx +11 -3
  36. package/src/components/message-timeline.tsx +22 -5
  37. package/src/components/sandbox-workspace.tsx +9 -2
  38. package/src/connect-accounts.tsx +171 -0
  39. package/src/connect-chooser.tsx +144 -0
  40. package/src/connect-panel.tsx +29 -0
  41. package/src/connect-setup.tsx +260 -0
  42. package/src/connect.ts +48 -0
  43. package/src/device-authorization.tsx +90 -0
  44. package/src/hooks/use-workspace-capture.ts +3 -2
  45. package/src/identity-link-accounts.tsx +144 -0
  46. package/src/identity-link-consent.tsx +192 -0
  47. package/src/session-ui.ts +2 -0
  48. package/src/sites-ui.tsx +385 -0
  49. package/src/sites.ts +3 -0
  50. package/src/timeline/tool-renderers.tsx +1 -1
  51. package/styles/connect.css +48 -0
  52. package/styles/connect.d.ts +1 -0
  53. package/dist/chunk-CQG7YUJB.js.map +0 -1
@@ -157,6 +157,8 @@ export type MessageTimelineProps = {
157
157
  * a Reconnect button without a handler to run it.
158
158
  */
159
159
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
160
+ /** Host-owned inline connection setup. Return undefined to use the default recovery card. */
161
+ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
160
162
  /**
161
163
  * Decide which durable authentication notices this timeline presents.
162
164
  * Defaults to showing every notice. Embedded hosts can suppress notices for
@@ -407,6 +409,7 @@ export function MessageTimeline({
407
409
  onOpenSession,
408
410
  onMemoryClick,
409
411
  onReconnect,
412
+ renderAuthNeeded,
410
413
  shouldRenderAuthNeeded,
411
414
  resolveProviderLogo,
412
415
  toolRegistry = defaultToolRegistry,
@@ -910,6 +913,7 @@ export function MessageTimeline({
910
913
  onOpenSession,
911
914
  onMemoryClick,
912
915
  onReconnect,
916
+ renderAuthNeeded,
913
917
  resolveProviderLogo,
914
918
  toolRegistry,
915
919
  loadRetainedScreenshot,
@@ -925,6 +929,7 @@ export function MessageTimeline({
925
929
  onMemoryClick,
926
930
  onOpenSession,
927
931
  onReconnect,
932
+ renderAuthNeeded,
928
933
  renderMessageActions,
929
934
  renderMessageText,
930
935
  resolveProviderLogo,
@@ -2309,6 +2314,7 @@ type TimelineGroupBehaviorProps = {
2309
2314
  onOpenSession: MessageTimelineProps["onOpenSession"];
2310
2315
  onMemoryClick: MessageTimelineProps["onMemoryClick"];
2311
2316
  onReconnect: MessageTimelineProps["onReconnect"];
2317
+ renderAuthNeeded: MessageTimelineProps["renderAuthNeeded"];
2312
2318
  resolveProviderLogo: MessageTimelineProps["resolveProviderLogo"];
2313
2319
  toolRegistry: ToolRegistry;
2314
2320
  loadRetainedScreenshot: MessageTimelineProps["loadRetainedScreenshot"];
@@ -2373,6 +2379,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2373
2379
  onOpenSession,
2374
2380
  onMemoryClick,
2375
2381
  onReconnect,
2382
+ renderAuthNeeded,
2376
2383
  resolveProviderLogo,
2377
2384
  toolRegistry,
2378
2385
  loadRetainedScreenshot,
@@ -2393,6 +2400,8 @@ const TimelineGroupView = memo(function TimelineGroupView({
2393
2400
  onOpenSession?: ((sessionId: string) => void) | undefined;
2394
2401
  onMemoryClick?: ((memoryId: string) => void) | undefined;
2395
2402
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
2403
+ /** Host-owned inline connection setup. Return undefined to use the default recovery card. */
2404
+ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
2396
2405
  resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
2397
2406
  toolRegistry: ToolRegistry;
2398
2407
  loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
@@ -2550,6 +2559,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2550
2559
  onOpenSession,
2551
2560
  onMemoryClick,
2552
2561
  onReconnect,
2562
+ renderAuthNeeded,
2553
2563
  resolveProviderLogo,
2554
2564
  toolRegistry,
2555
2565
  loadRetainedScreenshot,
@@ -2565,6 +2575,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2565
2575
  onOpenSession={onOpenSession}
2566
2576
  onMemoryClick={onMemoryClick}
2567
2577
  onReconnect={onReconnect}
2578
+ renderAuthNeeded={renderAuthNeeded}
2568
2579
  resolveProviderLogo={resolveProviderLogo}
2569
2580
  toolRegistry={toolRegistry}
2570
2581
  loadRetainedScreenshot={loadRetainedScreenshot}
@@ -2610,6 +2621,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2610
2621
  renderMessageActions={renderMessageActions}
2611
2622
  renderMessageText={renderMessageText}
2612
2623
  onReconnect={onReconnect}
2624
+ renderAuthNeeded={renderAuthNeeded}
2613
2625
  resolveProviderLogo={resolveProviderLogo}
2614
2626
  onOpenSession={onOpenSession}
2615
2627
  loadVideoArtifactPlayback={loadVideoArtifactPlayback}
@@ -2862,6 +2874,7 @@ export function TimelineRow({
2862
2874
  renderMessageActions,
2863
2875
  renderMessageText,
2864
2876
  onReconnect,
2877
+ renderAuthNeeded,
2865
2878
  resolveProviderLogo,
2866
2879
  onOpenSession,
2867
2880
  loadVideoArtifactPlayback,
@@ -2872,6 +2885,8 @@ export function TimelineRow({
2872
2885
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2873
2886
  | undefined;
2874
2887
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
2888
+ /** Host-owned inline connection setup. Return undefined to use the default recovery card. */
2889
+ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
2875
2890
  resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
2876
2891
  onOpenSession?: ((sessionId: string) => void) | undefined;
2877
2892
  loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
@@ -2915,11 +2930,13 @@ export function TimelineRow({
2915
2930
  return <CompactionRow item={item} />;
2916
2931
  case "auth-needed":
2917
2932
  return (
2918
- <AuthNeededRow
2919
- item={item}
2920
- onReconnect={onReconnect}
2921
- resolveProviderLogo={resolveProviderLogo}
2922
- />
2933
+ renderAuthNeeded?.(item) ?? (
2934
+ <AuthNeededRow
2935
+ item={item}
2936
+ onReconnect={onReconnect}
2937
+ resolveProviderLogo={resolveProviderLogo}
2938
+ />
2939
+ )
2923
2940
  );
2924
2941
  default:
2925
2942
  return null;
@@ -699,7 +699,8 @@ export function useSandboxWorkspaceTabs(
699
699
  (captureState.fileCount ?? 0) > 0 ||
700
700
  (captureState.capture?.repos.some((repo) => (repo.branchDiff?.length ?? 0) > 0) ?? false);
701
701
  const captureUnavailable =
702
- (captureState.fileCount === 0 && !captureHasChanges) || captureState.error !== null;
702
+ !captureState.loading &&
703
+ ((captureState.fileCount === 0 && !captureHasChanges) || captureState.error !== null);
703
704
  if (defaultTabRef.current.value === null) {
704
705
  if (initialTab && (!isWorkbenchSurface(initialTab) || surfaceSet.has(initialTab))) {
705
706
  defaultTabRef.current.value = initialTab;
@@ -711,7 +712,13 @@ export function useSandboxWorkspaceTabs(
711
712
  );
712
713
  } else if (captureUnavailable && initialWorkspaceTab(events) === WORKBENCH_TAB_CHANGES) {
713
714
  defaultTabRef.current.value = sourceDrivenDefaultTab(true, changesEnabled, filesEnabled);
714
- } else if (captureState.fileCount !== null || captureState.error !== null) {
715
+ } else if (
716
+ !captureState.loading &&
717
+ (captureState.fileCount !== null || captureState.error !== null)
718
+ ) {
719
+ // Metadata may arrive before a signed manifest. While that manifest is
720
+ // loading, neither a positive count nor zero (committed-only changes)
721
+ // proves an unavailable capture, so leave the default unresolved.
715
722
  // No durable review surface exists. Files owns the explicit live-workspace
716
723
  // gate; a user can open Changes deliberately if they need a fresh Git read.
717
724
  defaultTabRef.current.value = sourceDrivenDefaultTab(false, changesEnabled, filesEnabled);
@@ -0,0 +1,171 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { ConnectAccount, ConnectController } from "@opengeni/connect";
3
+ import { useConnect } from "./connect";
4
+
5
+ export type ConnectAccountsProps = {
6
+ controller: ConnectController;
7
+ className?: string;
8
+ /** Enables explicit account-bound reconnect. Host supplies the exact destination. */
9
+ returnUrl?: string;
10
+ };
11
+
12
+ /** Local credential revocation only. Unknown or changed versions must be
13
+ * refreshed; this component never retries a destructive operation implicitly. */
14
+ export function ConnectAccounts(props: ConnectAccountsProps) {
15
+ const [scope, setScope] = useState(props.controller);
16
+ const [generation, setGeneration] = useState(0);
17
+ if (scope !== props.controller) {
18
+ setScope(props.controller);
19
+ setGeneration(generation + 1);
20
+ }
21
+ return <ScopedAccounts key={generation} {...props} />;
22
+ }
23
+
24
+ function ScopedAccounts({ controller, className, returnUrl }: ConnectAccountsProps) {
25
+ const view = useConnect(controller);
26
+ const [accounts, setAccounts] = useState<ConnectAccount[] | null>(null);
27
+ const [selected, setSelected] = useState<string | null>(null);
28
+ const [busy, setBusy] = useState(false);
29
+ const [error, setError] = useState(false);
30
+ const [reload, setReload] = useState(0);
31
+ const mutation = useRef<AbortController | null>(null);
32
+ useEffect(() => () => mutation.current?.abort(), []);
33
+ useEffect(() => {
34
+ const abort = new AbortController();
35
+ setAccounts(null);
36
+ setSelected(null);
37
+ setError(false);
38
+ void Promise.resolve()
39
+ .then(() => {
40
+ abort.signal.throwIfAborted();
41
+ return controller.transport.accounts(controller.workspaceId, { signal: abort.signal });
42
+ })
43
+ .then((result) => {
44
+ if (!abort.signal.aborted) setAccounts(structuredClone(result));
45
+ })
46
+ .catch(() => {
47
+ if (!abort.signal.aborted) setError(true);
48
+ });
49
+ return () => abort.abort();
50
+ }, [controller, reload]);
51
+
52
+ const disconnect = async (account: ConnectAccount) => {
53
+ const expectedVersion = account.version;
54
+ if (
55
+ mutation.current ||
56
+ view.busy ||
57
+ expectedVersion === undefined ||
58
+ !Number.isSafeInteger(expectedVersion) ||
59
+ expectedVersion < 1
60
+ )
61
+ return;
62
+ const abort = new AbortController();
63
+ mutation.current = abort;
64
+ setBusy(true);
65
+ setError(false);
66
+ try {
67
+ await controller.transport.disconnect(controller.workspaceId, account.id, {
68
+ expectedVersion,
69
+ signal: abort.signal,
70
+ });
71
+ if (!abort.signal.aborted) setReload((value) => value + 1);
72
+ } catch {
73
+ if (!abort.signal.aborted) {
74
+ // Outcome may be unknown. Drop the inventory so a second click cannot
75
+ // replay against the stale selection; let the host reload live state.
76
+ setAccounts(null);
77
+ setError(true);
78
+ }
79
+ } finally {
80
+ if (!abort.signal.aborted) {
81
+ mutation.current = null;
82
+ setBusy(false);
83
+ setSelected(null);
84
+ }
85
+ }
86
+ };
87
+ const reconnect = async (account: ConnectAccount) => {
88
+ if (!returnUrl || mutation.current || view.busy) return;
89
+ setError(false);
90
+ try {
91
+ await controller.begin({
92
+ providerId: account.providerId,
93
+ ownership: account.ownership,
94
+ reconnectAccountId: account.id,
95
+ returnUrl,
96
+ idempotencyKey: crypto.randomUUID(),
97
+ });
98
+ } catch {
99
+ setError(true);
100
+ }
101
+ };
102
+
103
+ return (
104
+ <section
105
+ className={className}
106
+ aria-label="Connected accounts"
107
+ aria-busy={busy || view.busy || (!accounts && !error)}
108
+ >
109
+ {error && (
110
+ <p role="alert">Account state could not be confirmed. Reload before trying again.</p>
111
+ )}
112
+ {!accounts && !error && <p role="status">Loading accounts…</p>}
113
+ {accounts?.length === 0 && <p role="status">No connected accounts.</p>}
114
+ {accounts && accounts.length > 0 && (
115
+ <ul>
116
+ {accounts.map((account) => (
117
+ <li key={`${account.providerId}:${account.id}`}>
118
+ <span>
119
+ {account.label} — {account.providerId} — {account.ownership} —{" "}
120
+ {account.status.replaceAll("_", " ")}
121
+ </span>
122
+ {returnUrl && (
123
+ <button
124
+ type="button"
125
+ disabled={busy || view.busy || account.status === "disabled"}
126
+ onClick={() => void reconnect(account)}
127
+ >
128
+ Reconnect {account.label}
129
+ </button>
130
+ )}
131
+ {selected === `${account.providerId}:${account.id}` ? (
132
+ <div role="group" aria-label={`Disconnect ${account.label}`}>
133
+ <p>
134
+ This removes local OpenGeni access. It does not revoke consent at the provider.
135
+ </p>
136
+ <button
137
+ type="button"
138
+ disabled={busy || view.busy}
139
+ onClick={() => void disconnect(account)}
140
+ >
141
+ Confirm disconnect
142
+ </button>
143
+ <button type="button" disabled={busy} onClick={() => setSelected(null)}>
144
+ Keep account
145
+ </button>
146
+ </div>
147
+ ) : (
148
+ <button
149
+ type="button"
150
+ disabled={
151
+ busy ||
152
+ view.busy ||
153
+ account.status === "disabled" ||
154
+ !Number.isSafeInteger(account.version) ||
155
+ account.version! < 1
156
+ }
157
+ onClick={() => setSelected(`${account.providerId}:${account.id}`)}
158
+ >
159
+ Disconnect {account.label}
160
+ </button>
161
+ )}
162
+ </li>
163
+ ))}
164
+ </ul>
165
+ )}
166
+ <button type="button" disabled={busy} onClick={() => setReload((value) => value + 1)}>
167
+ Reload accounts
168
+ </button>
169
+ </section>
170
+ );
171
+ }
@@ -0,0 +1,144 @@
1
+ import { useEffect, useState, type FormEvent } from "react";
2
+ import type { ConnectController, ConnectProvider } from "@opengeni/connect";
3
+ import { useConnect } from "./connect";
4
+
5
+ export type ConnectChooserProps = {
6
+ controller: ConnectController;
7
+ /** Exact host return string; no completion parameters are added. */
8
+ returnUrl: string;
9
+ className?: string;
10
+ };
11
+
12
+ /** Catalog readiness is supplied by the authenticated backend, never inferred
13
+ * from the existence of a provider definition. Host replaces the controller on
14
+ * actor/workspace change. This form does not open OAuth windows itself. */
15
+ export function ConnectChooser(props: ConnectChooserProps) {
16
+ // Remount scope-local selections synchronously when the controller changes,
17
+ // before effects can expose a previous actor's inventory for one frame.
18
+ const [scope, setScope] = useState(props.controller);
19
+ const [generation, setGeneration] = useState(0);
20
+ if (scope !== props.controller) {
21
+ setScope(props.controller);
22
+ setGeneration(generation + 1);
23
+ }
24
+ return <ScopedChooser key={generation} {...props} />;
25
+ }
26
+
27
+ function ScopedChooser({ controller, returnUrl, className }: ConnectChooserProps) {
28
+ const view = useConnect(controller);
29
+ const [catalog, setCatalog] = useState<ConnectProvider[] | null>(null);
30
+ const [failed, setFailed] = useState(false);
31
+ const [load, setLoad] = useState(0);
32
+ const [providerId, setProviderId] = useState("");
33
+ const [ownership, setOwnership] = useState("");
34
+ useEffect(() => {
35
+ const abort = new AbortController();
36
+ setCatalog(null);
37
+ setFailed(false);
38
+ setProviderId("");
39
+ setOwnership("");
40
+ // Promise boundary also handles a synchronously throwing injected transport.
41
+ void Promise.resolve()
42
+ .then(() => {
43
+ abort.signal.throwIfAborted();
44
+ return controller.transport.catalog(controller.workspaceId, { signal: abort.signal });
45
+ })
46
+ .then((providers) => {
47
+ if (!abort.signal.aborted) setCatalog(structuredClone(providers));
48
+ })
49
+ .catch(() => {
50
+ if (!abort.signal.aborted) setFailed(true);
51
+ });
52
+ return () => abort.abort();
53
+ }, [controller, load]);
54
+ const provider = catalog?.find((entry) => entry.id === providerId);
55
+ const canBegin =
56
+ provider?.readiness === "available" &&
57
+ (ownership === "personal" || ownership === "workspace") &&
58
+ provider.ownership.includes(ownership);
59
+ const submit = (event: FormEvent) => {
60
+ event.preventDefault();
61
+ if (!canBegin || view.busy || (ownership !== "personal" && ownership !== "workspace")) return;
62
+ setFailed(false);
63
+ try {
64
+ void controller
65
+ .begin({ providerId, ownership, returnUrl, idempotencyKey: crypto.randomUUID() })
66
+ .catch(() => setFailed(true));
67
+ } catch {
68
+ setFailed(true);
69
+ }
70
+ };
71
+ return (
72
+ <section
73
+ className={className}
74
+ aria-label="Choose a connection"
75
+ aria-busy={view.busy || (!catalog && !failed)}
76
+ >
77
+ {failed && (
78
+ <p role="alert">
79
+ Connections could not be loaded or started. Check setup status before retrying.
80
+ </p>
81
+ )}
82
+ {!catalog && !failed && <p role="status">Loading connections…</p>}
83
+ {catalog?.length === 0 && <p role="status">No connections are available.</p>}
84
+ {Boolean(catalog?.length) && (
85
+ <form onSubmit={submit}>
86
+ <fieldset disabled={view.busy}>
87
+ <legend>Provider and ownership</legend>
88
+ <label>
89
+ Provider
90
+ <select
91
+ required
92
+ value={providerId}
93
+ onChange={(event) => {
94
+ setProviderId(event.target.value);
95
+ setOwnership("");
96
+ }}
97
+ >
98
+ <option value="" disabled>
99
+ Choose a provider
100
+ </option>
101
+ {catalog!.map((entry) => (
102
+ <option
103
+ key={entry.id}
104
+ value={entry.id}
105
+ disabled={entry.readiness !== "available" || entry.ownership.length === 0}
106
+ >
107
+ {entry.label} — {entry.id} ({entry.readiness.replaceAll("_", " ")})
108
+ </option>
109
+ ))}
110
+ </select>
111
+ </label>
112
+ {provider && (
113
+ <label>
114
+ Ownership
115
+ <select
116
+ required
117
+ value={ownership}
118
+ onChange={(event) => setOwnership(event.target.value)}
119
+ >
120
+ <option value="" disabled>
121
+ Choose ownership
122
+ </option>
123
+ {provider.ownership.map((choice) => (
124
+ <option key={choice} value={choice}>
125
+ {choice === "personal"
126
+ ? "Personal — owned by you"
127
+ : "Workspace — shared connection"}
128
+ </option>
129
+ ))}
130
+ </select>
131
+ </label>
132
+ )}
133
+ <button type="submit" disabled={!canBegin}>
134
+ Start setup
135
+ </button>
136
+ </fieldset>
137
+ </form>
138
+ )}
139
+ <button type="button" disabled={view.busy} onClick={() => setLoad(load + 1)}>
140
+ Reload providers
141
+ </button>
142
+ </section>
143
+ );
144
+ }
@@ -0,0 +1,29 @@
1
+ import { ConnectAccounts } from "./connect-accounts";
2
+ import { ConnectChooser } from "./connect-chooser";
3
+ import { ConnectSetup, type ConnectSetupProps } from "./connect-setup";
4
+
5
+ export type ConnectPanelProps = ConnectSetupProps & {
6
+ returnUrl: string;
7
+ title?: string;
8
+ showAccounts?: boolean;
9
+ };
10
+
11
+ /** Optional composition over the same unstyled surfaces. Import
12
+ * @opengeni/react/connect.css for styling; the host retains navigation,
13
+ * transport, actor admission, and controller lifetime. */
14
+ export function ConnectPanel({
15
+ returnUrl,
16
+ title = "Connections",
17
+ showAccounts = true,
18
+ className,
19
+ ...setup
20
+ }: ConnectPanelProps) {
21
+ return (
22
+ <div className={["og-connect", className].filter(Boolean).join(" ")}>
23
+ <h2>{title}</h2>
24
+ <ConnectChooser controller={setup.controller} returnUrl={returnUrl} />
25
+ <ConnectSetup {...setup} />
26
+ {showAccounts && <ConnectAccounts controller={setup.controller} returnUrl={returnUrl} />}
27
+ </div>
28
+ );
29
+ }