@alexkroman1/aai-ui 13.3.0 → 14.0.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 (73) hide show
  1. package/README.md +159 -69
  2. package/dist/{_colors-CZ6OlPbL.js → _colors-CpZO-88A.js} +24 -1
  3. package/dist/_recover-run.d.ts +2 -2
  4. package/dist/_submission-state.d.ts +92 -0
  5. package/dist/_upload-files.d.ts +2 -2
  6. package/dist/_upload-report.d.ts +26 -0
  7. package/dist/{_utils-CyzjK0gW.js → _utils-DnQDM9Uy.js} +3 -3
  8. package/dist/_utils.d.ts +3 -3
  9. package/dist/_web-storage.d.ts +43 -0
  10. package/dist/_workflow-files.d.ts +1 -1
  11. package/dist/agent-state-labels.d.ts +60 -0
  12. package/dist/audio.js +20 -14
  13. package/dist/{chat-view-Bv5VFJIE.js → chat-view-C_3T7Ln8.js} +96 -29
  14. package/dist/{client-config-DD820zHn.js → client-config-DJQHnYjm.js} +4 -4
  15. package/dist/client-config.d.ts +4 -4
  16. package/dist/client-dir.d.ts +1 -1
  17. package/dist/client-dir.js +1 -1
  18. package/dist/components/_colors.d.ts +23 -0
  19. package/dist/components/_form-readiness.d.ts +1 -1
  20. package/dist/components/bullet-list.d.ts +74 -0
  21. package/dist/components/button.js +3 -3
  22. package/dist/components/chat-view.js +1 -1
  23. package/dist/components/console-shell.d.ts +16 -20
  24. package/dist/components/controls.js +3 -3
  25. package/dist/components/facts.d.ts +81 -0
  26. package/dist/components/form-fields.d.ts +6 -6
  27. package/dist/components/form-types.d.ts +1 -1
  28. package/dist/components/form.d.ts +1 -1
  29. package/dist/components/message-list.js +1 -1
  30. package/dist/components/session-error-banner.d.ts +69 -0
  31. package/dist/components/start-screen.js +1 -1
  32. package/dist/components/tool-call-block.js +1 -1
  33. package/dist/components/tool-config-context.d.ts +1 -1
  34. package/dist/components/workflow-progress.d.ts +11 -4
  35. package/dist/context.d.ts +142 -19
  36. package/dist/context.js +155 -17
  37. package/dist/default-client/assets/{audio-BuDICbPf.js → audio-9zQsNc1w.js} +1 -1
  38. package/dist/default-client/assets/index-BTv30Z4F.css +2 -0
  39. package/dist/default-client/assets/index-RAZ-29Sz.js +284 -0
  40. package/dist/default-client/index.html +2 -2
  41. package/dist/define-client.d.ts +19 -19
  42. package/dist/define-client.js +19 -19
  43. package/dist/hooks.d.ts +44 -8
  44. package/dist/hooks.js +19 -13
  45. package/dist/index.d.ts +11 -7
  46. package/dist/index.js +418 -185
  47. package/dist/internal.d.ts +2 -2
  48. package/dist/internal.js +5 -5
  49. package/dist/{message-list-C0pL7x41.js → message-list-CdOnSh5m.js} +19 -12
  50. package/dist/page.d.ts +11 -11
  51. package/dist/session-core-audio-setup.d.ts +1 -1
  52. package/dist/session-core-dial.d.ts +0 -2
  53. package/dist/{session-core-C9elBIdu.js → session-core-gwePM95B.js} +125 -52
  54. package/dist/session-core-messages.d.ts +2 -2
  55. package/dist/session-core-types.d.ts +58 -1
  56. package/dist/session-core.d.ts +6 -6
  57. package/dist/session-core.js +2 -2
  58. package/dist/session-resume-store.d.ts +3 -3
  59. package/dist/{tool-call-block-Bunc6rCw.js → tool-call-block-C2t_5fpp.js} +27 -9
  60. package/dist/{tool-config-context-Bh8p3DtG.js → tool-config-context-Es4YUzV2.js} +1 -1
  61. package/dist/types.d.ts +19 -4
  62. package/dist/types.js +2 -2
  63. package/dist/{url-chips-C2u7QPv8.js → url-chips-BxhzZgk2.js} +4 -4
  64. package/dist/use-conversation.d.ts +1 -1
  65. package/dist/{use-user-transcript-DFTSEuZN.js → use-user-transcript-uyHhzy4d.js} +3 -2
  66. package/dist/use-workflow-form.d.ts +34 -2
  67. package/dist/{use-workflow-run-CXGEcM0l.js → use-workflow-run-CP2ekKPV.js} +3 -6
  68. package/dist/use-workflow-stream.d.ts +1 -1
  69. package/dist/workflow-client.d.ts +1 -1
  70. package/package.json +2 -2
  71. package/styles.css +78 -0
  72. package/dist/default-client/assets/index-B1_ROnTJ.js +0 -284
  73. package/dist/default-client/assets/index-S5fkKi6B.css +0 -2
@@ -1,5 +1,5 @@
1
1
  import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
2
- import type { FieldShell, FileRead } from "./form-types.ts";
2
+ import type { FieldShell, FileReadMode } from "./form-types.ts";
3
3
  /**
4
4
  * Label + control + hint, in the layout every field here uses.
5
5
  *
@@ -47,7 +47,7 @@ export declare function Field({ label, hint, htmlFor, className, children, }: {
47
47
  *
48
48
  * @public
49
49
  */
