@athenaintel/react 0.36.1 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -623,8 +623,11 @@ workbench the layout opens no asset column of its own: assets the agent or the
623
623
  person opens show as the workbench's **Open assets** tab (closing the tab
624
624
  closes the asset panel). Maximize is offered only inside `AthenaLayout`, which
625
625
  owns the column's width; collapsing a maximized workspace brings the chat back
626
- beside the rail. `SessionWorkbench` also works on its own anywhere under
627
- `<AthenaProvider>`; it then keeps its collapsed state itself.
626
+ beside the rail. Each session keeps its own open or collapsed workspace while
627
+ the layout stays mounted, so `defaultWorkbenchCollapsed` is how a session the
628
+ person has not opened it in starts. `SessionWorkbench` also works on its own
629
+ anywhere under `<AthenaProvider>`; it then keeps its collapsed state itself,
630
+ per session too.
628
631
 
629
632
  Built-in panes and what each reads:
630
633
 
@@ -774,6 +777,25 @@ Both use the same bearer / API key as session creation. The full recipe
774
777
  template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
775
778
  exported if you hold the bridge state yourself.
776
779
 
780
+ ## What's new in 0.38.0
781
+
782
+ **A brief sign-in hiccup no longer shows your app's sign-in screen.** `AthenaSSOProvider` read the session (`/api/sso/userinfo`) once and showed `displayOnError` on any failure, so a person who was signed in saw the sign-in screen until they reloaded. On computer apps this happens when several tabs load at once: Marathon's credential mint is briefly rate limited and answers `502 {"error":"mint_failed"}`. Each read is now one bounded cycle, the one the parent bridge runs for `/_athena/auth`:
783
+
784
+ - A transient failure (5xx, 408, 429, a network error, or no answer within 10 s) is retried with capped exponential backoff and jitter: at most 4 requests in all, about 0.5 s, 1 s and 2 s apart.
785
+ - A 401 or 403 (no session) is not retried, and neither is another status or a 2xx without a JSON body.
786
+ - A read that gives up reports one `AthenaSdkError`, with the new code `auth_sso_failed`, the HTTP `status` and `context.attempts`, to the provider's new `onError` prop and on the diagnostics bus. Pass `onError` to `AthenaSSOProvider` to observe it: an `AthenaProvider` nested inside is mounted only after sign-in, so its `onError` cannot see the first read. Nothing reads again on its own after that, except a forced token renewal or a focus revalidation (`revalidateOnWindowFocus`), each one bounded read.
787
+ - A focus revalidation or a forced renewal that starts while a read is in flight joins that read instead of cancelling it, so one mint answers both. A read that fails keeps the session an earlier read answered.
788
+
789
+ **`displayOnError` can tell a signed-out person from a failed read.** Pass a function and it receives `{ reason, retry }`. `reason` is `unauthenticated` (no session: offer sign-in), `request_failed` (the endpoint did not answer usably, even after retrying: the person may well be signed in, so offer `retry`) or `invalid_response`. `retry()` reads the session again and shows `displayWhileLoading` meanwhile. An element works as before. The types are `AthenaSSOFailure` and `AthenaSSOFailureReason`, from `@athenaintel/react/auth`.
790
+
791
+ New diagnostics event: `auth.sso.userinfo.retry` (level `warn`, one for each retry, with `status`, `reason`, `attempt`, `max_attempts` and `delay_ms`). A read that gives up emits `auth.sso.userinfo.error`.
792
+
793
+ No host code changes are needed. To stop showing a sign-in screen after a failed read, pass `displayOnError` as a function and branch on `reason` (see [Loading, sign-in and failures](#loading-sign-in-and-failures)).
794
+
795
+ ## What's new in 0.37.0
796
+
797
+ **Each session keeps its own open or collapsed workspace.** `SessionWorkbench` already kept each session's tabs; it now keeps whether its workspace is open too. A session the person has not opened or collapsed it in, a new one or one switched to, shows `AthenaLayout`'s `defaultWorkbenchCollapsed`. With `defaultWorkbenchCollapsed`, every session starts with the workspace on its rail, and opening it in one session no longer opens it in the next. An asset the agent or the person opens still brings it out, for that session. Without the prop nothing changes for a session that has not collapsed it, but collapsing one no longer collapses the others.
798
+
777
799
  ## What's new in 0.36.1
778
800
 
779
801
  **Opening a long session no longer jumps the transcript around.** As older history pages in above what you are reading, the reading position now stays put, instantly. Before, the chat added its own scroll correction on top of the browser's scroll anchoring (a jump by a page's height), let it play through the viewport's smooth scrolling (a fast scroll through the transcript), and missed a page that folds into the first loaded turn, which moved the text under the reader. Cut tool outputs are now read only for messages near the viewport, at most two at a time, instead of every loaded step at once: opening a long agent run sent dozens of reads, each re-rendering the transcript and all queued ahead of your next message. Nothing to change in your app.
@@ -1490,6 +1512,27 @@ import { AthenaSSOProvider } from '@athenaintel/react/auth';
1490
1512
 
1491
1513
  If you need full control, `ssoUserInfoUrl`, `ssoLoginUrl`, and `ssoLogoutUrl` remain available as explicit overrides.
1492
1514
 
1515
+ ### Loading, sign-in and failures
1516
+
1517
+ ```tsx
1518
+ <AthenaSSOProvider
1519
+ displayWhileLoading={<AppSkeleton />}
1520
+ displayOnError={({ reason, retry }) =>
1521
+ reason === 'unauthenticated' ? <SignIn /> : <SignInCheckFailed onRetry={retry} />
1522
+ }
1523
+ >
1524
+ <AthenaProvider>
1525
+ <AthenaChat />
1526
+ </AthenaProvider>
1527
+ </AthenaSSOProvider>
1528
+ ```
1529
+
1530
+ - `displayWhileLoading` shows while the provider reads the session. On a normal load that takes well under a second, so an outline of your app reads better than words.
1531
+ - A transient failure (5xx, 408, 429, a network error, a timeout) is retried within the read, about 0.5 s, 1 s and 2 s apart, before `displayOnError` shows.
1532
+ - Only `unauthenticated` means the person has no session. `request_failed` means the endpoint did not answer usably; the person may well be signed in, so offer `retry` rather than sign-in.
1533
+ - `onError` receives the typed `auth_sso_failed` error of a read that gave up.
1534
+ - Without `displayOnError`, a person with no session is sent to `ssoLoginUrl`.
1535
+
1493
1536
  ## Citation Links
1494
1537
 
1495
1538
  When you render chat inside `<AthenaLayout>`, Athena citation links (`app.athenaintel.com/dashboard/spaces?...`) now open the referenced asset in the SDK asset pane by default instead of navigating away.
@@ -96,8 +96,15 @@ const WorkbenchSplitLayout = ({
96
96
  )
97
97
  });
