@opengeni/react 1.0.2-canary.37191796234001 → 1.0.2

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 (75) hide show
  1. package/README.md +15 -6
  2. package/dist/artifacts.js +1 -1
  3. package/dist/{browser-viewer-GTR5XQ7X.js → browser-viewer-SHYKQRNX.js} +4 -4
  4. package/dist/{chunk-CR7U7HAR.js → chunk-26ZNDDLZ.js} +3 -3
  5. package/dist/{chunk-ELCE5SXV.js → chunk-3CTRFMAK.js} +2 -2
  6. package/dist/{chunk-XOAMRDDK.js → chunk-742CZ3L3.js} +2 -2
  7. package/dist/{chunk-H2NJQORO.js → chunk-DO544OSV.js} +7 -1
  8. package/dist/chunk-DO544OSV.js.map +1 -0
  9. package/dist/{chunk-IQUCRAT7.js → chunk-EESD3PY3.js} +2 -2
  10. package/dist/{chunk-23J2ZNXT.js → chunk-EN3YTHKR.js} +3 -3
  11. package/dist/{chunk-JFNEV4LU.js → chunk-GYVRFZDY.js} +3 -3
  12. package/dist/{chunk-YWFG7ZX7.js → chunk-HDJWDK7W.js} +3 -3
  13. package/dist/{chunk-NZUI76EM.js → chunk-J6DBS4T3.js} +2 -2
  14. package/dist/{chunk-SJZDKVW2.js → chunk-JIQYUGVL.js} +4 -4
  15. package/dist/{chunk-XUW2UWQM.js → chunk-RLUSC2WA.js} +3 -3
  16. package/dist/{chunk-XUW2UWQM.js.map → chunk-RLUSC2WA.js.map} +1 -1
  17. package/dist/{chunk-Q5QBEZIQ.js → chunk-RSI7XYXL.js} +2 -2
  18. package/dist/{chunk-NEXLLTP4.js → chunk-TKOTKRWY.js} +4 -4
  19. package/dist/{chunk-NEXLLTP4.js.map → chunk-TKOTKRWY.js.map} +1 -1
  20. package/dist/{chunk-TMJRG4YC.js → chunk-VV5E43LD.js} +432 -180
  21. package/dist/chunk-VV5E43LD.js.map +1 -0
  22. package/dist/{chunk-FFA2OVOV.js → chunk-WGXVEJO5.js} +3 -3
  23. package/dist/{chunk-E3G7SAYZ.js → chunk-YM2DE7BQ.js} +2 -2
  24. package/dist/components/open-geni-chat.d.ts +6 -3
  25. package/dist/components/session-conversation.d.ts +6 -3
  26. package/dist/components/session-proxy-scope.d.ts +21 -0
  27. package/dist/composer.js +6 -6
  28. package/dist/{computer-viewer-ENHVAZ2Q.js → computer-viewer-6SRGWRUX.js} +4 -4
  29. package/dist/index.js +17 -17
  30. package/dist/index.js.map +1 -1
  31. package/dist/interaction.js +4 -4
  32. package/dist/{latest-question-755OW5DS.js → latest-question-HTRIS4KJ.js} +2 -2
  33. package/dist/lib/host-theme.d.ts +7 -0
  34. package/dist/machines.js +3 -3
  35. package/dist/{model-policy-picker-menu-PWRI7R2X.js → model-policy-picker-menu-M54ISBMN.js} +4 -4
  36. package/dist/model-policy.js +1 -1
  37. package/dist/realtime.js +3 -3
  38. package/dist/{sandbox-files-BBC47SLD.js → sandbox-files-GORZOUUJ.js} +4 -4
  39. package/dist/session-ui.js +9 -9
  40. package/dist/session.js +5 -5
  41. package/dist/timeline/turn-summary.d.ts +3 -1
  42. package/dist/timeline/types.d.ts +6 -0
  43. package/dist/usage.js +1 -1
  44. package/package.json +3 -3
  45. package/src/components/message-timeline.tsx +704 -450
  46. package/src/components/open-geni-chat.tsx +56 -37
  47. package/src/components/session-conversation.tsx +87 -73
  48. package/src/components/session-proxy-scope.tsx +92 -0
  49. package/src/hooks/use-session-background-commands.ts +2 -2
  50. package/src/hooks/use-video-artifact-playback.ts +1 -1
  51. package/src/lib/host-theme.ts +15 -2
  52. package/src/model-policy.ts +2 -2
  53. package/src/session-context.ts +3 -3
  54. package/src/timeline/projection.ts +9 -0
  55. package/src/timeline/turn-summary.tsx +10 -0
  56. package/src/timeline/types.ts +6 -0
  57. package/dist/chunk-H2NJQORO.js.map +0 -1
  58. package/dist/chunk-TMJRG4YC.js.map +0 -1
  59. /package/dist/{browser-viewer-GTR5XQ7X.js.map → browser-viewer-SHYKQRNX.js.map} +0 -0
  60. /package/dist/{chunk-CR7U7HAR.js.map → chunk-26ZNDDLZ.js.map} +0 -0
  61. /package/dist/{chunk-ELCE5SXV.js.map → chunk-3CTRFMAK.js.map} +0 -0
  62. /package/dist/{chunk-XOAMRDDK.js.map → chunk-742CZ3L3.js.map} +0 -0
  63. /package/dist/{chunk-IQUCRAT7.js.map → chunk-EESD3PY3.js.map} +0 -0
  64. /package/dist/{chunk-23J2ZNXT.js.map → chunk-EN3YTHKR.js.map} +0 -0
  65. /package/dist/{chunk-JFNEV4LU.js.map → chunk-GYVRFZDY.js.map} +0 -0
  66. /package/dist/{chunk-YWFG7ZX7.js.map → chunk-HDJWDK7W.js.map} +0 -0
  67. /package/dist/{chunk-NZUI76EM.js.map → chunk-J6DBS4T3.js.map} +0 -0
  68. /package/dist/{chunk-SJZDKVW2.js.map → chunk-JIQYUGVL.js.map} +0 -0
  69. /package/dist/{chunk-Q5QBEZIQ.js.map → chunk-RSI7XYXL.js.map} +0 -0
  70. /package/dist/{chunk-FFA2OVOV.js.map → chunk-WGXVEJO5.js.map} +0 -0
  71. /package/dist/{chunk-E3G7SAYZ.js.map → chunk-YM2DE7BQ.js.map} +0 -0
  72. /package/dist/{computer-viewer-ENHVAZ2Q.js.map → computer-viewer-6SRGWRUX.js.map} +0 -0
  73. /package/dist/{latest-question-755OW5DS.js.map → latest-question-HTRIS4KJ.js.map} +0 -0
  74. /package/dist/{model-policy-picker-menu-PWRI7R2X.js.map → model-policy-picker-menu-M54ISBMN.js.map} +0 -0
  75. /package/dist/{sandbox-files-BBC47SLD.js.map → sandbox-files-GORZOUUJ.js.map} +0 -0
