@alexkroman1/aai-ui 6.7.2 → 6.9.1

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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Turning a form's `File`s into stored upload ids, pauses and all.
3
+ *
4
+ * Split out of `use-workflow-form.ts` for the 500-line cap, and the seam is a
5
+ * real one: that module is the two HOOKS and the state between them, where this
6
+ * is the walk over a submitted input — which is the only part of it that knows
7
+ * what a `File` is, holds a loop, and survives being re-entered.
8
+ *
9
+ * `_`-internal. `useWorkflowSubmit` is the only caller; `useWorkflowStream` sends
10
+ * one file rather than walking an input and shares only the gate underneath both
11
+ * (`_upload-session.ts`).
12
+ */
13
+ import type { UploadParallel } from "@alexkroman1/aai/workflow-api";
14
+ import { type UploadGate } from "./_upload-session.ts";
15
+ import type { UploadStatus } from "./use-workflow-form.ts";
16
+ import type { WorkflowApi } from "./workflow-client.ts";
17
+ /**
18
+ * What one submission's uploads know about themselves, across pauses.
19
+ *
20
+ * Held by the hook rather than by the walk below, because the walk RE-RUNS: a
21
+ * pause unwinds nothing, so resuming re-enters `uploadFiles` with the same input
22
+ * and the same session, and every file whose bytes are already in is skipped by
23
+ * `stored` rather than sent again.
24
+ *
25
+ * Which is also why the ids live here. A resumable upload is one whose id
26
+ * outlives the attempt that began it — that is the whole mechanism — so an id
27
+ * minted per attempt would make each round a fresh upload of the whole file.
28
+ */
29
+ export type UploadSession = {
30
+ /**
31
+ * What this form's remembered ids are filed under (`_upload-recall.ts`).
32
+ *
33
+ * The workflow's name, so two forms on one page do not read each other's
34
+ * entries. It is not a SAFETY boundary and does not need to be — a recalled id
35
+ * is checked against the agent before anything is sent to it — it just keeps a
36
+ * form from spending a round trip on another form's upload.
37
+ */
38
+ scope: string;
39
+ /** The id each file is being stored under, minted once. */
40
+ ids: Map<File, string>;
41
+ /** Files whose every byte has landed, by the id they landed under. */
42
+ stored: Map<File, string>;
43
+ /**
44
+ * Files that have had an attempt, so the next one must claim the id as its own.
45
+ *
46
+ * `sendThroughGate` tracks this itself WITHIN one file's attempts. What this
47
+ * carries is the attempt made by a page load that is gone: an id recalled from
48
+ * storage was claimed by whoever minted it, so the first attempt of this load is
49
+ * a resume even though this load has sent nothing.
50
+ */
51
+ tried: Set<File>;
52
+ /** The person's pause. */
53
+ gate: UploadGate;
54
+ };
55
+ /** A fresh session for one submission of `workflow`. */
56
+ export declare function createUploadSession(workflow: string): UploadSession;
57
+ /**
58
+ * Replace every `File` in a submitted form with the id of a stored upload,
59
+ * reporting how far each one has got.
60
+ *
61
+ * Sequential rather than `Promise.all`: these are large bodies, and a form with
62
+ * two 200 MB recordings should send them one after another rather than compete
63
+ * for the same connection. That is also what makes a single bar honest — one
64
+ * file is in flight at a time, and `index`/`count` say which.
65
+ *
66
+ * Anything that is not a `File` (or an array of them) passes through untouched,
67
+ * so this is invisible to every form that has none — including one whose values
68
+ * are not an object at all, which `submit` accepts.
69
+ *
70
+ * ## `uploadStream`, not `upload`, and the id is the reason
71
+ *
72
+ * The difference between the two calls is only who mints the id — and that is
73
+ * exactly what decides whether an interrupted upload can be picked up again. An
74
+ * `upload` mints its own at the END and hands it back, so a caller whose upload
75
+ * died has nothing to name what was stored and no choice but to send the file
76
+ * again. A `uploadStream` is told the id up front, so the windows already in the
77
+ * store are addressable, which is what both a pause and a server restart need.
78
+ *
79
+ * Nothing else about the submission changes: the run is still started after the
80
+ * last byte lands, so the incomplete record a streamed upload leaves along the
81
+ * way is one nobody reads.
82
+ */
83
+ export declare function uploadFiles(api: WorkflowApi, input: unknown, report: (status: UploadStatus) => void, parallel: UploadParallel | undefined, session: UploadSession): Promise<unknown>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Where an upload's ID survives a page RELOAD.
3
+ *
4
+ * A streamed upload is resumable because its id outlives the attempt that began
5
+ * it — `_upload-files.ts` says so, and `_upload-session.ts` turns that into a
6
+ * pause a person can press. Both of them hold the id in MEMORY: the walk's
7
+ * `UploadSession` lives in a `useRef`, so a reload was the one interruption the
8
+ * mechanism could not survive. Everything else was already in place — the windows
9
+ * were still in the store, the agent could still name them
10
+ * (`UploadInfo.ranges`), and the id was minted in the browser — and the browser
11
+ * had thrown away the only name for them. So a person who refreshed at 90% of a
12
+ * 200 MB recording sent the whole file again, which is the one interruption they
13
+ * are most likely to cause on purpose.
14
+ *
15
+ * This is that name, written down. It is what tus-js-client's `urlStorage` and
16
+ * Uppy's Golden Retriever sell, in the shape `session-resume-store.ts` already
17
+ * uses for a session id.
18
+ *
19
+ * ## A FINGERPRINT, because a `File` has no name a page can address
20
+ *
21
+ * A file from a picker carries no path and no handle, so the key is what
22
+ * tus-js-client fingerprints on: size, last-modified, type and name. Two
23
+ * different files agreeing on all four is the case this cannot tell apart — and
24
+ * the reason NOTHING here decides to resume. `_upload-files.ts` asks the agent
25
+ * what the id actually holds before sending a byte to it, so a wrong hit costs
26
+ * one `GET` and a fresh id rather than a corrupted upload.
27
+ *
28
+ * ## `sessionStorage`, deliberately
29
+ *
30
+ * The same call `session-resume-store.ts` makes, for a reason that happens to be
31
+ * stronger here: a reload and a same-tab navigation are exactly what this is for,
32
+ * and an id from yesterday names an upload the agent's sweep has very likely
33
+ * already collected. A tab is also the boundary the walk itself has — two tabs
34
+ * uploading the same recording are two submissions.
35
+ *
36
+ * Every access is guarded. Storage throws outright in Safari private mode and
37
+ * under a blocking policy, and an upload that cannot be REMEMBERED must degrade
38
+ * to the upload we would have done anyway rather than failing to start.
39
+ */
40
+ /**
41
+ * The id this file was last being stored under in this tab, if any.
42
+ *
43
+ * A hit is a CANDIDATE and never a decision — see the module doc.
44
+ *
45
+ * @internal
46
+ */
47
+ export declare function recallUploadId(scope: string, file: File): string | undefined;
48
+ /**
49
+ * Remember the id this file is being stored under.
50
+ *
51
+ * Called before the first byte leaves rather than after the last one lands: the
52
+ * reload this exists for happens in between, and an id written at the end is an
53
+ * id written for the one case that did not need it.
54
+ *
55
+ * @internal
56
+ */
57
+ export declare function rememberUploadId(scope: string, file: File, id: string): void;
58
+ /**
59
+ * Forget it: the agent holds nothing resumable under this id.
60
+ *
61
+ * The other half of the agent deciding. Without it a swept upload is re-read on
62
+ * every submission of the same file for the life of the tab, which is a round
63
+ * trip spent learning the same 404.
64
+ *
65
+ * @internal
66
+ */
67
+ export declare function forgetUploadId(scope: string, file: File): void;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Driving ONE resumable upload from a page: its id, and the gate a person can
3
+ * close.
4
+ *
5
+ * The SDK already survives the server going away — a round that fails for a
6
+ * reason that looks like an outage re-enters itself with `resume: true` and sends
7
+ * only the windows that are missing (`aai/sdk/_upload-resume.ts`). This is the
8
+ * same mechanism turned around and handed to the PERSON: pausing is an outage
9
+ * they caused, resuming is the round after it, and the store cannot tell the two
10
+ * apart because there is nothing to tell apart.
11
+ *
12
+ * ## Why a gate rather than a boolean
13
+ *
14
+ * Pausing has to do two things that a `paused` flag cannot: stop the bytes that
15
+ * are already on the wire, and hold the loop that would send the next ones. So it
16
+ * is an `AbortController` and a promise — abort what is in flight, and park the
17
+ * uploader on `settle()` until somebody opens the gate again.
18
+ *
19
+ * The controller is REPLACED on resume rather than reused, because an aborted
20
+ * signal stays aborted; a caller therefore has to read {@link UploadGate.signal}
21
+ * fresh for each attempt rather than capturing it once.
22
+ *
23
+ * ## The loop this is built for
24
+ *
25
+ * ```ts no-check
26
+ * while (!gate.cancelled) {
27
+ * await gate.settle();
28
+ * if (gate.cancelled) break;
29
+ * try {
30
+ * await api.uploadStream(id, file, { signal: gate.signal, resume: tried });
31
+ * break;
32
+ * } catch (err) {
33
+ * // An abort that is not a cancel is a PAUSE: go back and wait.
34
+ * if (!isAbort(err) || gate.cancelled) throw err;
35
+ * }
36
+ * }
37
+ * ```
38
+ *
39
+ * **The catch keys off the abort, not off `gate.paused`,** and that is the one
40
+ * subtle part. Pausing and immediately resuming — a double-click, or a person
41
+ * changing their mind inside the round trip — resolves the gate before the
42
+ * rejection this abort caused has even landed, so a `paused` check would read
43
+ * `false` and rethrow an `AbortError` as though the upload had failed. Every
44
+ * abort reaching that catch was caused by this gate, so every one of them is a
45
+ * pause unless the gate was cancelled outright.
46
+ */
47
+ /** A person's pause, as the uploader sees it. */
48
+ export type UploadGate = {
49
+ /** Whether the gate is currently closed. For rendering, not for control flow. */
50
+ readonly paused: boolean;
51
+ /** Whether the upload was abandoned. A cancelled gate never opens again. */
52
+ readonly cancelled: boolean;
53
+ /**
54
+ * The signal for the NEXT attempt. Re-read it per attempt: resuming installs a
55
+ * fresh controller, and the previous one stays aborted forever.
56
+ */
57
+ readonly signal: AbortSignal;
58
+ /** Stop the bytes in flight and hold the uploader. A no-op when already closed. */
59
+ pause: () => void;
60
+ /** Open the gate and install a fresh signal. A no-op when not paused. */
61
+ resume: () => void;
62
+ /**
63
+ * Abandon the upload for good.
64
+ *
65
+ * Distinct from a pause in exactly one way that matters to the loop above: it
66
+ * releases the gate rather than holding it, so an uploader parked on `settle()`
67
+ * wakes up and leaves instead of waiting for a resume that is not coming. What
68
+ * a caller does with a cancelled upload's stored windows is its own business —
69
+ * they stay in the store, addressable by an id only the caller has.
70
+ */
71
+ cancel: () => void;
72
+ /** Resolves as soon as the gate is open — immediately, when it already is. */
73
+ settle: () => Promise<void>;
74
+ };
75
+ /** Whether this rejection is an abort, in either of the two shapes runtimes throw. */
76
+ export declare function isAbortError(err: unknown): boolean;
77
+ /** A fresh upload id: a capability, so it is random rather than derived. */
78
+ export declare function randomUploadId(): string;
79
+ /**
80
+ * Send one file, waiting out however many pauses the person takes.
81
+ *
82
+ * The loop from the module doc, written once: both hooks need exactly this and a
83
+ * second copy of it is a second place for the abort/pause distinction to be got
84
+ * wrong. `send` is handed whether this attempt must CLAIM the id as its own —
85
+ * false the first time, since a fresh id has nothing to resume and saying
86
+ * otherwise waives the refusal that makes a caller-chosen id safe.
87
+ *
88
+ * Throws whatever `send` threw, except an abort the gate caused. A cancelled gate
89
+ * throws too: the caller distinguishes it by reading `gate.cancelled`, which is
90
+ * how an abandoned submission unwinds without being reported as a failure.
91
+ */
92
+ export declare function sendThroughGate(gate: UploadGate, send: (resume: boolean) => Promise<void>): Promise<void>;
93
+ /**
94
+ * A gate, open.
95
+ *
96
+ * One per upload rather than one per hook: the id and the windows already stored
97
+ * belong to a file, so a gate that outlived its file would resume something else.
98
+ */
99
+ export declare function createUploadGate(): UploadGate;
@@ -22,6 +22,14 @@ import type { UploadStatus } from "../use-workflow-form.ts";
22
22
  * - **The file is NAMED, and counted when there is more than one.** Files are