50
- export declare function TextField({ name, label, hint, className, ...rest }: FieldShell & Omit<InputHTMLAttributes<HTMLInputElement>, "name" | "className">): import("react").JSX.Element;
50
+ export declare function TextField(props: FieldShell & Omit<InputHTMLAttributes<HTMLInputElement>, "name" | "className">): import("react").JSX.Element;
51
51
  /**
52
52
  * A number input. Contributes a NUMBER to {@link FormValues}, or nothing when
53
53
  * left empty.
@@ -60,7 +60,7 @@ export declare function TextField({ name, label, hint, className, ...rest }: Fie
60
60
  *
61
61
  * @public
62
62
  */
63
- export declare function NumberField({ name, label, hint, className, ...rest }: FieldShell & Omit<InputHTMLAttributes<HTMLInputElement>, "name" | "className" | "type">): import("react").JSX.Element;
63
+ export declare function NumberField(props: FieldShell & Omit<InputHTMLAttributes<HTMLInputElement>, "name" | "className" | "type">): import("react").JSX.Element;
64
64
  /**
65
65
  * A multi-line text input.
66
66
  *
@@ -116,14 +116,14 @@ export declare function CheckboxField({ name, label, hint, className, ...rest }:
116
116
  * travel in it. With `upload` the field contributes the `File` itself,
117
117
  * `useWorkflowSubmit` stores it through `POST /workflows/uploads` before
118
118
  * starting the run, and the input carries the upload id — which a step reads
119
- * windows of with `readUpload`. Declaring the property in the workflow's
119
+ * windows of with `stepReadUpload`. Declaring the property in the workflow's
120
120
  * `uploads` list makes `<WorkflowFields>` render exactly this, so a declared
121
121
  * form needs no file markup at all.
122
122
  *
123
123
  * **Without it the field describes the file and does not read it.** `read`
124
124
  * exists for the cases where the bytes really are small and really are the
125
125
  * input — a CSV of ids, a config — and the size is the author's to check. See
126
- * {@link FileRead} for the four values; `upload` is shorthand for
126
+ * {@link FileReadMode} for the four values; `upload` is shorthand for
127
127
  * `read="upload"`.
128
128
  *
129
129
  * Otherwise accepts every `<input>` attribute except `name`, `className` and
@@ -136,7 +136,7 @@ export declare function CheckboxField({ name, label, hint, className, ...rest }:
136
136
  * @public
137
137
  */
138
138
  export declare function FileField({ name, label, hint, className, read, upload, ...rest }: FieldShell & {
139
- read?: FileRead;
139
+ read?: FileReadMode;
140
140
  /** Shorthand for `read="upload"` — see above. */
141
141
  upload?: boolean;
142
142
  } & Omit<InputHTMLAttributes<HTMLInputElement>, "name" | "className" | "type">): import("react").JSX.Element;