@@ -11,6 +11,7 @@ import {
11
11
  import { useOpenGeni, type ClientOverride } from "../session-context";
12
12
  import { SessionConversation, type SessionConversationProps } from "./session-conversation";
13
13
  import { SessionList, type SessionListLabels, type SessionListProps } from "./session-list";
14
+ import { SessionProxyScope, type SessionProxyBaseUrl } from "./session-proxy-scope";
14
15
 
15
16
  export type OpenGeniChatLabels = SessionListLabels & {
16
17
  openChats: string;
@@ -31,41 +32,47 @@ const DEFAULT_LABELS: Omit<OpenGeniChatLabels, keyof SessionListLabels> = {
31
32
  newChatUnavailable: "New chats are not enabled for this product.",
32
33
  };
33
34
 
34
- export type OpenGeniChatProps = ClientOverride & {
35
- /** Controlled selection. `null` shows the new-chat composer. */
36
- sessionId?: string | null | undefined;
37
- /** Initial selection when uncontrolled. Defaults to a new chat. */
38
- defaultSessionId?: string | null | undefined;
39
- onSessionChange?: ((sessionId: string | null) => void) | undefined;
40
- /**
41
- * Create a chat from its first message. Defaults to `client.createSession`
42
- * with `{ initialMessage, idempotencyKey }`, which `createSessionProxyHandler`
43
- * accepts when the server supplies a `createSession` hook. Return the new id.
44
- */
45
- createSession?: ((initialMessage: string, idempotencyKey: string) => Promise<string>) | undefined;
46
- /** Hide the "New chat" entry point. */
47
- newChat?: boolean | undefined;
48
- /** Forwarded to the conversation (message rendering, tool renderers, composer...). */
49
- conversationProps?: Omit<SessionConversationProps, "sessionId" | "client" | "workspaceId">;
50
- /** Forwarded to the list (rename/archive toggles, page size). */
51
- listProps?: Pick<SessionListProps, "rename" | "archive" | "pageSize"> | undefined;
52
- labels?: Partial<OpenGeniChatLabels> | undefined;
53
- className?: string | undefined;
54
- /** Defaults to filling the host. */
55
- height?: CSSProperties["height"];
56
- /**
57
- * Light or dark. Defaults to `auto`: follow the host page (an enclosing
58
- * `data-og-theme`, `class="dark"`/`data-theme` on <html> or <body>, the
59
- * host's `color-scheme`, then its background), not the OS setting alone.
60
- */
61
- theme?: HostThemePreference | undefined;
62
- /**
63
- * `host` (default) derives backgrounds and cards from the host background so
64
- * the chat blends in; `theme` uses the `--og-color-*` surface tokens as they
65
- * are. Customized surface tokens are always kept.
66
- */
67
- surface?: HostSurfacePreference | undefined;
68
- };
35
+ export type OpenGeniChatProps = ClientOverride &
36
+ SessionProxyBaseUrl & {
37
+ /** Controlled selection. `null` shows the new-chat composer. */
38
+ sessionId?: string | null | undefined;
39
+ /** Initial selection when uncontrolled. Defaults to a new chat. */
40
+ defaultSessionId?: string | null | undefined;
41
+ onSessionChange?: ((sessionId: string | null) => void) | undefined;
42
+ /**
43
+ * Create a chat from its first message. Defaults to `client.createSession`
44
+ * with `{ initialMessage, idempotencyKey }`, which `createSessionProxyHandler`
45
+ * accepts when the server supplies a `createSession` hook. Return the new id.
46
+ */
47
+ createSession?:
48
+ | ((initialMessage: string, idempotencyKey: string) => Promise<string>)
49
+ | undefined;
50
+ /** Hide the "New chat" entry point. */
51
+ newChat?: boolean | undefined;
52
+ /** Forwarded to the conversation (message rendering, tool renderers, composer...). */
53
+ conversationProps?: Omit<
54
+ SessionConversationProps,
55
+ "sessionId" | "client" | "workspaceId" | "baseUrl"
56
+ >;
57
+ /** Forwarded to the list (rename/archive toggles, page size). */
58
+ listProps?: Pick<SessionListProps, "rename" | "archive" | "pageSize"> | undefined;
59
+ labels?: Partial<OpenGeniChatLabels> | undefined;
60
+ className?: string | undefined;
61
+ /** Defaults to filling the host. */
62
+ height?: CSSProperties["height"];
63
+ /**
64
+ * Light or dark. Defaults to `auto`: follow the host page (an enclosing
65
+ * `data-og-theme`, `class="dark"`/`data-theme` on <html> or <body>, the
66
+ * host's `color-scheme`, then its background), not the OS setting alone.
67
+ */
68
+ theme?: HostThemePreference | undefined;
69
+ /**
70
+ * `host` (default) derives backgrounds and cards from the host background so
71
+ * the chat blends in; `theme` uses the `--og-color-*` surface tokens as they
72
+ * are. Customized surface tokens are always kept.
73
+ */
74
+ surface?: HostSurfacePreference | undefined;
75
+ };
69
76
 
70
77
  type CreateClient = Partial<Pick<OpenGeniClient, "createSession">>;
71
78
 
@@ -73,8 +80,20 @@ type CreateClient = Partial<Pick<OpenGeniClient, "createSession">>;
73
80
  * A complete chat experience: the user's chat list plus the conversation.
74
81
  * The list is a sidebar when the component is wide and a drawer when narrow
75
82
  * (container-based, so it adapts inside panels as well as full pages).
83
+ * `<OpenGeniChat baseUrl="/api/opengeni" />` needs no provider: it talks to
84
+ * your session proxy and uses the workspace the proxy resolves.
76
85
  */
77
- export function OpenGeniChat({
86
+ export function OpenGeniChat({ baseUrl, ...props }: OpenGeniChatProps) {
87
+ if (baseUrl === undefined) return <Chat {...props} />;
88
+ const { client: _client, workspaceId, ...rest } = props;
89
+ return (
90
+ <SessionProxyScope baseUrl={baseUrl} workspaceId={workspaceId}>
91
+ <Chat {...rest} />
92
+ </SessionProxyScope>
93
+ );
94
+ }
95
+
96
+ function Chat({
78
97
  client,
79
98
  workspaceId,
80
99
  sessionId: controlledSessionId,
@@ -89,7 +108,7 @@ export function OpenGeniChat({
89
108
  height = "100%",
90
109
  theme,
91
110
  surface,
92
- }: OpenGeniChatProps) {
111
+ }: Omit<OpenGeniChatProps, "baseUrl">) {
93
112
  const labels = { ...DEFAULT_LABELS, ...labelOverrides } as OpenGeniChatLabels;
94
113
  const scope = { client, workspaceId };
95
114
  const context = useOpenGeni(scope);
@@ -37,6 +37,7 @@ import {
37
37
  import type { UserMessageDisclosureLabels } from "./user-message-body";
38
38
  import { conversationTimeline } from "../conversation-timeline";
39
39
  import { cn } from "../lib/cn";
40
+ import { SessionProxyScope, type SessionProxyBaseUrl } from "./session-proxy-scope";
40
41
  import { useErrorMessage } from "../lib/error-message";
41
42
  import {
42
43
  useHostTheme,
@@ -56,81 +57,94 @@ const DEFAULT_CONVERSATION_LABELS: SessionConversationLabels = {
56
57
  tryAgain: "Try again",
57
58
  };
58
59
 
59
- export type SessionConversationProps = ClientOverride & {
60
- sessionId: string;
61
- /** Host-owned artifact links, previews and other message presentation. */
62
- renderMessageText?: MessageTimelineProps["renderMessageText"];
63
- /**
64
- * Open OpenGeni object links in agent replies (`artifact:`, `sandbox:`,
65
- * editable artifacts, Sites). Asked first; by default retained files and
66
- * sandbox files download only when the proxy explicitly enables them, while
67
- * editable artifacts and Sites stay unavailable until the host resolves them.
68
- */
69
- resolveLink?: OpenGeniLinkResolver | undefined;
70
- /**
71
- * Open agent links to editable artifacts and Sites in a host viewer, for
72
- * example `SessionArtifactViewer` mounted beside the conversation. Asked
73
- * after `resolveLink`.
74
- */
75
- onOpenArtifact?: ((target: OpenGeniViewerTarget) => void) | undefined;
76
- /**
77
- * Inline previews for assistant `opengeni-site` / `opengeni-html` fences.
78
- * Defaults to the OpenGeni preview (Site reads need the proxy's
79
- * `artifacts` option); `false` shows the fence as code.
80
- */
81
- renderInteractiveBlock?: MessageTimelineProps["renderInteractiveBlock"] | false;
82
- /** Product-specific tool-call renderers; defaults to the built-in registry. */
83
- toolRegistry?: MessageTimelineProps["toolRegistry"];
84
- /**
85
- * Replace the "usage limit reached" row for an `allowance_exhausted`
86
- * refusal, for example to link your own plan or admin page.
87
- */
88
- renderAllowanceExhausted?: MessageTimelineProps["renderAllowanceExhausted"];
89
- /** Replace the words of the default "usage limit reached" row. */
90
- allowanceExhaustedLabels?: MessageTimelineProps["allowanceExhaustedLabels"];
91
- /**
92
- * File attachments in the composer. Defaults to true; the attach control
93
- * appears only when the deployment's client config enables file uploads.
94
- */
95
- attachments?: boolean | undefined;
96
- /**
97
- * Show the model/reasoning picker. End users of an embedded product rarely
98
- * choose models, so it is hidden unless this is `true` or the client config
99
- * reports `modelSelection: true` (`createSessionProxyHandler({ modelSelection: true })`).
100
- */
101
- modelPicker?: boolean | undefined;
102
- /** Model-picker appearance only; visibility, policy and delivery remain owned here. */
103
- modelPickerProps?: Pick<ModelPolicyPickerProps, "groupPresentation" | "messages"> | undefined;
104
- /** Localized actions for already-sent user-message disclosure. */
105
- userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined;
106
- loadSkillReview?: HumanInputSurfaceProps["loadSkillReview"];
107
- className?: string;
108
- /** Defaults to filling the host. The host owns available height. */
109
- height?: CSSProperties["height"];
110
- /**
111
- * Light or dark. Defaults to `auto`: follow the host page (an enclosing
112
- * `data-og-theme`, `class="dark"`/`data-theme` on <html> or <body>, the
113
- * host's `color-scheme`, then its background), not the OS setting alone.
114
- */
115
- theme?: HostThemePreference | undefined;
116
- /**
117
- * `host` (default) derives backgrounds and cards from the host background so
118
- * the conversation blends in; `theme` uses the `--og-color-*` surface tokens
119
- * as they are. Customized surface tokens are always kept.
120
- */
121
- surface?: HostSurfacePreference | undefined;
122
- /** Conversation-owned copy (error actions). */
123
- labels?: Partial<SessionConversationLabels> | undefined;
124
- /** Presentation/custom controls only; queue and delivery wiring stay owned here. */
125
- composerProps?: Omit<
126
- ChatComposerProps,
127
- "composer" | "effectiveControl" | "queuedAheadCount" | "attachments"
128
- >;
129
- };
60
+ export type SessionConversationProps = ClientOverride &
61
+ SessionProxyBaseUrl & {
62
+ sessionId: string;
63
+ /** Host-owned artifact links, previews and other message presentation. */
64
+ renderMessageText?: MessageTimelineProps["renderMessageText"];
65
+ /**
66
+ * Open OpenGeni object links in agent replies (`artifact:`, `sandbox:`,
67
+ * editable artifacts, Sites). Asked first; by default retained files and
68
+ * sandbox files download only when the proxy explicitly enables them, while
69
+ * editable artifacts and Sites stay unavailable until the host resolves them.
70
+ */
71
+ resolveLink?: OpenGeniLinkResolver | undefined;
72
+ /**
73
+ * Open agent links to editable artifacts and Sites in a host viewer, for
74
+ * example `SessionArtifactViewer` mounted beside the conversation. Asked
75
+ * after `resolveLink`.
76
+ */
77
+ onOpenArtifact?: ((target: OpenGeniViewerTarget) => void) | undefined;
78
+ /**
79
+ * Inline previews for assistant `opengeni-site` / `opengeni-html` fences.
80
+ * Defaults to the OpenGeni preview (Site reads need the proxy's
81
+ * `artifacts` option); `false` shows the fence as code.
82
+ */
83
+ renderInteractiveBlock?: MessageTimelineProps["renderInteractiveBlock"] | false;
84
+ /** Product-specific tool-call renderers; defaults to the built-in registry. */
85
+ toolRegistry?: MessageTimelineProps["toolRegistry"];
86
+ /**
87
+ * Replace the "usage limit reached" row for an `allowance_exhausted`
88
+ * refusal, for example to link your own plan or admin page.
89
+ */
90
+ renderAllowanceExhausted?: MessageTimelineProps["renderAllowanceExhausted"];
91
+ /** Replace the words of the default "usage limit reached" row. */
92
+ allowanceExhaustedLabels?: MessageTimelineProps["allowanceExhaustedLabels"];
93
+ /**
94
+ * File attachments in the composer. Defaults to true; the attach control
95
+ * appears only when the deployment's client config enables file uploads.
96
+ */
97
+ attachments?: boolean | undefined;
98
+ /**
99
+ * Show the model/reasoning picker. End users of an embedded product rarely
100
+ * choose models, so it is hidden unless this is `true` or the client config
101
+ * reports `modelSelection: true` (`createSessionProxyHandler({ modelSelection: true })`).
102
+ */
103
+ modelPicker?: boolean | undefined;
104
+ /** Model-picker appearance only; visibility, policy and delivery remain owned here. */
105
+ modelPickerProps?: Pick<ModelPolicyPickerProps, "groupPresentation" | "messages"> | undefined;
106
+ /** Localized actions for already-sent user-message disclosure. */
107
+ userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined;
108
+ loadSkillReview?: HumanInputSurfaceProps["loadSkillReview"];
109
+ className?: string;
110
+ /** Defaults to filling the host. The host owns available height. */
111
+ height?: CSSProperties["height"];
112
+ /**
113
+ * Light or dark. Defaults to `auto`: follow the host page (an enclosing
114
+ * `data-og-theme`, `class="dark"`/`data-theme` on <html> or <body>, the
115
+ * host's `color-scheme`, then its background), not the OS setting alone.
116
+ */
117
+ theme?: HostThemePreference | undefined;
118
+ /**
119
+ * `host` (default) derives backgrounds and cards from the host background so
120
+ * the conversation blends in; `theme` uses the `--og-color-*` surface tokens
121
+ * as they are. Customized surface tokens are always kept.
122
+ */
123
+ surface?: HostSurfacePreference | undefined;
124
+ /** Conversation-owned copy (error actions). */
125
+ labels?: Partial<SessionConversationLabels> | undefined;
126
+ /** Presentation/custom controls only; queue and delivery wiring stay owned here. */
127
+ composerProps?: Omit<
128
+ ChatComposerProps,
129
+ "composer" | "effectiveControl" | "queuedAheadCount" | "attachments"
130
+ >;
131
+ };
130
132
 
131
133
  /** Complete existing-session conversation. Uses the provider's normal SDK client
132
- * (including Site clients), one shared event feed, and authoritative queue state. */
133
- export function SessionConversation(props: SessionConversationProps) {
134
+ * (including Site clients), one shared event feed, and authoritative queue state.
135
+ * `<SessionConversation baseUrl="/api/opengeni" sessionId={id} />` needs no
136
+ * provider: it talks to your session proxy and its resolved workspace. */
137
+ export function SessionConversation({ baseUrl, ...props }: SessionConversationProps) {
138
+ if (baseUrl === undefined) return <RetryingConversation {...props} />;
139
+ const { client: _client, workspaceId, ...rest } = props;
140
+ return (
141
+ <SessionProxyScope baseUrl={baseUrl} workspaceId={workspaceId}>
142
+ <RetryingConversation {...rest} />
143
+ </SessionProxyScope>
144
+ );
145
+ }
146
+
147
+ function RetryingConversation(props: Omit<SessionConversationProps, "baseUrl">) {
134
148
  // A failed initial load retries by remounting the whole conversation.
135
149
  const [attempt, setAttempt] = useState(0);
136
150
  return (
@@ -0,0 +1,92 @@
1
+ import { OpenGeniBrowserClient } from "@opengeni/sdk/browser";
2
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
3
+ import { OpenGeniProvider } from "../provider";
4
+
5
+ export type SessionProxyBaseUrl = {
6
+ /**
7
+ * Mount path of your `createSessionProxyHandler` (for example
8
+ * `"/api/opengeni"`). The component then needs no `OpenGeniProvider`,
9
+ * client, or workspace id: it creates the browser client for that path and
10
+ * uses the workspace the proxy resolved for the signed-in user. An explicit
11
+ * `workspaceId` skips that lookup.
12
+ */
13
+ baseUrl?: string | undefined;
14
+ };
15
+
16
+ type ScopeState =
17
+ | { status: "loading" }
18
+ | { status: "ready"; workspaceId: string }
19
+ | { status: "error"; message: string };
20
+
21
+ /**
22
+ * `OpenGeniProvider` for a same-origin session proxy, from its base URL alone.
23
+ * The proxy's client config names the resolved workspace, so the browser
24
+ * never handles an Opengeni id. Provider-based usage is unchanged.
25
+ */
26
+ export function SessionProxyScope({
27
+ baseUrl,
28
+ workspaceId,
29
+ children,
30
+ }: {
31
+ baseUrl: string;
32
+ workspaceId?: string | undefined;
33
+ children: ReactNode;
34
+ }) {
35
+ // Version skew between this bundle and the server SDK is tolerated, as for
36
+ // any embedded host: the proxy reports its own contract revision.
37
+ const client = useMemo(
38
+ () => new OpenGeniBrowserClient({ baseUrl, apiContract: "compatible" }),
39
+ [baseUrl],
40
+ );
41
+ const [state, setState] = useState<ScopeState>(
42
+ workspaceId ? { status: "ready", workspaceId } : { status: "loading" },
43
+ );
44
+ useEffect(() => {
45
+ if (workspaceId) {
46
+ setState({ status: "ready", workspaceId });
47
+ return;
48
+ }
49
+ let active = true;
50
+ setState({ status: "loading" });
51
+ client.getClientConfig().then(
52
+ (config) => {
53
+ if (!active) return;
54
+ setState(
55
+ config.workspaceId
56
+ ? { status: "ready", workspaceId: config.workspaceId }
57
+ : {
58
+ status: "error",
59
+ message:
60
+ "This server does not report its workspace. Update @opengeni/sdk on the server, or pass workspaceId.",
61
+ },
62
+ );
63
+ },
64
+ (error: unknown) => {
65
+ if (!active) return;
66
+ setState({
67
+ status: "error",
68
+ message: error instanceof Error ? error.message : "Could not load the conversation.",
69
+ });
70
+ },
71
+ );
72
+ return () => {
73
+ active = false;
74
+ };
75
+ }, [client, workspaceId]);
76
+
77
+ if (state.status === "error") {
78
+ return (
79
+ <div role="alert" className="og-root p-3 text-og-sm text-og-danger" data-og-proxy-error="">
80
+ {state.message}
81
+ </div>
82
+ );
83
+ }
84
+ if (state.status === "loading") {
85
+ return <div className="og-root" aria-busy="true" data-og-proxy-loading="" />;
86
+ }
87
+ return (
88
+ <OpenGeniProvider client={client} workspaceId={state.workspaceId}>
89
+ {children}
90
+ </OpenGeniProvider>
91
+ );
92
+ }
@@ -27,7 +27,7 @@ export function useSessionBackgroundCommands(
27
27
  async (signal?: AbortSignal) => {
28
28
  if (!sessionId) return { commands: [] };
29
29
  if (!client.listSessionBackgroundCommands) {
30
- throw new Error("The configured OpenGeni client does not support background commands");
30
+ throw new Error("The configured Opengeni client does not support background commands");
31
31
  }
32
32
  return await client.listSessionBackgroundCommands(workspaceId, sessionId, { signal });
33
33
  },
@@ -49,7 +49,7 @@ export function useSessionBackgroundCommands(
49
49
  async (commandId: string): Promise<void> => {
50
50
  if (!sessionId) return;
51
51
  if (!client.cancelSessionBackgroundCommand) {
52
- throw new Error("The configured OpenGeni client does not support background commands");
52
+ throw new Error("The configured Opengeni client does not support background commands");
53
53
  }
54
54
  const ticket = generation.current;
55
55
  await client.cancelSessionBackgroundCommand(workspaceId, sessionId, commandId);
@@ -15,7 +15,7 @@ export function useVideoArtifactPlaybackLoader(
15
15
  return useMemo(
16
16
  () => async (artifactId: string, signal?: AbortSignal) => {
17
17
  if (typeof client.createVideoArtifactPlaybackSource !== "function") {
18
- throw new Error("This OpenGeni host does not expose retained-video playback.");
18
+ throw new Error("This Opengeni host does not expose retained-video playback.");
19
19
  }
20
20
  return await client.createVideoArtifactPlaybackSource(
21
21
  workspaceId,
@@ -310,8 +310,21 @@ export function resolveHostTheme(element: Element): ResolvedHostTheme {
310
310
  return "light";
311
311
  }
312
312
 
313
- function normalizeToken(value: string): string {
314
- return value.trim().toLowerCase().replace(/\s+/g, " ");
313
+ /**
314
+ * Canonical text for comparing a token value with the stock one. The
315
+ * published `compiled.css` is minified (`#333`, `oklch(.52 .16 22)`), so the
316
+ * same color must compare equal in short and long form; otherwise every stock
317
+ * token reads as a host customization and dark values leak into light themes.
318
+ */
319
+ export function normalizeToken(value: string): string {
320
+ return value
321
+ .trim()
322
+ .toLowerCase()
323
+ .replace(/\s+/g, " ")
324
+ .replace(/#([\da-f])([\da-f])([\da-f])(?![\da-f])/g, "#$1$1$2$2$3$3")
325
+ .replace(/(^|[\s(,/])0+(\.\d)/g, "$1$2")
326
+ .replace(/(\.\d*?)0+(?=[\s),/%]|$)/g, "$1")
327
+ .replace(/\.(?=[\s),/%]|$)/g, "");
315
328
  }
316
329
 
317
330
  /**
@@ -228,7 +228,7 @@ function workspaceProviderPayerSummary(model: ClientModel): string {
228
228
  if (model.provider === "workspace-anthropic")
229
229
  return "Billed to the workspace Anthropic API account";
230
230
  if (model.provider === "workspace-claude-subscription")
231
- return "Uses the workspace Claude subscription · no OpenGeni credits";
231
+ return "Uses the workspace Claude subscription · no Opengeni credits";
232
232
  if (model.provider === "workspace-openrouter") {
233
233
  return "Billed to the workspace OpenRouter account";
234
234
  }
@@ -242,7 +242,7 @@ function organizationProviderPayerSummary(model: ClientModel): string {
242
242
  if (model.provider === "organization-anthropic")
243
243
  return "Billed to the organization Anthropic API account";
244
244
  if (model.provider === "organization-claude-subscription")
245
- return "Uses the connected Claude subscription · no OpenGeni credits";
245
+ return "Uses the connected Claude subscription · no Opengeni credits";
246
246
  if (model.provider === "organization-openrouter") {
247
247
  return "Billed to the organization OpenRouter account";
248
248
  }
@@ -129,7 +129,7 @@ function useEmbeddedClientRefinement<TClient extends object>(
129
129
  const workspaceId = override.workspaceId ?? context?.workspaceId;
130
130
  if (!candidate || !workspaceId) {
131
131
  throw new Error(
132
- "@opengeni/react: no OpenGeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook.",
132
+ "@opengeni/react: no Opengeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook.",
133
133
  );
134
134
  }
135
135
  const methods = candidate as Record<string, unknown>;
@@ -164,7 +164,7 @@ export function useOpenGeni(override: ClientOverride = {}): OpenGeniContextValue
164
164
  const workspaceId = override.workspaceId ?? context?.workspaceId;
165
165
  if (!client || !workspaceId) {
166
166
  throw new Error(
167
- "@opengeni/react: no OpenGeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook.",
167
+ "@opengeni/react: no Opengeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook.",
168
168
  );
169
169
  }
170
170
  return {
@@ -423,7 +423,7 @@ export function useOpenGeniClient(
423
423
  const client = override.client ?? context?.client;
424
424
  if (!client) {
425
425
  throw new Error(
426
- "@opengeni/react: no OpenGeni client available. Wrap the tree in <OpenGeniProvider> or pass { client } to the hook.",
426
+ "@opengeni/react: no Opengeni client available. Wrap the tree in <OpenGeniProvider> or pass { client } to the hook.",
427
427
  );
428
428
  }
429
429
  return client;
@@ -2163,6 +2163,15 @@ function groupReadableTurns(items: TimelineItem[]): TimelineGroup[] {
2163
2163
  foldedProse.add(message);
2164
2164
  group.work!.details.push({ kind: "item", item: message });
2165
2165
  }
2166
+ } else {
2167
+ // Live progress stays primary above the work row, and is also listed in
2168
+ // the work history so an expanded disclosure reads like the settled one.
2169
+ // The timeline folds the outside copies only while that disclosure is open.
2170
+ for (const message of prose) {
2171
+ if (message.phase === "final_answer" || message.text.includes("![")) continue;
2172
+ group.work!.details.push({ kind: "item", item: message });
2173
+ (group.work!.liveNoteIds ??= []).push(message.id);
2174
+ }
2166
2175
  }
2167
2176
  // Preserve event chronology, not completion timestamps or activity kinds.
2168
2177
  group.work!.details.sort((a, b) => {
@@ -4,6 +4,7 @@ import {
4
4
  createContext,
5
5
  useContext,
6
6
  useEffect,
7
+ useLayoutEffect,
7
8
  useMemo,
8
9
  useRef,
9
10
  useState,
@@ -144,6 +145,8 @@ export type TurnSummaryStatus = Readonly<{
144
145
  }>;
145
146
 
146
147
  export type TurnSummaryProps = {
148
+ /** Reports the disclosure's actual open state on mount and on every change. */
149
+ onOpenStateChange?: ((open: boolean) => void) | undefined;
147
150
  /** The activity items in the turn (used only to compute the facet counts). */
148
151
  items: ActivityItem[];
149
152
  /**
@@ -226,6 +229,7 @@ export function TurnSummary({
226
229
  copyText,
227
230
  contextCompactionCount,
228
231
  status,
232
+ onOpenStateChange,
229
233
  children,
230
234
  }: TurnSummaryProps) {
231
235
  // An explicit `defaultOpen` always wins; otherwise an ancestor may seed it
@@ -247,6 +251,12 @@ export function TurnSummary({
247
251
  const initialSettle = Boolean(settleFold) && !restingOpen && remembered === undefined;
248
252
  const [settling, setSettling] = useState(initialSettle);
249
253
  const [open, setOpen] = useState(initialSettle ? true : restingOpen);
254
+ const reportOpen = useRef(onOpenStateChange);
255
+ reportOpen.current = onOpenStateChange;
256
+ // Before paint: a listener may need to start motion in the same frame.
257
+ useLayoutEffect(() => {
258
+ reportOpen.current?.(open);
259
+ }, [open]);
250
260
  const readerOwnsOpen = useRef(remembered !== undefined);
251
261
  useEffect(() => {
252
262
  // Readable work can produce primary media or fail after mounting. Reveal
@@ -586,6 +586,12 @@ export type TimelineGroup =
586
586
  responseStartedAt?: string;
587
587
  waiting?: { label: string; since: string };
588
588
  details: TimelineGroup[];
589
+ /**
590
+ * While the turn is live, its progress notes stay readable above the
591
+ * work row and are also listed in `details`. These are their item ids,
592
+ * so an expanded work disclosure can fold the outside copies away.
593
+ */
594
+ liveNoteIds?: string[];
589
595
  };
590
596
  }
591
597
  | {