23
23
  * sent one after another, so a single bar otherwise appears to restart from
24
24
  * zero partway through with nothing to say why.
25
+ * - **A paused upload SAYS SO, rather than being a bar that stopped.** Those look
26
+ * identical, which is the whole reason `UploadStatus.paused` exists, and the
27
+ * fill stops animating so the difference is visible without reading.
28
+ *
29
+ * The pause control appears only when a handler for it is passed. That is not
30
+ * politeness about props: a button whose press does nothing is worse than no
31
+ * button, and a page holding an `upload` it did not produce (a saved status, a
32
+ * parent's state) has nothing to pause.
25
33
  *
26
34
  * @example
27
35
  * ```tsx
@@ -41,12 +49,17 @@ import type { UploadStatus } from "../use-workflow-form.ts";
41
49
  *
42
50
  * @param upload - What `useWorkflowSubmit` reports. `undefined` renders nothing,
43
51
  * so a page may pass its state straight through.
52
+ * @param onPause - The hook's `pauseUpload`. Pass it together with `onResume` to
53
+ * get the control; pass neither for a bar that only reports.
54
+ * @param onResume - The hook's `resumeUpload`.
44
55
  * @param className - Replaces the default classes rather than extending them,
45
56
  * so a custom chrome is not fighting a default it did not ask for.
46
57
  *
47
58
  * @public
48
59
  */
49
- export declare function UploadProgressBar({ upload, className, }: {
60
+ export declare function UploadProgressBar({ upload, onPause, onResume, className, }: {
50
61
  upload?: UploadStatus | undefined;
62
+ onPause?: (() => void) | undefined;
63
+ onResume?: (() => void) | undefined;
51
64
  className?: string | undefined;
52
65
  }): ReactNode;
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-aai:"Monument Grotesk", "ABC Monument Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-aai-serif:"Source Serif 4", "Source Serif Pro", Charter, "Iowan Old Style", Georgia, serif;--font-aai-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--radius-aai:4px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html,body{margin:0;padding:0}}@layer components;@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.order-last{order:9999}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.my-0\.5{margin-block:calc(var(--spacing) * .5)}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.my-2\.5{margin-block:calc(var(--spacing) * 2.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.box-border{box-sizing:border-box}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-9{height:calc(var(--spacing) * 9)}.h-11{height:calc(var(--spacing) * 11)}.h-\[7px\]{height:7px}.h-full{height:100%}.h-screen{height:100vh}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-\[40vh\]{max-height:40vh}.min-h-0{min-height:0}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-4{width:calc(var(--spacing) * 4)}.w-\[7px\]{width:7px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-75{max-width:calc(var(--spacing) * 75)}.max-w-105{max-width:calc(var(--spacing) * 105)}.max-w-190{max-width:calc(var(--spacing) * 190)}.max-w-\[82\%\]{max-width:82%}.max-w-\[min\(78\%\,64ch\)\]{max-width:min(78%,64ch)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.border-collapse{border-collapse:collapse}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.resize{resize:both}.\[scrollbar-width\:none\]{scrollbar-width:none}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.appearance-none{appearance:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-aai{border-radius:var(--radius-aai)}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-none{--tw-border-style:none;border-style:none}.border-\(--aai-btn-bd\){border-color:var(--aai-btn-bd)}.bg-\(--aai-btn-bg\){background-color:var(--aai-btn-bg)}.bg-transparent{background-color:#0000}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-7{padding:calc(var(--spacing) * 7)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pl-1\.5{padding-left:calc(var(--spacing) * 1.5)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-left{text-align:left}.font-aai{font-family:var(--font-aai)}.font-aai-mono{font-family:var(--font-aai-mono)}.font-aai-serif{font-family:var(--font-aai-serif)}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.625rem\]{font-size:.625rem}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[17px\]{font-size:17px}.text-\[22px\]{font-size:22px}.text-\[32px\]{font-size:32px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-\[22px\]{--tw-leading:22px;line-height:22px}.leading-\[23px\]{--tw-leading:23px;line-height:23px}.leading-\[130\%\]{--tw-leading:130%;line-height:130%}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.2px\]{--tw-tracking:-.2px;letter-spacing:-.2px}.tracking-\[1\.2px\]{--tw-tracking:1.2px;letter-spacing:1.2px}.tracking-\[1\.4px\]{--tw-tracking:1.4px;letter-spacing:1.4px}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.wrap-break-word{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\(--aai-btn-fg\){color:var(--aai-btn-fg)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.file\:mr-3::file-selector-button{margin-right:calc(var(--spacing) * 3)}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-aai::file-selector-button{border-radius:var(--radius-aai)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:px-3::file-selector-button{padding-inline:calc(var(--spacing) * 3)}.file\:py-1\.5::file-selector-button{padding-block:calc(var(--spacing) * 1.5)}.file\:font-aai::file-selector-button{font-family:var(--font-aai)}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:\[color\:var\(--aai-file-button-fg\)\]::file-selector-button{color:var(--aai-file-button-fg)}.file\:\[background\:var\(--aai-file-button-bg\)\]::file-selector-button{background:var(--aai-file-button-bg)}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.focus-visible\:\[outline\:2px_solid\]:focus-visible{outline:2px solid}.focus-visible\:\[outline-offset\:2px\]:focus-visible{outline-offset:2px}@media (hover:hover){.enabled\:hover\:border-\(--aai-btn-bd-hover\):enabled:hover{border-color:var(--aai-btn-bd-hover)}.enabled\:hover\:bg-\(--aai-btn-bg-hover\):enabled:hover{background-color:var(--aai-btn-bg-hover)}.enabled\:hover\:text-\(--aai-btn-fg-hover\):enabled:hover{color:var(--aai-btn-fg-hover)}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:ml-auto{margin-left:auto}.sm\:max-w-\[60\%\]{max-width:60%}.sm\:basis-auto{flex-basis:auto}.sm\:px-16{padding-inline:calc(var(--spacing) * 16)}.sm\:py-14{padding-block:calc(var(--spacing) * 14)}}@media (width>=48rem){.md\:order-none{order:0}.md\:h-screen{height:100vh}.md\:max-h-none{max-height:none}.md\:w-\(--aai-sidebar-w\){width:var(--aai-sidebar-w)}.md\:flex-row{flex-direction:row}.md\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.md\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}}}@keyframes aai-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.82)}}@keyframes aai-bounce{0%,80%,to{opacity:.3;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}@keyframes aai-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.tool-shimmer{-webkit-text-fill-color:transparent;background:linear-gradient(90deg,currentColor 25%,#0000 50%,currentColor 75%) 0 0/200% 100%;-webkit-background-clip:text;background-clip:text;animation:2s infinite aai-shimmer}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}
@@ -6,9 +6,9 @@
6
6
  <title>aai</title>
7
7
  <link rel="icon" href="./favicon.ico" />
8
8
  <style>html, body { background: #FBF8F2; margin: 0; }</style>
9
- <script type="module" crossorigin src="./assets/index-CepvxbNU.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-DXODx_9r.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="./assets/client-audio-constants-Ck0IJO4c.js">
11
- <link rel="stylesheet" crossorigin href="./assets/index-BZlePk3y.css">
11
+ <link rel="stylesheet" crossorigin href="./assets/index-DTLrhtTF.css">
12
12
  </head>
13
13
  <body>
14
14
  <main id="app"></main>
package/dist/hooks.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DefaultToolResult } from "@alexkroman1/aai";
1
+ import type { DefaultToolResult, StateProjection } from "@alexkroman1/aai";
2
2
  import type { ToolCallInfo } from "./types.ts";
3
3
  /**
4
4
  * Fire a callback when a tool call settles, with the tool's JSON result.
@@ -39,6 +39,42 @@ export declare function useToolResult<R = DefaultToolResult>(callback: (name: st
39
39
  * @public
40
40
  */
41
41
  export declare function useAgentState<S = DefaultToolResult>(): S | null;
42
+ /**
43
+ * The agent's projected session state, typed and defaulted by the SAME
44
+ * projection the agent pushes — pass `slot.projection(view)` and there is no
45
+ * type argument to restate and no empty frame to derive.
46
+ *
47
+ * This is the overload to reach for whenever `syncState` is a slot projection,
48
+ * because it closes the round-trip the other two leave open. A projection is
49
+ * callable, so the pre-first-push frame is what `projection()` returns — the
50
+ * `fallback` overload's own doc tells you to build it that way — and the
51
+ * projection's return type is the state's type, so `useAgentState<CartView>`
52
+ * was restating what `cartView` already knew. Both halves came out of the same
53
+ * declaration and both were written by hand:
54
+ *
55
+ * ```tsx no-check
56
+ * // `no-check`: the projection lives with the agent, in another file.
57
+ * // Before — the empty frame derived by hand, the type named three times:
58
+ * const EMPTY: CartView = cartSlot.projection(cartView)(undefined);
59
+ * const cart = useAgentState<CartView>(EMPTY);
60
+ *
61
+ * // After — `shared.ts` exports the projection once, both ends import it:
62
+ * const cart = useAgentState(cartProjection);
63
+ * ```
64
+ *
65
+ * The empty frame is memoized on the projection's identity, so a module-scope
66
+ * projection (the normal case) produces ONE frame for the life of the
67
+ * component — which the `fallback` overload can only ask you to arrange by
68
+ * hoisting, and which a `slot.projection(view)` spelled inline in the render
69
+ * body silently got wrong.
70
+ *
71
+ * @param projection - The same `slot.projection(view)` the agent declares as
72
+ * `syncState`. Export it from the module that declares the slot so the two
73
+ * ends cannot drift.
74
+ *
75
+ * @public
76
+ */
77
+ export declare function useAgentState<V>(projection: StateProjection<V>): V;
42
78
  /**
43
79
  * The agent's projected session state, falling back to `fallback` before the
44
80
  * first push — so the return is never `null` and a sidebar needs no branch for
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useSessionSelector } from "./context.js";
2
2
  import { i as tryParseJSON } from "./_utils-B6498_bm.js";
3
- import { useEffect, useRef } from "react";
3
+ import { useEffect, useMemo, useRef } from "react";
4
4
  //#region hooks.ts
5
5
  /** Index of the first item whose sequence number is above the watermark (tail scan from the end). */
6
6
  function tailStart(items, seqOf, watermark) {
@@ -80,7 +80,10 @@ function useToolResult(...args) {
80
80
  }
81
81
  function useAgentState(fallback) {
82
82
  const state = useSessionSelector((snapshot) => snapshot.agentState);
83
+ const isProjection = typeof fallback === "function";
84
+ const projected = useMemo(() => isProjection ? fallback() : void 0, [fallback, isProjection]);
83
85
  if (state !== null) return state;
86
+ if (isProjection) return projected;
84
87
  return fallback === void 0 ? null : fallback;
85
88
  }
86
89
  /**