@@ -46,7 +46,7 @@ export type FileValue = {
46
46
  *
47
47
  * @public
48
48
  */
49
- export type FileRead = "none" | "text" | "dataUrl" | "upload";
49
+ export type FileReadMode = "none" | "text" | "dataUrl" | "upload";
50
50
  /**
51
51
  * The props every field in `form.tsx` shares.
52
52
  *
@@ -2,7 +2,7 @@ import type { ButtonHTMLAttributes, FormHTMLAttributes, ReactNode } from "react"
2
2
  import { type ButtonSize, type ButtonVariant } from "./button.tsx";
3
3
  import type { FormValues } from "./form-types.ts";
4
4
  export { CheckboxField, Field, FileField, NumberField, SelectField, TextAreaField, TextField, } from "./form-fields.tsx";
5
- export type { FieldShell, FileRead, FileValue, FormValues } from "./form-types.ts";
5
+ export type { FieldShell, FileReadMode, FileValue, FormValues } from "./form-types.ts";
6
6
  /** Props of {@link Form}. */
7
7
  export type FormProps = {
8
8
  /**
@@ -1,3 +1,3 @@
1
- import { t as MessageList } from "../message-list-C0pL7x41.js";
1
+ import { t as MessageList } from "../message-list-CdOnSh5m.js";
2
2
  import "../context.js";
3
3
  export { MessageList };
@@ -0,0 +1,69 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Props of {@link SessionErrorBanner}.
4
+ *
5
+ * Every field is optional, so `<SessionErrorBanner />` is the whole call. It is
6
+ * a NAMED type all the same, for the reason `ControlsProps` and
7
+ * `ConsoleShellProps` are: an inline object literal in the signature leaves a
8
+ * `createElement(SessionErrorBanner, { className })` caller unable to infer the
9
+ * props at all, and gives the reference page nothing to link to.
10
+ *
11
+ * @public
12
+ */
13
+ export type SessionErrorBannerProps = {
14
+ /** Additional CSS class names for the banner, appended to its own. */
15
+ className?: string | undefined;
16
+ };
17
+ /**
18
+ * The announced banner for a failed session: the error's message and code, or
19
+ * nothing at all when the session is fine.
20
+ *
21
+ * **This used to be four lines inside `ConsoleShell`, and that is why it is its
22
+ * own component.** The banner was the reason `ConsoleShell` was published —
23
+ * `role="alert"` is the one part of that component a reviewer cannot see is
24
+ * missing, since per the `fatalError` latch in `session-core.ts` the banner is
25
+ * the ONLY remaining signal a session died (the state eyebrow beside it goes
26
+ * back to reading like a live session), and a screen reader is never told an
27
+ * unannounced one appeared. But `ConsoleShell` is a whole FRAME: a centred
28
+ * `max-w-190` column with its own header and footer. Every full-bleed chrome —
29
+ * a two-pane board, a CRT — therefore could not adopt it, rebuilt the banner
30
+ * instead, and the three that did had ALREADY drifted: one rendered
31
+ * `ERROR: {message}` and dropped the code entirely, one `ERROR: {message}
32
+ * ({code})`, one `{message} ({code})`. Splitting the banner out is what lets a
33
+ * chrome take the announced-error decision without taking the layout, and
34
+ * `ConsoleShell` composes this rather than keeping a second copy, so the two
35
+ * cannot drift again.
36
+ *
37
+ * **It reads the session itself.** There is no `error` prop: a banner that
38
+ * takes its text from the caller is a banner a caller can forget to wire, which
39
+ * is exactly the failure above with an extra step. It subscribes narrowly via
40
+ * {@link useSessionError}, so a page that renders it does not re-render with
41
+ * the transcript.
42
+ *
43
+ * **The code is shown, always.** `SessionError.code` is the eight-member wire
44
+ * union — it is what a user pastes into a bug report and the only part of the
45
+ * error that is stable across wordings — and the chrome that dropped it left
46
+ * its readers with a sentence and no way to say which failure it was.
47
+ *
48
+ * Must be rendered inside the providers `client()` installs.
49
+ *
50
+ * @example A full-bleed chrome that wants the banner and not the frame
51
+ * ```tsx
52
+ * import { SessionErrorBanner } from "@alexkroman1/aai-ui";
53
+ *
54
+ * function Board() {
55
+ * return (
56
+ * <div className="grid grid-cols-[1fr_320px] h-screen">
57
+ * <main>…</main>
58
+ * <aside>…</aside>
59
+ * <SessionErrorBanner className="col-span-2" />
60
+ * </div>
61
+ * );
62
+ * }
63
+ * ```
64
+ *
65
+ * @param props - See {@link SessionErrorBannerProps}.
66
+ *
67
+ * @public
68
+ */
69
+ export declare function SessionErrorBanner({ className }: SessionErrorBannerProps): ReactNode;
@@ -1,5 +1,5 @@
1
1
  import { useSessionCore, useSessionSelector, useTheme } from "../context.js";
2
- import { r as inkTint } from "../_colors-CZ6OlPbL.js";
2
+ import { a as inkTint } from "../_colors-CpZO-88A.js";
3
3
  import { Button } from "./button.js";
4
4
  import { t as AaiLogo } from "../aai-logo-CFlomZlS.js";
5
5
  import { t as Eyebrow } from "../eyebrow-UfmSz9yy.js";
@@ -1,3 +1,3 @@
1
1
  import "../context.js";
2
- import { t as ToolCallBlock } from "../tool-call-block-Bunc6rCw.js";
2
+ import { t as ToolCallBlock } from "../tool-call-block-C2t_5fpp.js";
3
3
  export { ToolCallBlock };
@@ -8,7 +8,7 @@ export type ToolDisplayConfig = Record<string, {
8
8
  label?: string;
9
9
  }>;
10
10
  /**
11
- * Context for tool display configuration. Installed by `client()` from
11
+ * Context for tool display configuration. Installed by `mountClient()` from
12
12
  * `ClientConfig.tools`; the built-in components read it via `useToolConfig`.
13
13
  *
14
14
  * @internal
@@ -1,17 +1,17 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import type { WorkflowApi } from "../workflow-client.ts";
3
3
  /**
4
4
  * What a run has said so far, rendered.
5
5
  *
6
6
  * The complement of a status line, and the reason both exist: a run is
7
7
  * `running` for its whole life, so a one-round job and a ten-round one look
8
- * identical while they happen. These lines come from the run itself (`report()`
8
+ * identical while they happen. These lines come from the run itself (`stepReport()`
9
9
  * in a `"use step"` body), which is the only channel a workflow has before it
10
10
  * produces an output.
11
11
  *
12
- * Three rules are baked in, and they are why this is a component rather than
12
+ * Four rules are baked in, and they are why this is a component rather than
13
13
  * three lines each page writes for itself — the two templates that had written
14
- * it had written all three, comments included:
14
+ * it had written three of them, comments included:
15
15
  *
16
16
  * - **It renders nothing until there is something to render.** `supported` is
17
17
  * what keeps this from being an empty box forever on an agent deployed before
@@ -25,6 +25,13 @@ import type { WorkflowApi } from "../workflow-client.ts";
25
25
  * or opening a finished run tomorrow — shows how it got there rather than an
26
26
  * empty box. That is `useWorkflowProgress`'s doing; this is what makes it
27
27
  * visible.
28
+ * - **They are ANNOUNCED**, for the reason the first paragraph gives: this is
29
+ * the only channel a run has before it produces an output, and a `<pre>` that
30
+ * grows is a silent one. A screen-reader user pressing "Digest" got nothing
31
+ * between the click and a terminal state minutes later — no "fetching", no
32
+ * "summarising", no evidence the button did anything. See `role="log"` below.
33
+ * The six pages that render this pass only `className`, so no template could
34
+ * have fixed it locally; that is what makes it this component's job.
28
35
  *
29
36
  * @example
30
37
  * ```tsx
package/dist/context.d.ts CHANGED
@@ -1,34 +1,45 @@
1
1
  import { type ReactNode } from "react";
2
- import type { SessionCore, SessionSnapshot } from "./session-core-types.ts";
3
- import type { ClientTheme } from "./types.ts";
2
+ import type { BrowserSession, SessionSnapshot } from "./session-core-types.ts";
3
+ import type { AgentState, ClientTheme, SessionError } from "./types.ts";
4
4
  /**
5
- * Provides the {@link SessionCore} the session hooks read. `client()`
5
+ * Provides the {@link BrowserSession} the session hooks read. `mountClient()`
6
6
  * installs it automatically; a custom tree only needs it when bypassing
7
- * `client()` and mounting React itself.
7
+ * `mountClient()` and mounting React itself.
8
8
  *
9
9
  * @internal
10
10
  */
11
- export declare function SessionProvider({ value, children }: {
12
- value: SessionCore;
11
+ export declare function SessionProvider({ value, children, }: {
12
+ value: BrowserSession;
13
13
  children?: ReactNode;
14
- }): import("react").FunctionComponentElement<import("react").ProviderProps<SessionCore | null>>;
14
+ }): import("react").FunctionComponentElement<import("react").ProviderProps<BrowserSession | null>>;
15
+ /**
16
+ * The session's control methods, and nothing else — what a `client.tsx` may
17
+ * legitimately CALL on a session, as against what it may read.
18
+ *
19
+ * Declared once and merged into {@link Session} rather than written out at both
20
+ * places: the two lists have to be the same list, and a member added to one and
21
+ * not the other is a hook that cannot do what `useSession()` can.
22
+ *
23
+ * Method signatures come from {@link BrowserSession} — one source of truth.
24
+ *
25
+ * @public
26
+ */
27
+ export type SessionActions = Pick<BrowserSession, "start" | "cancel" | "resetState" | "reset" | "restart" | "disconnect" | "toggle" | "end">;
15
28
  /**
16
29
  * What {@link useSession} returns: the live {@link SessionSnapshot} fields
17
30
  * (`state`, `messages`, `toolCalls`, `agentState`, live transcripts, `error`,
18
31
  * `apiUrl`, `started`/`running`/`recording`, …) merged with the session's
19
- * control methods (`start`, `toggle`, `reset`, `resetState`, `disconnect`,
20
- * `cancel`, `end`).
32
+ * control methods (`start`, `toggle`, `reset`, `restart`, `resetState`,
33
+ * `disconnect`, `cancel`, `end`).
21
34
  *
22
35
  * Note there is no text-send method — sessions are voice-only; the only
23
36
  * client→server inputs are audio and the control methods above.
24
37
  *
25
- * Method signatures come from {@link SessionCore} — one source of truth.
26
- *
27
38
  * @public
28
39
  */
29
- export type Session = SessionSnapshot & Pick<SessionCore, "start" | "cancel" | "resetState" | "reset" | "disconnect" | "toggle" | "end">;
40
+ export type Session = SessionSnapshot & SessionActions;
30
41
  /**
31
- * Return the raw {@link SessionCore} from context without subscribing to
42
+ * Return the raw {@link BrowserSession} from context without subscribing to
32
43
  * snapshot changes. Useful for accessing stable methods (`start`, `toggle`,
33
44
  * `reset`, …) from components that select narrow state via
34
45
  * {@link useSessionSelector}.
@@ -36,15 +47,73 @@ export type Session = SessionSnapshot & Pick<SessionCore, "start" | "cancel" | "
36
47
  * Not part of the package's public export surface — internal to aai-ui
37
48
  * components.
38
49
  */
39
- export declare function useSessionCore(): SessionCore;
50
+ export declare function useSessionCore(): BrowserSession;
51
+ /**
52
+ * The session's control methods — `start`, `cancel`, `resetState`, `reset`,
53
+ * `restart`, `disconnect`, `toggle`, `end` — with **no snapshot
54
+ * subscription**.
55
+ *
56
+ * This is the narrow half of {@link useSession}, and it is the half a custom
57
+ * chrome could not reach. `<Controls>` and `<StartScreen>` in this package pair
58
+ * a one-field `useSessionSelector` with this package's own `useSessionCore`
59
+ * (`context.ts`, unpublished); a `client.tsx`
60
+ * could not, because that hook is not published — so a footer needing `start`
61
+ * and `toggle` held a WHOLE-SNAPSHOT `useSession()`, and `session-core.ts`
62
+ * rebuilds the snapshot object on every change. Measured consequence: four
63
+ * components across three templates re-rendered on every STT partial and every
64
+ * streaming delta, in files whose every other component is narrowly subscribed
65
+ * on purpose. One of them (`infocom-adventure`'s `TitleScreen`) reads nothing
66
+ * from the snapshot at all and subscribes to all of it for `session.start`.
67
+ *
68
+ * **Why publishing this does not reopen what `/internal` closed.**
69
+ * `useSessionCore` hands back the STORE — `subscribe`, `getSnapshot`,
70
+ * `connect`, `Symbol.dispose` — which is the framework's own plumbing, the same
71
+ * category as the providers and `buildAgentUrl` that live on
72
+ * `@alexkroman1/aai-ui/internal`. A client that holds it can subscribe out of
73
+ * band of React, dial a socket the mount did not, and dispose the session under
74
+ * the tree that is rendering it. What comes back from here is the SAME eight
75
+ * methods `useSession()` already publishes on its result, built into a fresh
76
+ * object rather than passed through, so the store is not reachable from it.
77
+ * There is no new capability here — only the existing one without the
78
+ * subscription tax.
79
+ *
80
+ * Identity-stable per core, so it is safe in a dependency array and in a
81
+ * `memo()` child's props: the methods are closures created once by
82
+ * `createBrowserSession`, and the object wrapping them is memoized on the core.
83
+ *
84
+ * Throws outside the provider `mountClient()` installs, like every session hook.
85
+ *
86
+ * @example A footer that acts on the session without re-rendering with it
87
+ * ```tsx
88
+ * import { useSessionActions, useSessionSelector } from "@alexkroman1/aai-ui";
89
+ *
90
+ * function Footer() {
91
+ * // Two narrow subscriptions and no snapshot read: this row re-renders when
92
+ * // `running` flips, and not on every transcript delta.
93
+ * const running = useSessionSelector((s) => s.running);
94
+ * const { toggle, end } = useSessionActions();
95
+ * return (
96
+ * <>
97
+ * <button onClick={toggle}>{running ? "Pause" : "Resume"}</button>
98
+ * <button onClick={end}>Hang up</button>
99
+ * </>
100
+ * );
101
+ * }
102
+ * ```
103
+ *
104
+ * @returns The eight control methods — see {@link SessionActions}.
105
+ *
106
+ * @public
107
+ */
108
+ export declare function useSessionActions(): SessionActions;
40
109
  /**
41
110
  * Return the live {@link Session}: the current snapshot fields plus the
42
111
  * control methods (`start`, `toggle`, `reset`, `resetState`, `disconnect`,
43
112
  * `cancel`, `end`).
44
113
  *
45
- * Throws if used outside the provider `client()` installs (the error names
114
+ * Throws if used outside the provider `mountClient()` installs (the error names
46
115
  * `<SessionProvider>` — you only mount that yourself when bypassing
47
- * `client()`). Re-renders the component on *every* snapshot change; for a
116
+ * `mountClient()`). Re-renders the component on *every* snapshot change; for a
48
117
  * component that reads one field, prefer {@link useSessionSelector} for a
49
118
  * targeted subscription.
50
119
  *
@@ -94,9 +163,63 @@ export declare function useSession(): Session;
94
163
  */
95
164
  export declare function useSessionSelector<T>(selector: (snapshot: SessionSnapshot) => T, isEqual?: (a: T, b: T) => boolean): T;
96
165
  /**
97
- * Provides the theme the components read via `useTheme`. `client()` installs
166
+ * The agent's live {@link AgentState} — `disconnected`, `connecting`, `ready`,
167
+ * `listening`, `thinking`, `speaking`, `error` — on its own narrow
168
+ * subscription.
169
+ *
170
+ * `useSessionSelector((s) => s.state)` spelled once. It is one of exactly two
171
+ * snapshot fields that more than one custom chrome ever selects (the other is
172
+ * {@link useSessionError}), and it had been written inline at eight sites —
173
+ * including inside this package and, worse, in `ConsoleShell`'s own `@example`,
174
+ * which taught the inline form to everyone who read it.
175
+ *
176
+ * **Named `useSessionStatus`, not `useSessionState`.** `useAgentState` is the
177
+ * SLOT hook — the agent's own synced application state, whatever a
178
+ * `sessionSlot()` projects — and `AgentState` here is the phase of the CALL.
179
+ * Two different concepts one letter apart, so the shorter-sounding name is the
180
+ * one deliberately not taken.
181
+ *
182
+ * Pair it with {@link AGENT_STATE_LABELS} for a rendered word; the raw member
183
+ * is a wire value, not a label.
184
+ *
185
+ * @example
186
+ * ```tsx
187
+ * import { AGENT_STATE_LABELS, useSessionStatus } from "@alexkroman1/aai-ui";
188
+ *
189
+ * function StatusDot() {
190
+ * const status = useSessionStatus();
191
+ * return <span data-state={status}>{AGENT_STATE_LABELS[status]}</span>;
192
+ * }
193
+ * ```
194
+ *
195
+ * @returns The current agent state.
196
+ *
197
+ * @public
198
+ */
199
+ export declare function useSessionStatus(): AgentState;
200
+ /**
201
+ * The session's current {@link SessionError}, or `null` when there is none, on
202
+ * its own narrow subscription.
203
+ *
204
+ * The other half of {@link useSessionStatus} — the second of the two fields a
205
+ * custom chrome reads over and over, and the one whose absence is invisible:
206
+ * per the `fatalError` latch in `session-core.ts` the error is the ONLY
207
+ * remaining signal that a session died, since the state beside it goes back to
208
+ * reading like a live one.
209
+ *
210
+ * A chrome rendering it owes `role="alert"` — which is what
211
+ * {@link SessionErrorBanner} is for, and why reaching for that beats reaching
212
+ * for this.
213
+ *
214
+ * @returns The current error, or `null`.
215
+ *
216
+ * @public
217
+ */
218
+ export declare function useSessionError(): SessionError | null;
219
+ /**
220
+ * Provides the theme the components read via `useTheme`. `mountClient()` installs
98
221
  * it automatically (from `ClientConfig.theme`); a custom tree only needs it
99
- * when bypassing `client()` and mounting React itself.
222
+ * when bypassing `mountClient()` and mounting React itself.
100
223
  *
101
224
  * @internal
102
225
  */
@@ -110,7 +233,7 @@ export declare function ThemeProvider({ value, children, }: {
110
233
  * provider is present, so components can call it unconditionally.
111
234
  *
112
235
  * This is how a custom component stays on the agent's palette: a
113
- * `client({ theme })` override reaches it here, where a hardcoded colour or a
236
+ * `mountClient({ theme })` override reaches it here, where a hardcoded colour or a
114
237
  * Tailwind class cannot see it.
115
238
  *
116
239
  * @example
package/dist/context.js CHANGED
@@ -10,9 +10,9 @@ const DEFAULT_THEME = {
10
10
  };
11
11
  const SessionCtx = createContext(null);
12
12
  /**
13
- * Provides the {@link SessionCore} the session hooks read. `client()`
13
+ * Provides the {@link BrowserSession} the session hooks read. `mountClient()`
14
14
  * installs it automatically; a custom tree only needs it when bypassing
15
- * `client()` and mounting React itself.
15
+ * `mountClient()` and mounting React itself.
16
16
  *
17
17
  * @internal
18
18
  */
@@ -20,7 +20,7 @@ function SessionProvider({ value, children }) {
20
20
  return createElement(SessionCtx.Provider, { value }, children);
21
21
  }
22
22
  /**
23
- * Return the raw {@link SessionCore} from context without subscribing to
23
+ * Return the raw {@link BrowserSession} from context without subscribing to
24
24
  * snapshot changes. Useful for accessing stable methods (`start`, `toggle`,
25
25
  * `reset`, …) from components that select narrow state via
26
26
  * {@link useSessionSelector}.
@@ -34,13 +34,83 @@ function useSessionCore() {
34
34
  return core;
35
35
  }
36
36
  /**
37
+ * The session's control methods — `start`, `cancel`, `resetState`, `reset`,
38
+ * `restart`, `disconnect`, `toggle`, `end` — with **no snapshot
39
+ * subscription**.
40
+ *
41
+ * This is the narrow half of {@link useSession}, and it is the half a custom
42
+ * chrome could not reach. `<Controls>` and `<StartScreen>` in this package pair
43
+ * a one-field `useSessionSelector` with this package's own `useSessionCore`
44
+ * (`context.ts`, unpublished); a `client.tsx`
45
+ * could not, because that hook is not published — so a footer needing `start`
46
+ * and `toggle` held a WHOLE-SNAPSHOT `useSession()`, and `session-core.ts`
47
+ * rebuilds the snapshot object on every change. Measured consequence: four
48
+ * components across three templates re-rendered on every STT partial and every
49
+ * streaming delta, in files whose every other component is narrowly subscribed
50
+ * on purpose. One of them (`infocom-adventure`'s `TitleScreen`) reads nothing
51
+ * from the snapshot at all and subscribes to all of it for `session.start`.
52
+ *
53
+ * **Why publishing this does not reopen what `/internal` closed.**
54
+ * `useSessionCore` hands back the STORE — `subscribe`, `getSnapshot`,
55
+ * `connect`, `Symbol.dispose` — which is the framework's own plumbing, the same
56
+ * category as the providers and `buildAgentUrl` that live on
57
+ * `@alexkroman1/aai-ui/internal`. A client that holds it can subscribe out of
58
+ * band of React, dial a socket the mount did not, and dispose the session under
59
+ * the tree that is rendering it. What comes back from here is the SAME eight
60
+ * methods `useSession()` already publishes on its result, built into a fresh
61
+ * object rather than passed through, so the store is not reachable from it.
62
+ * There is no new capability here — only the existing one without the
63
+ * subscription tax.
64
+ *
65
+ * Identity-stable per core, so it is safe in a dependency array and in a
66
+ * `memo()` child's props: the methods are closures created once by
67
+ * `createBrowserSession`, and the object wrapping them is memoized on the core.
68
+ *
69
+ * Throws outside the provider `mountClient()` installs, like every session hook.
70
+ *
71
+ * @example A footer that acts on the session without re-rendering with it
72
+ * ```tsx
73
+ * import { useSessionActions, useSessionSelector } from "@alexkroman1/aai-ui";
74
+ *
75
+ * function Footer() {
76
+ * // Two narrow subscriptions and no snapshot read: this row re-renders when
77
+ * // `running` flips, and not on every transcript delta.
78
+ * const running = useSessionSelector((s) => s.running);
79
+ * const { toggle, end } = useSessionActions();
80
+ * return (
81
+ * <>
82
+ * <button onClick={toggle}>{running ? "Pause" : "Resume"}</button>
83
+ * <button onClick={end}>Hang up</button>
84
+ * </>
85
+ * );
86
+ * }
87
+ * ```
88
+ *
89
+ * @returns The eight control methods — see {@link SessionActions}.
90
+ *
91
+ * @public
92
+ */
93
+ function useSessionActions() {
94
+ const core = useSessionCore();
95
+ return useMemo(() => ({
96
+ start: core.start,
97
+ cancel: core.cancel,
98
+ resetState: core.resetState,
99
+ reset: core.reset,
100
+ restart: core.restart,
101
+ disconnect: core.disconnect,
102
+ toggle: core.toggle,
103
+ end: core.end
104
+ }), [core]);
105
+ }
106
+ /**
37
107
  * Return the live {@link Session}: the current snapshot fields plus the
38
108
  * control methods (`start`, `toggle`, `reset`, `resetState`, `disconnect`,
39
109
  * `cancel`, `end`).
40
110
  *
41
- * Throws if used outside the provider `client()` installs (the error names
111
+ * Throws if used outside the provider `mountClient()` installs (the error names
42
112
  * `<SessionProvider>` — you only mount that yourself when bypassing
43
- * `client()`). Re-renders the component on *every* snapshot change; for a
113
+ * `mountClient()`). Re-renders the component on *every* snapshot change; for a
44
114
  * component that reads one field, prefer {@link useSessionSelector} for a
45
115
  * targeted subscription.
46
116
  *
@@ -60,16 +130,11 @@ function useSessionCore() {
60
130
  function useSession() {
61
131
  const core = useSessionCore();
62
132
  const snapshot = useSyncExternalStore(core.subscribe, core.getSnapshot);
133
+ const actions = useSessionActions();
63
134
  return useMemo(() => ({
64
135
  ...snapshot,
65
- start: core.start,
66
- cancel: core.cancel,
67
- resetState: core.resetState,
68
- reset: core.reset,
69
- disconnect: core.disconnect,
70
- toggle: core.toggle,
71
- end: core.end
72
- }), [snapshot, core]);
136
+ ...actions
137
+ }), [snapshot, actions]);
73
138
  }
74
139
  /**
75
140
  * Subscribe to a narrow slice of the session snapshot.
@@ -105,11 +170,84 @@ function useSessionSelector(selector, isEqual = Object.is) {
105
170
  const core = useSessionCore();
106
171
  return useSyncExternalStoreWithSelector(core.subscribe, core.getSnapshot, core.getSnapshot, selector, isEqual);
107
172
  }
173
+ /**
174
+ * The two selectors below are MODULE-SCOPE functions, and that is load-bearing
175
+ * rather than tidy.
176
+ *
177
+ * `useSyncExternalStoreWithSelector` caches its selection keyed on the selector
178
+ * it was handed: a fresh arrow per render invalidates that memo every render,
179
+ * so the selector re-runs and the `isEqual` short-circuit protects only the
180
+ * re-RENDER, never the work. Hoisting them means the two fields more than one
181
+ * chrome ever reads are selected by one stable function for the life of the
182
+ * program — which is also the thing a caller writing the arrow inline cannot
183
+ * do for themselves, and the reason these two are hooks at all rather than a
184
+ * documented one-liner.
185
+ */
186
+ const selectState = (s) => s.state;
187
+ const selectError = (s) => s.error;
188
+ /**
189
+ * The agent's live {@link AgentState} — `disconnected`, `connecting`, `ready`,
190
+ * `listening`, `thinking`, `speaking`, `error` — on its own narrow
191
+ * subscription.
192
+ *
193
+ * `useSessionSelector((s) => s.state)` spelled once. It is one of exactly two
194
+ * snapshot fields that more than one custom chrome ever selects (the other is
195
+ * {@link useSessionError}), and it had been written inline at eight sites —
196
+ * including inside this package and, worse, in `ConsoleShell`'s own `@example`,
197
+ * which taught the inline form to everyone who read it.
198
+ *
199
+ * **Named `useSessionStatus`, not `useSessionState`.** `useAgentState` is the
200
+ * SLOT hook — the agent's own synced application state, whatever a
201
+ * `sessionSlot()` projects — and `AgentState` here is the phase of the CALL.
202
+ * Two different concepts one letter apart, so the shorter-sounding name is the
203
+ * one deliberately not taken.
204
+ *
205
+ * Pair it with {@link AGENT_STATE_LABELS} for a rendered word; the raw member
206
+ * is a wire value, not a label.
207
+ *
208
+ * @example
209
+ * ```tsx
210
+ * import { AGENT_STATE_LABELS, useSessionStatus } from "@alexkroman1/aai-ui";
211
+ *
212
+ * function StatusDot() {
213
+ * const status = useSessionStatus();
214
+ * return <span data-state={status}>{AGENT_STATE_LABELS[status]}</span>;
215
+ * }
216
+ * ```
217
+ *
218
+ * @returns The current agent state.
219
+ *
220
+ * @public
221
+ */
222
+ function useSessionStatus() {
223
+ return useSessionSelector(selectState);
224
+ }
225
+ /**
226
+ * The session's current {@link SessionError}, or `null` when there is none, on
227
+ * its own narrow subscription.
228
+ *
229
+ * The other half of {@link useSessionStatus} — the second of the two fields a
230
+ * custom chrome reads over and over, and the one whose absence is invisible:
231
+ * per the `fatalError` latch in `session-core.ts` the error is the ONLY
232
+ * remaining signal that a session died, since the state beside it goes back to
233
+ * reading like a live one.
234
+ *
235
+ * A chrome rendering it owes `role="alert"` — which is what
236
+ * {@link SessionErrorBanner} is for, and why reaching for that beats reaching
237
+ * for this.
238
+ *
239
+ * @returns The current error, or `null`.
240
+ *
241
+ * @public
242
+ */
243
+ function useSessionError() {
244
+ return useSessionSelector(selectError);
245
+ }
108
246
  const ThemeCtx = createContext(DEFAULT_THEME);
109
247
  /**
110
- * Provides the theme the components read via `useTheme`. `client()` installs
248
+ * Provides the theme the components read via `useTheme`. `mountClient()` installs
111
249
  * it automatically (from `ClientConfig.theme`); a custom tree only needs it
112
- * when bypassing `client()` and mounting React itself.
250
+ * when bypassing `mountClient()` and mounting React itself.
113
251
  *
114
252
  * @internal
115
253
  */
@@ -208,7 +346,7 @@ function useThemeStyles(theme) {
208
346
  * provider is present, so components can call it unconditionally.
209
347
  *
210
348
  * This is how a custom component stays on the agent's palette: a
211
- * `client({ theme })` override reaches it here, where a hardcoded colour or a
349
+ * `mountClient({ theme })` override reaches it here, where a hardcoded colour or a
212
350
  * Tailwind class cannot see it.
213
351
  *
214
352
  * @example
@@ -233,4 +371,4 @@ function useTheme() {
233
371
  return useContext(ThemeCtx);
234
372
  }
235
373
  //#endregion
236
- export { SessionProvider, ThemeProvider, useSession, useSessionCore, useSessionSelector, useTheme };
374
+ export { SessionProvider, ThemeProvider, useSession, useSessionActions, useSessionCore, useSessionError, useSessionSelector, useSessionStatus, useTheme };
@@ -1 +1 @@
1
- import{a as e,o as t}from"./client-audio-constants-CP13UQZt.js";var n={echoCancellation:!0,noiseSuppression:!1,autoGainControl:!1,voiceIsolation:!1};function r(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function i(e){let t=Error(`Audio ${e} worklet crashed`);return console.error(`[aai-ui]`,t.message),t}function a(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function o(e,t,n){let r=new AudioWorkletNode(e,`capture-processor`,{channelCount:1,channelCountMode:`explicit`}),i=null;return r.port.onmessage=e=>{let r=e.data;r.event===`chunk`&&r.buffer?t(r.buffer):r.event===`silent`?n?.():r.event===`stopped`&&(i?.(),i=null)},{node:r,start(){r.port.postMessage({event:`start`})},stop(){let{promise:e,resolve:t}=Promise.withResolvers(),n=setTimeout(t,250);return i=()=>{clearTimeout(n),t()},r.port.postMessage({event:`stop`}),e}}}async function s(s){let{sttSampleRate:c,ttsSampleRate:l,captureWorkletSrc:u,playbackWorkletSrc:d,onMicData:f,onError:p,onPlaybackStats:m,onPlaybackProgress:h,onMicSilent:g}=s,_=new AudioContext({sampleRate:l,latencyHint:`playback`}),v=c===l,y=v?_:new AudioContext({sampleRate:c,latencyHint:`interactive`});async function b(){let e=v?[_]:[_,y];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let x=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...n}}),S;try{[S]=await Promise.all([x,_.resume(),y.resume(),y.audioWorklet.addModule(u),_.audioWorklet.addModule(d)]),r(y.sampleRate,c,`capture`),r(_.sampleRate,l,`playback`)}catch(e){throw a(x),await b(),e}let C=y.createMediaStreamSource(S),w=o(y,f,g);C.connect(w.node),w.node.onprocessorerror=()=>p?.(i(`capture`)),w.start();let T=null,E=null,D=0,O=null,k=new AbortController;function A(){E?.(),E=null,O=null}function j(e){e.stats&&e.stats.concealedSamples>0&&m?.(e.stats),e.reason!==`interrupt`&&(O===null||e.turn===O)&&A()}function M(){if(T)return T;let e=new AudioWorkletNode(_,`playback-processor`);return e.connect(_.destination),e.port.onmessage=e=>{e.data.event===`stop`?j(e.data):e.data.event===`progress`&&h?.(e.data.bufferedMs)},e.onprocessorerror=()=>{let e=i(`playback`);A(),p?.(e)},T=e,e}let N={enqueue(e){k.signal.aborted||e.byteLength!==0&&M().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){if(!T)return Promise.resolve();let n=++D;if(T.port.postMessage({event:`done`,turn:n}),_.state!==`running`)return O=null,Promise.resolve();let{promise:r,resolve:i}=Promise.withResolvers();E?.();let a=()=>{clearInterval(o),clearTimeout(s),E===a&&(E=null,O=null),i()},o=setInterval(()=>{_.state!==`running`&&a()},t),s=setTimeout(a,e);return E=a,O=n,r},flush(){T&&(A(),T.port.postMessage({event:`interrupt`}))},async close(){if(!k.signal.aborted){k.abort(),await w.stop(),C.disconnect(),w.node.disconnect(),T&&T.disconnect();for(let e of S.getTracks())e.stop();await b()}},async[Symbol.asyncDispose](){await N.close()}};return N}export{s as createVoiceIO};
1
+ import{a as e,o as t}from"./client-audio-constants-CP13UQZt.js";var n={echoCancellation:!0,noiseSuppression:!1,autoGainControl:!1,voiceIsolation:!1};function r(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function i(e){let t=Error(`Audio ${e} worklet crashed`);return console.error(`[aai-ui]`,t.message),t}function a(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function o(e,t,n){let r=new AudioWorkletNode(e,`capture-processor`,{channelCount:1,channelCountMode:`explicit`}),i=null;return r.port.onmessage=e=>{let r=e.data;r.event===`chunk`&&r.buffer?t(r.buffer):r.event===`silent`?n?.():r.event===`stopped`&&(i?.(),i=null)},{node:r,start(){r.port.postMessage({event:`start`})},stop(){let{promise:e,resolve:t}=Promise.withResolvers(),n=setTimeout(t,250);return i=()=>{clearTimeout(n),t()},r.port.postMessage({event:`stop`}),e}}}async function s(s){let{sttSampleRate:c,ttsSampleRate:l,captureWorkletSrc:u,playbackWorkletSrc:d,onMicData:f,onError:p,onPlaybackStats:m,onPlaybackProgress:h,onMicSilent:g}=s,_=new AudioContext({sampleRate:l,latencyHint:`playback`}),v=c===l,y=v?_:new AudioContext({sampleRate:c,latencyHint:`interactive`});async function b(){let e=v?[_]:[_,y];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let x=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...n}}),S;try{[S]=await Promise.all([x,_.resume(),y.resume(),y.audioWorklet.addModule(u),_.audioWorklet.addModule(d)]),r(y.sampleRate,c,`capture`),r(_.sampleRate,l,`playback`)}catch(e){throw a(x),await b(),e}let C=y.createMediaStreamSource(S),w=o(y,f,g);C.connect(w.node),w.node.onprocessorerror=()=>p?.(i(`capture`)),w.start();let T=null,E=0,D=null,O=new AbortController;function k(){let e=D;D=null,e?.settle()}function A(e){e.stats&&e.stats.concealedSamples>0&&m?.(e.stats),e.reason!==`interrupt`&&(D===null||e.turn===D.turn)&&k()}function j(){if(T)return T;let e=new AudioWorkletNode(_,`playback-processor`);return e.connect(_.destination),e.port.onmessage=e=>{e.data.event===`stop`?A(e.data):e.data.event===`progress`&&h?.(e.data.bufferedMs)},e.onprocessorerror=()=>{let e=i(`playback`);k(),p?.(e)},T=e,e}let M={enqueue(e){O.signal.aborted||e.byteLength!==0&&j().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){if(!T)return Promise.resolve();let n=++E;if(T.port.postMessage({event:`done`,turn:n}),_.state!==`running`)return k(),Promise.resolve();let{promise:r,resolve:i}=Promise.withResolvers();k();let a=()=>{clearInterval(o),clearTimeout(s),D?.settle===a&&(D=null),i()},o=setInterval(()=>{_.state!==`running`&&a()},t),s=setTimeout(a,e);return D={turn:n,settle:a},r},flush(){T&&(k(),T.port.postMessage({event:`interrupt`}))},async close(){if(!O.signal.aborted){O.abort(),await w.stop(),C.disconnect(),w.node.disconnect(),T&&T.disconnect();for(let e of S.getTracks())e.stop();await b()}},async[Symbol.asyncDispose](){await M.close()}};return M}export{s as createVoiceIO};