98
98
  const layoutState = react.useMemo(
99
- () => ({ collapsed, setCollapsed, maximized, setMaximized, hostsAssetPanel: true }),
100
- [collapsed, maximized, setCollapsed, setMaximized]
99
+ () => ({
100
+ collapsed,
101
+ setCollapsed,
102
+ defaultCollapsed: defaultWorkbenchCollapsed,
103
+ maximized,
104
+ setMaximized,
105
+ hostsAssetPanel: true
106
+ }),
107
+ [collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized]
101
108
  );
102
109
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "flex h-full w-full", children: [
103
110
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1 +1 @@
1
- {"version":3,"file":"AthenaLayout.cjs","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /** Start with the workbench collapsed to its rail. Defaults to `false`. */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({ collapsed, setCollapsed, maximized, setMaximized, hostsAssetPanel: true }),\n [collapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":["useRef","useCallback","useAssetPanelStore","useEffect","jsx","useState","jsxs","AssetPanel","useMemo","cn","WorkbenchLayoutContext"],"mappings":"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"}
1
+ {"version":3,"file":"AthenaLayout.cjs","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /**\n * Whether a session's workbench starts collapsed to its rail. Each session\n * keeps its own open or collapsed workbench while the layout stays mounted,\n * as it keeps its own tabs, so this is how every session the person has not\n * opened it in shows it — a new one, or one switched to. Defaults to `false`.\n */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({\n collapsed,\n setCollapsed,\n defaultCollapsed: defaultWorkbenchCollapsed,\n maximized,\n setMaximized,\n hostsAssetPanel: true,\n }),\n [collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":["useRef","useCallback","useAssetPanelStore","useEffect","jsx","useState","jsxs","AssetPanel","useMemo","cn","WorkbenchLayoutContext"],"mappings":"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"}
@@ -15,7 +15,12 @@ export interface AthenaLayoutProps {
15
15
  workbench?: ReactNode;
16
16
  /** Default width (%) of the workbench column. Defaults to 40. */
17
17
  defaultWorkbenchPercent?: number;
18
- /** Start with the workbench collapsed to its rail. Defaults to `false`. */
18
+ /**
19
+ * Whether a session's workbench starts collapsed to its rail. Each session
20
+ * keeps its own open or collapsed workbench while the layout stays mounted,
21
+ * as it keeps its own tabs, so this is how every session the person has not
22
+ * opened it in shows it — a new one, or one switched to. Defaults to `false`.
23
+ */
19
24
  defaultWorkbenchCollapsed?: boolean;
20
25
  }
21
26
  /**
@@ -94,8 +94,15 @@ const WorkbenchSplitLayout = ({
94
94
  )
95
95
  });
96
96
  const layoutState = useMemo(
97
- () => ({ collapsed, setCollapsed, maximized, setMaximized, hostsAssetPanel: true }),
98
- [collapsed, maximized, setCollapsed, setMaximized]
97
+ () => ({
98
+ collapsed,
99
+ setCollapsed,
100
+ defaultCollapsed: defaultWorkbenchCollapsed,
101
+ maximized,
102
+ setMaximized,
103
+ hostsAssetPanel: true
104
+ }),
105
+ [collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized]
99
106
  );
100
107
  return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "flex h-full w-full", children: [
101
108
  /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"file":"AthenaLayout.js","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /** Start with the workbench collapsed to its rail. Defaults to `false`. */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({ collapsed, setCollapsed, maximized, setMaximized, hostsAssetPanel: true }),\n [collapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AthenaLayout.js","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /**\n * Whether a session's workbench starts collapsed to its rail. Each session\n * keeps its own open or collapsed workbench while the layout stays mounted,\n * as it keeps its own tabs, so this is how every session the person has not\n * opened it in shows it — a new one, or one switched to. Defaults to `false`.\n */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({\n collapsed,\n setCollapsed,\n defaultCollapsed: defaultWorkbenchCollapsed,\n maximized,\n setMaximized,\n hostsAssetPanel: true,\n }),\n [collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
@@ -1,8 +1,129 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
+ const reactQuery = require("@tanstack/react-query");
4
5
  const react = require("react");
6
+ const athenaDiagnostics = require("../diagnostics/athena-diagnostics.cjs");
7
+ const errors = require("../diagnostics/errors.cjs");
8
+ const auth = require("../runtime/auth.cjs");
5
9
  const AthenaAuthContext = require("./AthenaAuthContext.cjs");
10
+ const SSO_USERINFO_RETRY_POLICY = {
11
+ ...auth.DEFAULT_MINT_RETRY_POLICY,
12
+ timeoutMs: 1e4
13
+ };
14
+ const abortError = () => new DOMException("The session read was cancelled.", "AbortError");
15
+ async function readUserInfoOnce({
16
+ url,
17
+ signal,
18
+ timeoutMs
19
+ }) {
20
+ if (signal.aborted) throw abortError();
21
+ const request = new AbortController();
22
+ const cancel = () => request.abort();
23
+ signal.addEventListener("abort", cancel, { once: true });
24
+ let timedOut = false;
25
+ const timer = setTimeout(() => {
26
+ timedOut = true;
27
+ request.abort();
28
+ }, timeoutMs);
29
+ try {
30
+ const response = await fetch(url, { credentials: "include", signal: request.signal });
31
+ if (response.status === 401 || response.status === 403) return { kind: "unauthenticated" };
32
+ if (!response.ok) {
33
+ return auth.isRetryableMintStatus(response.status) ? { kind: "transient", status: response.status, reason: "status" } : { kind: "failed", status: response.status };
34
+ }
35
+ let data;
36
+ try {
37
+ data = await response.json();
38
+ } catch (error) {
39
+ if (signal.aborted) throw abortError();
40
+ return timedOut ? { kind: "transient", status: null, reason: "timeout", cause: error } : { kind: "failed", status: response.status };
41
+ }
42
+ return isValidSSOUserInfo(data) ? { kind: "session", data } : { kind: "invalid_response" };
43
+ } catch (error) {
44
+ if (signal.aborted) throw abortError();
45
+ return { kind: "transient", status: null, reason: timedOut ? "timeout" : "network", cause: error };
46
+ } finally {
47
+ clearTimeout(timer);
48
+ signal.removeEventListener("abort", cancel);
49
+ }
50
+ }
51
+ async function readSession({
52
+ url,
53
+ signal,
54
+ policy
55
+ }) {
56
+ const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
57
+ for (let attempts = 1; ; attempts += 1) {
58
+ const attempt = await readUserInfoOnce({ url, signal, timeoutMs: policy.timeoutMs });
59
+ if (attempt.kind === "session") return { kind: "session", data: attempt.data };
60
+ if (attempt.kind === "unauthenticated") return { kind: "no_session", reason: "unauthenticated" };
61
+ if (attempt.kind === "invalid_response") {
62
+ console.error(
63
+ '[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
64
+ );
65
+ return { kind: "no_session", reason: "invalid_response" };
66
+ }
67
+ if (attempt.kind === "transient" && attempts < maxAttempts) {
68
+ const delayMs = auth.getMintRetryDelay({ retry: attempts, policy });
69
+ athenaDiagnostics.athenaDiagnostics.emit("auth.sso.userinfo.retry", {
70
+ level: "warn",
71
+ data: {
72
+ status: attempt.status,
73
+ reason: attempt.reason,
74
+ attempt: attempts,
75
+ max_attempts: maxAttempts,
76
+ delay_ms: delayMs
77
+ }
78
+ });
79
+ await waitBeforeRetry({ delayMs, signal });
80
+ continue;
81
+ }
82
+ throw sessionReadError({ url, attempt, attempts });
83
+ }
84
+ }
85
+ function waitBeforeRetry({ delayMs, signal }) {
86
+ return new Promise((resolve, reject) => {
87
+ if (signal.aborted) {
88
+ reject(abortError());
89
+ return;
90
+ }
91
+ const onAbort = () => {
92
+ clearTimeout(timer);
93
+ reject(abortError());
94
+ };
95
+ const timer = setTimeout(() => {
96
+ signal.removeEventListener("abort", onAbort);
97
+ resolve();
98
+ }, delayMs);
99
+ signal.addEventListener("abort", onAbort, { once: true });
100
+ });
101
+ }
102
+ function sessionReadError({
103
+ url,
104
+ attempt,
105
+ attempts
106
+ }) {
107
+ if (attempt.kind === "failed") {
108
+ const unreadable = attempt.status >= 200 && attempt.status < 300;
109
+ return new errors.AthenaSdkError({
110
+ code: "auth_sso_failed",
111
+ message: unreadable ? `The SSO session read (${url}) answered HTTP ${attempt.status} without a JSON body.` : `The SSO session read (${url}) answered HTTP ${attempt.status}.`,
112
+ hint: "Check that ssoBaseUrl / ssoUserInfoUrl point at the SSO userinfo endpoint.",
113
+ status: attempt.status,
114
+ context: { attempts, reason: unreadable ? "invalid_body" : "status" }
115
+ });
116
+ }
117
+ const answer = attempt.status !== null ? `HTTP ${attempt.status}` : attempt.reason === "timeout" ? "a timeout" : "a network error";
118
+ return new errors.AthenaSdkError({
119
+ code: "auth_sso_failed",
120
+ message: `The SSO session read (${url}) failed ${attempts} time${attempts === 1 ? "" : "s"} (last: ${answer}).`,
121
+ hint: "The sign-in service did not answer; the person may still be signed in. Offer a retry (displayOnError receives retry).",
122
+ ...attempt.status !== null && { status: attempt.status },
123
+ ...attempt.cause !== void 0 && { cause: attempt.cause },
124
+ context: { attempts, reason: attempt.reason }
125
+ });
126
+ }
6
127
  const DEFAULT_SSO_PATHS = {
7
128
  userInfo: "/api/sso/userinfo",
8
129
  login: "/api/sso/initiate",
@@ -54,13 +175,15 @@ function AthenaSSOProvider({
54
175
  ssoLogoutUrl,
55
176
  displayWhileLoading,
56
177
  displayOnError,
57
- revalidateOnWindowFocus = false
178
+ onError,
179
+ revalidateOnWindowFocus = false,
180
+ retryPolicy = SSO_USERINFO_RETRY_POLICY
58
181
  }) {
59
- const [ssoData, setSSOData] = react.useState(null);
60
- const [loading, setLoading] = react.useState(true);
61
- const [error, setError] = react.useState(null);
62
- const activeRequestRef = react.useRef(null);
63
- const activeReadRef = react.useRef(null);
182
+ const [queryClient] = react.useState(() => new reactQuery.QueryClient());
183
+ const retryPolicyRef = react.useRef(retryPolicy);
184
+ retryPolicyRef.current = retryPolicy;
185
+ const onErrorRef = react.useRef(onError);
186
+ onErrorRef.current = onError;
64
187
  const ssoUrls = react.useMemo(
65
188
  () => ({
66
189
  userInfo: resolveSSOUrl({
@@ -81,101 +204,75 @@ function AthenaSSOProvider({
81
204
  }),
82
205
  [ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl]
83
206
  );
84
- const refreshSession = react.useCallback(
85
- ({ showLoading = false } = {}) => {
207
+ const queryKey = react.useMemo(() => ["athena-sdk", "sso-session", ssoUrls.userInfo], [ssoUrls.userInfo]);
208
+ const queryFn = react.useCallback(
209
+ async ({ signal }) => {
86
210
  var _a;
87
- (_a = activeRequestRef.current) == null ? void 0 : _a.abort();
88
- const abortController = new AbortController();
89
- activeRequestRef.current = abortController;
90
- const read = (async () => {
91
- if (showLoading) {
92
- setLoading(true);
93
- setError(null);
94
- setSSOData(null);
95
- }
96
- try {
97
- const response = await fetch(ssoUrls.userInfo, {
98
- credentials: "include",
99
- signal: abortController.signal
100
- });
101
- if (!response.ok) {
102
- if (response.status === 401 || response.status === 403) {
103
- setSSOData(null);
104
- setError("unauthenticated");
105
- } else {
106
- setError("request_failed");
107
- }
108
- return null;
109
- }
110
- const data = await response.json();
111
- if (!isValidSSOUserInfo(data)) {
112
- console.error(
113
- '[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
114
- );
115
- setSSOData(null);
116
- setError("invalid_response");
117
- return null;
118
- }
119
- setSSOData((prev) => {
120
- if (prev && prev.accessToken === data.accessToken && prev.user.user_id === data.user.user_id) {
121
- return prev;
122
- }
123
- return data;
124
- });
125
- setError(null);
126
- return data;
127
- } catch (fetchError) {
128
- if (fetchError instanceof DOMException && fetchError.name === "AbortError") {
129
- return activeRequestRef.current !== abortController ? activeReadRef.current : null;
130
- }
131
- setError("request_failed");
132
- return null;
133
- } finally {
134
- if (activeRequestRef.current === abortController) {
135
- activeRequestRef.current = null;
136
- setLoading(false);
211
+ try {
212
+ return await readSession({ url: ssoUrls.userInfo, signal, policy: retryPolicyRef.current });
213
+ } catch (readError) {
214
+ if (readError instanceof errors.AthenaSdkError) {
215
+ athenaDiagnostics.athenaDiagnostics.emit("auth.sso.userinfo.error", { error: readError });
216
+ try {
217
+ (_a = onErrorRef.current) == null ? void 0 : _a.call(onErrorRef, readError);
218
+ } catch (callbackError) {
219
+ console.error("[AthenaSDK] AthenaSSOProvider onError threw", callbackError);
137
220
  }
138
221
  }
139
- })();
140
- activeReadRef.current = read;
141
- return read;
222
+ throw readError;
223
+ }
142
224
  },
143
225
  [ssoUrls.userInfo]
144
226
  );
145
- react.useEffect(() => {
146
- void refreshSession({ showLoading: true });
147
- return () => {
148
- var _a;
149
- (_a = activeRequestRef.current) == null ? void 0 : _a.abort();
150
- };
151
- }, [refreshSession]);
227
+ const query = reactQuery.useQuery(
228
+ {
229
+ queryKey,
230
+ queryFn,
231
+ // `readSession` retries a transient failure itself, with the bridge's
232
+ // policy; the query never retries on top of it, and reads while the
233
+ // browser reports itself offline too, so that policy decides.
234
+ retry: false,
235
+ networkMode: "always",
236
+ staleTime: Number.POSITIVE_INFINITY,
237
+ refetchOnWindowFocus: false,
238
+ refetchOnReconnect: false
239
+ },
240
+ queryClient
241
+ );
242
+ const answer = query.data;
243
+ const session = (answer == null ? void 0 : answer.kind) === "session" ? answer.data : null;
244
+ const loading = query.isPending;
245
+ const reason = session ? null : (answer == null ? void 0 : answer.kind) === "no_session" ? answer.reason : query.isError ? "request_failed" : null;
246
+ const signedOut = (answer == null ? void 0 : answer.kind) === "no_session" && answer.signedOut === true;
152
247
  react.useEffect(() => {
153
248
  if (!revalidateOnWindowFocus || typeof window === "undefined") {
154
249
  return;
155
250
  }
156
- const handleFocus = () => {
157
- void refreshSession();
251
+ const revalidate = () => {
252
+ void queryClient.refetchQueries({ queryKey }, { cancelRefetch: false });
158
253
  };
159
254
  const handleVisibilityChange = () => {
160
255
  if (document.visibilityState === "visible") {
161
- void refreshSession();
256
+ revalidate();
162
257
  }
163
258
  };
164
- window.addEventListener("focus", handleFocus);
259
+ window.addEventListener("focus", revalidate);
165
260
  document.addEventListener("visibilitychange", handleVisibilityChange);
166
261
  return () => {
167
- window.removeEventListener("focus", handleFocus);
262
+ window.removeEventListener("focus", revalidate);
168
263
  document.removeEventListener("visibilitychange", handleVisibilityChange);
169
264
  };
170
- }, [revalidateOnWindowFocus, refreshSession]);
265
+ }, [revalidateOnWindowFocus, queryClient, queryKey]);
171
266
  react.useEffect(() => {
172
- if (!loading && error === "unauthenticated" && !displayOnError && typeof window !== "undefined") {
267
+ if (!loading && reason === "unauthenticated" && !signedOut && !displayOnError && typeof window !== "undefined") {
173
268
  window.location.assign(ssoUrls.login);
174
269
  }
175
- }, [loading, error, displayOnError, ssoUrls.login]);
270
+ }, [loading, reason, signedOut, displayOnError, ssoUrls.login]);
271
+ const retry = react.useCallback(() => {
272
+ void queryClient.resetQueries({ queryKey });
273
+ }, [queryClient, queryKey]);
176
274
  const logout = react.useCallback(async (redirectOnLogout = true) => {
177
- var _a;
178
- (_a = activeRequestRef.current) == null ? void 0 : _a.abort();
275
+ await queryClient.cancelQueries({ queryKey });
179
276
  try {
180
277
  await fetch(ssoUrls.logout, {
181
278
  method: "POST",
@@ -184,25 +281,36 @@ function AthenaSSOProvider({
184
281
  } catch (logoutError) {
185
282
  console.error("[AthenaSDK] Failed to invalidate SSO session during logout", logoutError);
186
283
  }
187
- setSSOData(null);
188
- setError(null);
189
- setLoading(false);
284
+ queryClient.setQueryData(queryKey, {
285
+ kind: "no_session",
286
+ reason: "unauthenticated",
287
+ signedOut: true
288
+ });
190
289
  if (redirectOnLogout && typeof window !== "undefined") {
191
290
  window.location.assign(ssoUrls.login);
192
291
  }
193
- }, [ssoUrls.login, ssoUrls.logout]);
194
- const ssoDataRef = react.useRef(ssoData);
195
- ssoDataRef.current = ssoData;
292
+ }, [queryClient, queryKey, ssoUrls.login, ssoUrls.logout]);
293
+ const sessionRef = react.useRef(session);
294
+ sessionRef.current = session;
196
295
  const getToken = react.useCallback(
197
296
  async ({ force = false } = {}) => {
198
297
  var _a;
199
298
  if (force) {
200
- const renewed = await refreshSession();
201
- if (renewed) return renewed.accessToken;
299
+ try {
300
+ const renewed = await queryClient.fetchQuery({
301
+ queryKey,
302
+ queryFn,
303
+ staleTime: 0,
304
+ retry: false,
305
+ networkMode: "always"
306
+ });
307
+ if (renewed.kind === "session") return renewed.data.accessToken;
308
+ } catch {
309
+ }
202
310
  }
203
- return ((_a = ssoDataRef.current) == null ? void 0 : _a.accessToken) ?? null;
311
+ return ((_a = sessionRef.current) == null ? void 0 : _a.accessToken) ?? null;
204
312
  },
205
- [refreshSession]
313
+ [queryClient, queryKey, queryFn]
206
314
  );
207
315
  const authState = react.useMemo(() => {
208
316
  if (loading) {
@@ -217,19 +325,7 @@ function AthenaSSOProvider({
217
325
  getToken
218
326
  };
219
327
  }
220
- if (error === "request_failed" && ssoData) {
221
- const { user: user2, orgs: orgs2, accessToken: accessToken2 } = mapSSOResponse(ssoData);
222
- return {
223
- isLoggedIn: true,
224
- isLoading: false,
225
- user: user2,
226
- accessToken: accessToken2,
227
- orgs: orgs2,
228
- logout,
229
- getToken
230
- };
231
- }
232
- if (error || !ssoData) {
328
+ if (!session) {
233
329
  return {
234
330
  isLoggedIn: false,
235
331
  isLoading: false,
@@ -240,7 +336,7 @@ function AthenaSSOProvider({
240
336
  getToken
241
337
  };
242
338
  }
243
- const { user, orgs, accessToken } = mapSSOResponse(ssoData);
339
+ const { user, orgs, accessToken } = mapSSOResponse(session);
244
340
  return {
245
341
  isLoggedIn: true,
246
342
  isLoading: false,
@@ -250,11 +346,14 @@ function AthenaSSOProvider({
250
346
  logout,
251
347
  getToken
252
348
  };
253
- }, [loading, error, ssoData, logout, getToken]);
349
+ }, [loading, session, logout, getToken]);
254
350
  if (authState.isLoading) {
255
351
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayWhileLoading ?? null });
256
352
  }
257
353
  if (!authState.isLoggedIn) {
354
+ if (typeof displayOnError === "function") {
355
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayOnError({ reason: reason ?? "unauthenticated", retry }) });
356
+ }
258
357
  return displayOnError ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayOnError }) : null;
259
358
  }
260
359
  return /* @__PURE__ */ jsxRuntime.jsx(AthenaAuthContext.AthenaAuthContext.Provider, { value: authState, children });