basalt-ui 1.12.0 → 1.13.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/AGENTS.md +21 -21
- package/CHANGELOG.md +17 -0
- package/README.md +38 -35
- package/agent/rules/basalt-agent.md +14 -0
- package/configs/oxlint-plugin.js +7 -3
- package/configs/oxlint-plugin.test.ts +34 -5
- package/configs/oxlint.json +4 -4
- package/dist/agent/adapter.d.ts +5 -1
- package/dist/agent/adapter.d.ts.map +1 -1
- package/dist/agent/adapter.js +22 -13
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +6 -2
- package/dist/agent/id.d.ts +67 -19
- package/dist/agent/id.d.ts.map +1 -1
- package/dist/agent/id.js +21 -7
- package/dist/agent/thread.d.ts +4 -0
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +2 -2
- package/dist/agent/use-agent-thread-runs.d.ts +24 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +55 -15
- package/dist/agent-chat/index.d.ts +4 -0
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/message-affordances.d.ts +59 -0
- package/dist/agent-chat/message-affordances.d.ts.map +1 -0
- package/dist/agent-chat/message-affordances.js +7 -0
- package/dist/agent-chat/relative-time.d.ts +27 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -0
- package/dist/agent-chat/relative-time.js +27 -0
- package/dist/agent-chat/relative-time.test.d.ts +2 -0
- package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed-row.d.ts +65 -0
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed-row.js +156 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed.d.ts +34 -5
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +52 -19
- package/dist/agent-chat/thread-feed.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-message.d.ts +183 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +315 -34
- package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
- package/dist/agent-chat/thread-outcome-card.js +1 -16
- package/dist/agent-chat/thread-workspace.d.ts +14 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +29 -3
- package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
- package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.d.ts +123 -0
- package/dist/agent-chat/virtualize.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.js +9 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
- package/dist/data/index.d.ts +5 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual.d.ts +2 -1
- package/dist/data/virtual.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/surfaces.d.ts +2 -2
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +3 -2
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/llms.txt +6 -6
- package/package.json +2 -2
- package/src/agent/adapter.test.ts +237 -0
- package/src/agent/adapter.ts +72 -14
- package/src/agent/ai-sdk-transport.test.ts +64 -0
- package/src/agent/ai-sdk-transport.ts +8 -2
- package/src/agent/id.test.ts +95 -1
- package/src/agent/id.ts +98 -28
- package/src/agent/thread.ts +4 -0
- package/src/agent/use-agent-stream.test.tsx +22 -0
- package/src/agent/use-agent-stream.ts +6 -3
- package/src/agent/use-agent-thread-runs.test.tsx +326 -0
- package/src/agent/use-agent-thread-runs.ts +159 -23
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +80 -0
- package/src/agent-chat/index.ts +12 -0
- package/src/agent-chat/message-affordances.ts +61 -0
- package/src/agent-chat/relative-time.test.ts +95 -0
- package/src/agent-chat/relative-time.ts +55 -0
- package/src/agent-chat/thread-feed-row.test.tsx +510 -0
- package/src/agent-chat/thread-feed-row.tsx +274 -0
- package/src/agent-chat/thread-feed.test.tsx +241 -0
- package/src/agent-chat/thread-feed.tsx +119 -18
- package/src/agent-chat/thread-message.test.tsx +937 -3
- package/src/agent-chat/thread-message.tsx +766 -38
- package/src/agent-chat/thread-outcome-card.tsx +1 -25
- package/src/agent-chat/thread-workspace.test.tsx +169 -0
- package/src/agent-chat/thread-workspace.tsx +69 -5
- package/src/agent-chat/virtualize.ts +132 -0
- package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
- package/src/data/index.ts +5 -1
- package/src/data/virtual-list.tsx +2 -1
- package/src/data/virtual.ts +2 -1
- package/src/index.ts +5 -0
- package/src/surfaces.ts +2 -1
- package/src/theme/shadow-surfaces.test.ts +9 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { JSX } from 'react';
|
|
2
2
|
import type { AgentPartRenderers, ChatMessage, ForeignPart, PartRenderer, PartRenderers, StreamStatus, TranscriptPart } from '../agent/index.js';
|
|
3
|
+
import type { MessageAffordances } from './message-affordances.js';
|
|
4
|
+
import type { VirtualizeOptions, VirtualizeProps } from './virtualize.js';
|
|
3
5
|
/**
|
|
4
6
|
* Mantine overrides for `PartList`'s default headless renderers — the house style for rendering
|
|
5
7
|
* an `AgentPart` inside a chat surface.
|
|
@@ -19,6 +21,165 @@ export declare const threadPartRenderers: Partial<AgentPartRenderers>;
|
|
|
19
21
|
export declare const messageBlockRenderCounter: {
|
|
20
22
|
count: number;
|
|
21
23
|
};
|
|
24
|
+
/** Caps `applyInitialScroll`'s clobber-recovery retries (see `./virtualize`'s fourth composition
|
|
25
|
+
* rule for the full mechanism this guards). A small fixed budget, not a time/frame-based deadline:
|
|
26
|
+
* every observed case of virtual-core's `_willUpdate` clobbering this effect's jump resolves within
|
|
27
|
+
* one extra commit, so this exists purely as a backstop against a pathological DOM that never
|
|
28
|
+
* reports settled — not as the expected path. Once exhausted, the effect gives up and marks the
|
|
29
|
+
* jump permanently done wherever the last attempt landed, rather than fighting the DOM (or a user
|
|
30
|
+
* who has since scrolled away) forever. Exported (like `messageBlockRenderCounter`) purely so its
|
|
31
|
+
* test can assert the exact bound without duplicating the number; not part of the public surface. */
|
|
32
|
+
export declare const MAX_INITIAL_SCROLL_ATTEMPTS = 5;
|
|
33
|
+
type TranscriptRow = {
|
|
34
|
+
readonly key: string;
|
|
35
|
+
readonly node: JSX.Element;
|
|
36
|
+
};
|
|
37
|
+
type VirtualizedTranscriptProps = {
|
|
38
|
+
readonly rows: readonly TranscriptRow[];
|
|
39
|
+
readonly height: number | string;
|
|
40
|
+
readonly overscan: number;
|
|
41
|
+
readonly estimateSize: number;
|
|
42
|
+
readonly initialScroll: NonNullable<VirtualizeOptions['initialScroll']>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The degrade target: every row rendered unwindowed inside a fixed-height scroll container.
|
|
46
|
+
* This is what a `virtualize: true` transcript falls back to when `@tanstack/react-virtual` fails
|
|
47
|
+
* to resolve (peer absent) — same visual shape (a scrollable `height`-bound pane), just without
|
|
48
|
+
* windowing.
|
|
49
|
+
*
|
|
50
|
+
* NOT used as the `Suspense` fallback (see `VirtualizeSuspenseFallback` below) — mounting every
|
|
51
|
+
* real `MessageBlock` subtree for the one tick the lazy import takes to settle would make that
|
|
52
|
+
* settle a genuine unmount/remount of every rendered row, re-firing each row's effects once and
|
|
53
|
+
* risking hitting a message mid-stream during the resolve window.
|
|
54
|
+
*/
|
|
55
|
+
declare function NonVirtualizedRows({ rows, height }: VirtualizedTranscriptProps): JSX.Element;
|
|
56
|
+
/** Test-only escape hatch (mirrors `messageBlockRenderCounter` above) — proves the peer-absent
|
|
57
|
+
* degrade target itself renders every row without windowing and without throwing, without having
|
|
58
|
+
* to fight `React.lazy`'s permanent once-per-module-instance resolution cache to simulate the
|
|
59
|
+
* peer's absence at the `import()` layer (see thread-message.test.tsx's virtualization describe
|
|
60
|
+
* block for why that approach was rejected). Not exported from `agent-chat/index.ts`. */
|
|
61
|
+
export declare const nonVirtualizedRowsFallback: typeof NonVirtualizedRows;
|
|
62
|
+
/**
|
|
63
|
+
* Minimal structural slice of `Virtualizer` (virtual-core 3.17.1) that
|
|
64
|
+
* {@link resolveGuardedMeasurement} needs — `indexFromElement`/`itemSizeCache`/`options` are all
|
|
65
|
+
* public instance members, but typing against the real (generic, peer-only) `Virtualizer` class
|
|
66
|
+
* would force this test-only-exported helper to import the peer's runtime types at module scope,
|
|
67
|
+
* the exact static-import-forces-the-peer problem the surrounding `lazy(() => import(...))` exists
|
|
68
|
+
* to avoid. A structural type sidesteps that without loosening to `any`. Generic over the item
|
|
69
|
+
* element type (rather than fixed at `Element`) so it stays exactly assignable FROM a real
|
|
70
|
+
* `Virtualizer<TScrollElement, TItemElement>` at the call site below — `indexFromElement` is
|
|
71
|
+
* declared with arrow-property (not method) syntax upstream, so it is checked contravariantly, and
|
|
72
|
+
* a fixed `Element` parameter there does not structurally match a real instance's narrower
|
|
73
|
+
* `TItemElement` parameter.
|
|
74
|
+
*/
|
|
75
|
+
type MeasurementCacheHost<TItemElement extends Element> = {
|
|
76
|
+
readonly indexFromElement: (node: TItemElement) => number;
|
|
77
|
+
readonly itemSizeCache: Map<unknown, number>;
|
|
78
|
+
readonly options: {
|
|
79
|
+
readonly getItemKey: (index: number) => unknown;
|
|
80
|
+
readonly estimateSize: (index: number) => number;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Resolves the size a `measureElement` implementation should report, given what the underlying
|
|
85
|
+
* (default or custom) measurement already produced: that value unchanged when it is a genuine
|
|
86
|
+
* measurement, or the row's own last-known size when it is `0` — never a real reading for a mounted
|
|
87
|
+
* row, so a `0` only ever means the ResizeObserver measured through a `display: none` (or otherwise
|
|
88
|
+
* unlaid-out) ancestor, e.g. a virtualized transcript sitting inside a collapsed `ThreadFeedRow`.
|
|
89
|
+
* Falling through to it unguarded is what corrupted `itemSizeCache`/`measurementsCache`: virtual-
|
|
90
|
+
* core's default `resizeItem` writes whatever `measureElement` returns with no floor, so every
|
|
91
|
+
* mounted row remeasures at 0px the instant an ancestor hides, `getTotalSize()` collapses within
|
|
92
|
+
* the same tick, and the collapse is never undone — a later real remeasurement only OVERWRITES
|
|
93
|
+
* entries the ResizeObserver happens to re-fire for, it does not restore ones that silently kept
|
|
94
|
+
* their poisoned 0 (see `virtualize.ts`'s composition-rule doc for the full mechanism and the
|
|
95
|
+
* measured numbers).
|
|
96
|
+
*
|
|
97
|
+
* Takes the already-computed `size` plus the {@link MeasurementCacheHost} slice, rather than
|
|
98
|
+
* wrapping the `measureElement` function itself, so this stays a plain, non-generic-over-a-
|
|
99
|
+
* generic-function call at its one call site below (`instance` there is `Virtualizer<TScrollElement,
|
|
100
|
+
* TItemElement>` for THAT call's concrete type arguments, not the peer's own generic
|
|
101
|
+
* `measureElement` export — wrapping the latter directly hits TS's higher-rank inference limits on
|
|
102
|
+
* a doubly-generic HOF). That shape is also what makes this unit-testable with a fake host object
|
|
103
|
+
* and no ResizeObserver/layout engine at all (happy-dom has neither). Not exported from
|
|
104
|
+
* `agent-chat/index.ts`: a test-only escape hatch, matching
|
|
105
|
+
* `messageBlockRenderCounter`/`nonVirtualizedRowsFallback` above.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveGuardedMeasurement<TItemElement extends Element>(host: MeasurementCacheHost<TItemElement>, element: TItemElement, size: number): number;
|
|
108
|
+
/**
|
|
109
|
+
* Decides what `VirtualizedRowsInner`'s one-shot initial-scroll effect should do this render,
|
|
110
|
+
* given the inputs the effect closes over. Pure and peer/DOM-free — pulled out of the lazy-loaded
|
|
111
|
+
* closure for the same reason `resolveGuardedMeasurement` is: it stays unit-testable without a
|
|
112
|
+
* real `Virtualizer` instance or `@tanstack/react-virtual` itself.
|
|
113
|
+
*
|
|
114
|
+
* `'start'` and an empty transcript resolve to `'skip-done'` — there is nothing to scroll to, and
|
|
115
|
+
* DONE is permanent (see `virtualize.ts`'s fourth composition rule: the real scroll must fire at
|
|
116
|
+
* most once, ever, so a later append never yanks a user who has scrolled up). A non-positive
|
|
117
|
+
* `containerHeight` — the transcript's own scroll node measuring `0`, e.g. mounted behind a
|
|
118
|
+
* `display: none` ancestor per `virtualize.ts`'s third composition rule — resolves to
|
|
119
|
+
* `'skip-retry'`: NOT done, so the caller's effect (which re-runs after EVERY commit until the jump
|
|
120
|
+
* lands — see {@link applyInitialScroll}) gets another chance the next time this subtree renders,
|
|
121
|
+
* instead of permanently stranding the transcript at the top the moment it becomes visible. Only a
|
|
122
|
+
* genuinely measured, non-empty, `'end'`-targeted mount resolves to `'scroll'`.
|
|
123
|
+
*/
|
|
124
|
+
export declare function resolveInitialScrollAction(initialScroll: NonNullable<VirtualizeOptions['initialScroll']>, rowCount: number, containerHeight: number): 'scroll' | 'skip-done' | 'skip-retry';
|
|
125
|
+
/** The mutable half of the one-shot initial-scroll contract. `hasApplied` is whether it has
|
|
126
|
+
* PERMANENTLY finished — either there was nothing to do (`'start'`/empty transcript), or
|
|
127
|
+
* `scrollToEnd` fired and was subsequently CONFIRMED (via a real DOM read — see
|
|
128
|
+
* {@link applyInitialScroll}'s doc for why virtual-core's own tracked offset can't be trusted for
|
|
129
|
+
* this) to have survived whatever else committed since. `attempts` counts how many times
|
|
130
|
+
* `scrollToEnd` has been (re-)fired this mount; 0 means never attempted, and also gates whether a
|
|
131
|
+
* call re-derives {@link resolveInitialScrollAction} at all. One instance lives for the lifetime of
|
|
132
|
+
* a `VirtualizedRowsInner` mount, held in a `useRef` so it survives re-renders without itself
|
|
133
|
+
* triggering one. */
|
|
134
|
+
export type InitialScrollState = {
|
|
135
|
+
hasApplied: boolean;
|
|
136
|
+
attempts: number;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* The stateful half of "fires once, on first mount, and never again" (`virtualize.ts`'s fourth
|
|
140
|
+
* composition rule): wraps {@link resolveInitialScrollAction}'s per-call decision with the
|
|
141
|
+
* ACROSS-CALLS memory that decision alone can't carry. Exported (like `resolveGuardedMeasurement`)
|
|
142
|
+
* so `VirtualizedRowsInner`'s effect below calls this SAME function rather than a hand-mirrored
|
|
143
|
+
* copy for tests — a fake `scrollToEnd` is the only DOM/peer-free way to prove the guard, since a
|
|
144
|
+
* real `Virtualizer`'s own `followOnAppend` also calls the real DOM `scrollTo` on a later append,
|
|
145
|
+
* making that specific call indistinguishable from this one at the DOM boundary (see this
|
|
146
|
+
* function's test suite for the measurement that ruled that boundary out).
|
|
147
|
+
*
|
|
148
|
+
* Firing `scrollToEnd()` once is NOT the same as landing there: virtual-core 3.17.1's own
|
|
149
|
+
* `_willUpdate` (verified against the installed dist) writes to the SAME scroll container from
|
|
150
|
+
* `this.scrollOffset`, which updates only from the ASYNCHRONOUS native `scroll` event — never from
|
|
151
|
+
* the synchronous call that requested a scroll. A commit that runs `_willUpdate` before this
|
|
152
|
+
* effect gets a turn (element-attach) or that re-derives an anchor while `scrollOffset` still holds
|
|
153
|
+
* the pre-jump value writes that STALE offset straight back to `scrollTop`, undoing the jump within
|
|
154
|
+
* about one extra commit of it landing (see `./virtualize`'s fourth composition rule for the full
|
|
155
|
+
* trace this was diagnosed from). So `resolveInitialScrollAction`'s `'scroll'` outcome does not, by
|
|
156
|
+
* itself, set `hasApplied`: `isAtEnd` — a real `scrollTop`/`scrollHeight`/`clientHeight` read, the
|
|
157
|
+
* one piece of ground truth that is NEVER stale, unlike virtual-core's own tracked offset — is
|
|
158
|
+
* consulted on the NEXT call (i.e. the next commit, exactly the cadence this dependency-free effect
|
|
159
|
+
* already runs on) to confirm the jump survived. If it didn't, `scrollToEnd` fires again; if it did,
|
|
160
|
+
* `hasApplied` is set and the effect is inert for the rest of the mount. `MAX_INITIAL_SCROLL_ATTEMPTS`
|
|
161
|
+
* bounds this so a DOM that pathologically never reports settled — or a user who scrolls away mid-
|
|
162
|
+
* measurement — can never be fought forever; once exhausted, `hasApplied` is set regardless, wherever
|
|
163
|
+
* the last attempt left the container.
|
|
164
|
+
*
|
|
165
|
+
* `state.hasApplied` is the ONLY thing that can permanently stop this from calling `scrollToEnd`
|
|
166
|
+
* again: `'start'` and an empty transcript set it immediately (nothing to verify), and `'skip-retry'`
|
|
167
|
+
* deliberately leaves it `false` so a transcript that mounted behind a hidden ancestor gets another
|
|
168
|
+
* attempt on the caller's next commit, rather than being silently abandoned the moment it becomes
|
|
169
|
+
* visible. That retry — and the clobber-recovery above — is why the caller's effect carries NO
|
|
170
|
+
* dependency array: the commit that matters (a collapsed `ThreadFeedRow` flipping its body back to
|
|
171
|
+
* `display: block`, or virtual-core's own follow-up commit after this effect's own jump) changes
|
|
172
|
+
* neither the row count nor the virtualizer identity, so a dependency-keyed effect would never
|
|
173
|
+
* re-run for it.
|
|
174
|
+
*
|
|
175
|
+
* `getContainerHeight` is a THUNK, not a number, for the same reason: reading `offsetHeight` forces
|
|
176
|
+
* a synchronous layout, and it is read at most ONCE per mount — only on the very first call
|
|
177
|
+
* (`attempts === 0`) — never again while verifying or retrying, so a landed transcript still pays
|
|
178
|
+
* nothing for the effect it keeps re-running (a streaming transcript commits on every chunk).
|
|
179
|
+
* `isAtEnd` is cheaper (a plain property read, no forced reflow beyond what the browser already
|
|
180
|
+
* tracks) and is exactly what the verification/retry loop above consults instead.
|
|
181
|
+
*/
|
|
182
|
+
export declare function applyInitialScroll(state: InitialScrollState, initialScroll: NonNullable<VirtualizeOptions['initialScroll']>, rowCount: number, getContainerHeight: () => number, scrollToEnd: () => void, isAtEnd: () => boolean): void;
|
|
22
183
|
type ThreadTranscriptBase = {
|
|
23
184
|
/** Settled messages for the thread, oldest first. */
|
|
24
185
|
readonly messages: readonly ChatMessage<TranscriptPart>[];
|
|
@@ -31,18 +192,38 @@ type ThreadTranscriptBase = {
|
|
|
31
192
|
/** Called for a part whose type is neither an AgentPart variant nor a registered key. Defaults
|
|
32
193
|
* to a visible `UnknownPartChip` outside production, `null` in production — never throws. */
|
|
33
194
|
readonly fallbackRenderer?: PartRenderer<ForeignPart>;
|
|
195
|
+
/** Per-message hover-row affordances (timestamp/copy/regenerate/custom actions). Unset fields
|
|
196
|
+
* fall back to `DEFAULT_AFFORDANCES`. Never shown on the live/streaming message — see
|
|
197
|
+
* `MessageBlockProps.affordances`. */
|
|
198
|
+
readonly affordances?: MessageAffordances;
|
|
199
|
+
/** Suppresses the role label and surface chrome on a message whose predecessor shares its role
|
|
200
|
+
* and landed within 5 minutes — the Slack rhythm.
|
|
201
|
+
* @default true */
|
|
202
|
+
readonly groupConsecutive?: boolean;
|
|
34
203
|
};
|
|
35
|
-
export type ThreadTranscriptProps = ThreadTranscriptBase;
|
|
204
|
+
export type ThreadTranscriptProps = ThreadTranscriptBase & VirtualizeProps;
|
|
36
205
|
/**
|
|
37
206
|
* Renders a thread's settled messages, each role-labelled ("You" / "Assistant"). Each part
|
|
38
207
|
* resolves in order: a registered `renderers[part.type]` first, then basalt's own `PartList` for
|
|
39
208
|
* the built-in six, then `fallbackRenderer`. When `liveParts` is non-empty, an extra in-progress
|
|
40
209
|
* assistant block is appended at the tail.
|
|
41
210
|
*
|
|
211
|
+
* Set `virtualize` (with a required `height`) to window a long thread over
|
|
212
|
+
* `@tanstack/react-virtual` (an optional peer) instead of rendering every message. The virtualized
|
|
213
|
+
* transcript owns its own fixed-height scroll container and must NOT be nested inside
|
|
214
|
+
* `BasaltStickToBottom` — see `./virtualize`'s module doc. With the peer absent, `virtualize` still
|
|
215
|
+
* renders (unwindowed, inside the same fixed-height pane) rather than throwing. A virtualized
|
|
216
|
+
* transcript scrolls itself to the newest message once, on mount (`initialScroll: 'start'` opts
|
|
217
|
+
* out) — see `./virtualize`'s fourth composition rule.
|
|
218
|
+
*
|
|
42
219
|
* @example
|
|
43
220
|
* import { ThreadTranscript } from 'basalt-ui'
|
|
44
221
|
* <ThreadTranscript messages={thread.messages} liveParts={liveParts} liveStatus={runStatus} />
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* // Windowed, for a long thread:
|
|
225
|
+
* <ThreadTranscript messages={thread.messages} virtualize height={480} />
|
|
45
226
|
*/
|
|
46
|
-
export declare function ThreadTranscript(
|
|
227
|
+
export declare function ThreadTranscript(props: ThreadTranscriptProps): JSX.Element;
|
|
47
228
|
export {};
|
|
48
229
|
//# sourceMappingURL=thread-message.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread-message.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-message.tsx"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,KAAK,EAEV,kBAAkB,EAClB,WAAW,EAEX,WAAW,EACX,YAAY,EACZ,aAAa,EAGb,YAAY,EAGZ,cAAc,EACf,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"thread-message.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-message.tsx"],"names":[],"mappings":"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"}
|
|
@@ -10,12 +10,16 @@ import {
|
|
|
10
10
|
Text,
|
|
11
11
|
UnstyledButton
|
|
12
12
|
} from "@mantine/core";
|
|
13
|
-
import { useDisclosure } from "@mantine/hooks";
|
|
14
|
-
import { Fragment, memo, useMemo } from "react";
|
|
13
|
+
import { useDisclosure, useFocusWithin, useHover, useMergedRef } from "@mantine/hooks";
|
|
14
|
+
import { Fragment, lazy, memo, Suspense, useLayoutEffect, useMemo, useRef } from "react";
|
|
15
15
|
import { coalesceParts, narrowAgentPart, PartList } from "../agent/index.js";
|
|
16
16
|
import { Markdown } from "../content/markdown.js";
|
|
17
|
+
import { CopyAction } from "../content/copy-action.js";
|
|
17
18
|
import { VX } from "../tokens/index.js";
|
|
19
|
+
import { DEFAULT_AFFORDANCES } from "./message-affordances.js";
|
|
20
|
+
import { formatRelativeTime } from "./relative-time.js";
|
|
18
21
|
import { ToolChip } from "./tool-chip.js";
|
|
22
|
+
import { resolveVirtualize } from "./virtualize.js";
|
|
19
23
|
const MICRO_LABEL_STYLE = {
|
|
20
24
|
fontFamily: "var(--basalt-font-mono)",
|
|
21
25
|
fontSize: VX.text.micro,
|
|
@@ -98,6 +102,51 @@ function resolveSegments(parts, renderers, fallbackRenderer) {
|
|
|
98
102
|
flushBuffer();
|
|
99
103
|
return segments;
|
|
100
104
|
}
|
|
105
|
+
function extractCoalescedText(segments) {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
for (const segment of segments) {
|
|
108
|
+
if (segment.kind !== "agent") continue;
|
|
109
|
+
for (const part of segment.parts) {
|
|
110
|
+
if (part.type === "text") chunks.push(part.text);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return chunks.join("\n\n");
|
|
114
|
+
}
|
|
115
|
+
const GROUP_WINDOW_MS = 5 * 6e4;
|
|
116
|
+
function isConsecutiveWithPredecessor(message, predecessor) {
|
|
117
|
+
if (predecessor === void 0) return false;
|
|
118
|
+
if (predecessor.role !== message.role) return false;
|
|
119
|
+
return message.createdAt - predecessor.createdAt <= GROUP_WINDOW_MS;
|
|
120
|
+
}
|
|
121
|
+
function MessageAffordanceRow({
|
|
122
|
+
message,
|
|
123
|
+
coalescedText,
|
|
124
|
+
affordances,
|
|
125
|
+
isLastAssistant,
|
|
126
|
+
visible
|
|
127
|
+
}) {
|
|
128
|
+
const timestampMode = affordances.timestamp ?? DEFAULT_AFFORDANCES.timestamp;
|
|
129
|
+
const showTimestamp = timestampMode !== "none";
|
|
130
|
+
const showCopy = affordances.copy ?? DEFAULT_AFFORDANCES.copy;
|
|
131
|
+
const showRegenerate = isLastAssistant && affordances.onRegenerate !== void 0;
|
|
132
|
+
const customActions = affordances.actions?.({ message });
|
|
133
|
+
if (!showTimestamp && !showCopy && !showRegenerate && customActions === void 0) return null;
|
|
134
|
+
const timestampLabel = timestampMode === "absolute" ? new Date(message.createdAt).toLocaleString() : formatRelativeTime(message.createdAt);
|
|
135
|
+
return /* @__PURE__ */ jsxs(
|
|
136
|
+
Group,
|
|
137
|
+
{
|
|
138
|
+
gap: 8,
|
|
139
|
+
"data-testid": `message-affordances-${message.id}`,
|
|
140
|
+
style: { opacity: visible ? 1 : 0, transition: "opacity 120ms ease" },
|
|
141
|
+
children: [
|
|
142
|
+
showTimestamp && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: timestampLabel }),
|
|
143
|
+
showCopy && /* @__PURE__ */ jsx(CopyAction, { value: coalescedText, ariaLabel: "Copy message" }),
|
|
144
|
+
showRegenerate && /* @__PURE__ */ jsx(UnstyledButton, { onClick: () => affordances.onRegenerate?.(message.id), children: /* @__PURE__ */ jsx(Text, { style: MICRO_LABEL_STYLE, children: "Regenerate" }) }),
|
|
145
|
+
customActions
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
101
150
|
const ROLE_LABEL = {
|
|
102
151
|
user: "You",
|
|
103
152
|
assistant: "Assistant"
|
|
@@ -111,27 +160,43 @@ function MessageBlockImpl({
|
|
|
111
160
|
message,
|
|
112
161
|
streaming = false,
|
|
113
162
|
renderers,
|
|
114
|
-
fallbackRenderer
|
|
163
|
+
fallbackRenderer,
|
|
164
|
+
grouped = false,
|
|
165
|
+
affordances,
|
|
166
|
+
isLastAssistant = false
|
|
115
167
|
}) {
|
|
116
168
|
messageBlockRenderCounter.count += 1;
|
|
117
169
|
const settled = !streaming;
|
|
170
|
+
const { hovered, ref: hoverRef } = useHover();
|
|
171
|
+
const { focused, ref: focusWithinRef } = useFocusWithin();
|
|
172
|
+
const ref = useMergedRef(hoverRef, focusWithinRef);
|
|
118
173
|
const segments = useMemo(
|
|
119
174
|
() => resolveSegments(message.parts, renderers, fallbackRenderer),
|
|
120
175
|
[message.parts, renderers, fallbackRenderer]
|
|
121
176
|
);
|
|
122
177
|
const finishIndicator = message.finish === void 0 ? void 0 : FINISH_INDICATOR[message.finish];
|
|
178
|
+
const coalescedText = useMemo(
|
|
179
|
+
() => affordances === void 0 ? "" : extractCoalescedText(segments),
|
|
180
|
+
[affordances, segments]
|
|
181
|
+
);
|
|
182
|
+
const showHeader = !grouped || streaming || finishIndicator !== void 0;
|
|
123
183
|
return /* @__PURE__ */ jsx(
|
|
124
184
|
Box,
|
|
125
185
|
{
|
|
186
|
+
ref,
|
|
187
|
+
"data-testid": `agent-message-${message.id}`,
|
|
188
|
+
"data-grouped": grouped,
|
|
126
189
|
style: {
|
|
127
190
|
padding: "var(--vx-space-agent-message-inset-y) var(--vx-space-agent-message-inset-x)",
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
191
|
+
...grouped ? {} : {
|
|
192
|
+
backgroundColor: message.role === "user" ? VX.surface.subtle : VX.surface.panel,
|
|
193
|
+
borderRadius: VX.radiusCard,
|
|
194
|
+
boxShadow: VX.shadowCard
|
|
195
|
+
}
|
|
131
196
|
},
|
|
132
197
|
children: /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
|
|
133
|
-
/* @__PURE__ */ jsxs(Group, { gap: 6, align: "center", children: [
|
|
134
|
-
/* @__PURE__ */ jsx(Text, { style: MICRO_LABEL_STYLE, children: ROLE_LABEL[message.role] }),
|
|
198
|
+
showHeader && /* @__PURE__ */ jsxs(Group, { gap: 6, align: "center", children: [
|
|
199
|
+
!grouped && /* @__PURE__ */ jsx(Text, { style: MICRO_LABEL_STYLE, children: ROLE_LABEL[message.role] }),
|
|
135
200
|
streaming && /* @__PURE__ */ jsx(Loader, { size: "xs" }),
|
|
136
201
|
finishIndicator !== void 0 && /* @__PURE__ */ jsx(Text, { style: { ...MICRO_LABEL_STYLE, color: finishIndicator.statusToken }, children: finishIndicator.label })
|
|
137
202
|
] }),
|
|
@@ -151,51 +216,267 @@ function MessageBlockImpl({
|
|
|
151
216
|
settled,
|
|
152
217
|
role: message.role
|
|
153
218
|
}) }, segment.key)
|
|
219
|
+
),
|
|
220
|
+
affordances !== void 0 && /* @__PURE__ */ jsx(
|
|
221
|
+
MessageAffordanceRow,
|
|
222
|
+
{
|
|
223
|
+
message,
|
|
224
|
+
coalescedText,
|
|
225
|
+
affordances,
|
|
226
|
+
isLastAssistant,
|
|
227
|
+
visible: hovered || focused
|
|
228
|
+
}
|
|
154
229
|
)
|
|
155
230
|
] })
|
|
156
231
|
}
|
|
157
232
|
);
|
|
158
233
|
}
|
|
159
234
|
function areMessageBlockPropsEqual(prev, next) {
|
|
160
|
-
return prev.message === next.message && prev.streaming === next.streaming && prev.renderers === next.renderers && prev.fallbackRenderer === next.fallbackRenderer;
|
|
235
|
+
return prev.message === next.message && prev.streaming === next.streaming && prev.renderers === next.renderers && prev.fallbackRenderer === next.fallbackRenderer && prev.grouped === next.grouped && prev.affordances === next.affordances && prev.isLastAssistant === next.isLastAssistant;
|
|
161
236
|
}
|
|
162
237
|
const MessageBlock = memo(MessageBlockImpl, areMessageBlockPropsEqual);
|
|
238
|
+
const DEFAULT_VIRTUALIZE_OVERSCAN = 6;
|
|
239
|
+
const DEFAULT_VIRTUALIZE_ESTIMATE_SIZE = 160;
|
|
240
|
+
const DEFAULT_VIRTUALIZE_SCROLL_END_THRESHOLD = 64;
|
|
241
|
+
const DEFAULT_VIRTUALIZE_INITIAL_SCROLL = "end";
|
|
242
|
+
const MAX_INITIAL_SCROLL_ATTEMPTS = 5;
|
|
243
|
+
function NonVirtualizedRows({ rows, height }) {
|
|
244
|
+
return (
|
|
245
|
+
// theme-allow — degrade target owns its own scroll node, same as the real virtualizer below.
|
|
246
|
+
/* @__PURE__ */ jsx(Box, { style: { height, overflow: "auto" }, children: /* @__PURE__ */ jsx(Stack, { gap: "sm", children: rows.map((row) => /* @__PURE__ */ jsx(Fragment, { children: row.node }, row.key)) }) })
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const nonVirtualizedRowsFallback = NonVirtualizedRows;
|
|
250
|
+
function resolveGuardedMeasurement(host, element, size) {
|
|
251
|
+
if (size > 0) return size;
|
|
252
|
+
const index = host.indexFromElement(element);
|
|
253
|
+
const key = host.options.getItemKey(index);
|
|
254
|
+
return host.itemSizeCache.get(key) ?? host.options.estimateSize(index);
|
|
255
|
+
}
|
|
256
|
+
function resolveInitialScrollAction(initialScroll, rowCount, containerHeight) {
|
|
257
|
+
if (initialScroll === "start") return "skip-done";
|
|
258
|
+
if (rowCount === 0) return "skip-done";
|
|
259
|
+
if (containerHeight <= 0) return "skip-retry";
|
|
260
|
+
return "scroll";
|
|
261
|
+
}
|
|
262
|
+
function applyInitialScroll(state, initialScroll, rowCount, getContainerHeight, scrollToEnd, isAtEnd) {
|
|
263
|
+
if (state.hasApplied) return;
|
|
264
|
+
if (state.attempts > 0) {
|
|
265
|
+
if (isAtEnd() || state.attempts >= MAX_INITIAL_SCROLL_ATTEMPTS) {
|
|
266
|
+
state.hasApplied = true;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
scrollToEnd();
|
|
270
|
+
state.attempts += 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const action = resolveInitialScrollAction(initialScroll, rowCount, getContainerHeight());
|
|
274
|
+
if (action === "skip-retry") return;
|
|
275
|
+
if (action === "skip-done") {
|
|
276
|
+
state.hasApplied = true;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
scrollToEnd();
|
|
280
|
+
state.attempts += 1;
|
|
281
|
+
}
|
|
282
|
+
function isContainerAtEnd(container, threshold) {
|
|
283
|
+
if (container === null) return false;
|
|
284
|
+
const distanceFromEnd = container.scrollHeight - container.clientHeight - container.scrollTop;
|
|
285
|
+
return distanceFromEnd <= threshold;
|
|
286
|
+
}
|
|
287
|
+
const LazyVirtualizedRows = lazy(
|
|
288
|
+
() => import("@tanstack/react-virtual").then(({ useVirtualizer, measureElement: defaultMeasureElement }) => {
|
|
289
|
+
function VirtualizedRowsInner({
|
|
290
|
+
rows,
|
|
291
|
+
height,
|
|
292
|
+
overscan,
|
|
293
|
+
estimateSize,
|
|
294
|
+
initialScroll
|
|
295
|
+
}) {
|
|
296
|
+
const parentRef = useRef(null);
|
|
297
|
+
const initialScrollStateRef = useRef({
|
|
298
|
+
hasApplied: false,
|
|
299
|
+
attempts: 0
|
|
300
|
+
});
|
|
301
|
+
const virtualizer = useVirtualizer({
|
|
302
|
+
count: rows.length,
|
|
303
|
+
getScrollElement: () => parentRef.current,
|
|
304
|
+
estimateSize: () => estimateSize,
|
|
305
|
+
overscan,
|
|
306
|
+
getItemKey: (index) => rows[index]?.key ?? index,
|
|
307
|
+
anchorTo: "end",
|
|
308
|
+
followOnAppend: true,
|
|
309
|
+
scrollEndThreshold: DEFAULT_VIRTUALIZE_SCROLL_END_THRESHOLD,
|
|
310
|
+
useFlushSync: false,
|
|
311
|
+
// Guards against the hidden-ancestor cache-poisoning mechanism
|
|
312
|
+
// `resolveGuardedMeasurement`'s doc and `virtualize.ts`'s composition rules describe —
|
|
313
|
+
// NOT `enabled: false`, which virtual-core 3.17.1 wires to WIPE
|
|
314
|
+
// `itemSizeCache`/`measurementsCache` entirely (`getMeasurements`'s `!enabled` branch)
|
|
315
|
+
// rather than pause measurement, so toggling it off/on around a collapse would be
|
|
316
|
+
// strictly worse than the bug it was meant to fix.
|
|
317
|
+
measureElement: (element, entry, instance) => resolveGuardedMeasurement(
|
|
318
|
+
instance,
|
|
319
|
+
element,
|
|
320
|
+
defaultMeasureElement(element, entry, instance)
|
|
321
|
+
)
|
|
322
|
+
});
|
|
323
|
+
useLayoutEffect(() => {
|
|
324
|
+
applyInitialScroll(
|
|
325
|
+
initialScrollStateRef.current,
|
|
326
|
+
initialScroll,
|
|
327
|
+
rows.length,
|
|
328
|
+
() => parentRef.current?.offsetHeight ?? 0,
|
|
329
|
+
() => virtualizer.scrollToEnd({ behavior: "auto" }),
|
|
330
|
+
() => isContainerAtEnd(parentRef.current, DEFAULT_VIRTUALIZE_SCROLL_END_THRESHOLD)
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
return (
|
|
334
|
+
// theme-allow — TanStack Virtual measures/scrolls this element (never nest in BasaltStickToBottom, see ./virtualize).
|
|
335
|
+
/* @__PURE__ */ jsx(Box, { ref: parentRef, style: { height, overflow: "auto" }, children: /* @__PURE__ */ jsx(Box, { style: { height: virtualizer.getTotalSize(), position: "relative" }, children: virtualizer.getVirtualItems().map((virtualItem) => {
|
|
336
|
+
const row = rows[virtualItem.index];
|
|
337
|
+
if (row === void 0) return null;
|
|
338
|
+
return /* @__PURE__ */ jsx(
|
|
339
|
+
Box,
|
|
340
|
+
{
|
|
341
|
+
ref: virtualizer.measureElement,
|
|
342
|
+
"data-index": virtualItem.index,
|
|
343
|
+
style: {
|
|
344
|
+
position: "absolute",
|
|
345
|
+
top: 0,
|
|
346
|
+
left: 0,
|
|
347
|
+
width: "100%",
|
|
348
|
+
transform: `translateY(${virtualItem.start}px)`
|
|
349
|
+
},
|
|
350
|
+
children: row.node
|
|
351
|
+
},
|
|
352
|
+
row.key
|
|
353
|
+
);
|
|
354
|
+
}) }) })
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return { default: VirtualizedRowsInner };
|
|
358
|
+
}).catch(() => ({ default: NonVirtualizedRows }))
|
|
359
|
+
);
|
|
360
|
+
function VirtualizeSuspenseFallback({ height }) {
|
|
361
|
+
return /* @__PURE__ */ jsx(Box, { style: { height, overflow: "auto" } });
|
|
362
|
+
}
|
|
363
|
+
function VirtualizedTranscript(props) {
|
|
364
|
+
return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(VirtualizeSuspenseFallback, { height: props.height }), children: /* @__PURE__ */ jsx(LazyVirtualizedRows, { ...props }) });
|
|
365
|
+
}
|
|
163
366
|
const EMPTY_RENDERERS = {};
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
367
|
+
function useStableCallback(callback) {
|
|
368
|
+
const callbackRef = useRef(callback);
|
|
369
|
+
callbackRef.current = callback;
|
|
370
|
+
const stable = useMemo(
|
|
371
|
+
() => (...args) => callbackRef.current?.(...args),
|
|
372
|
+
[]
|
|
373
|
+
);
|
|
374
|
+
return callback === void 0 ? void 0 : stable;
|
|
375
|
+
}
|
|
376
|
+
function ThreadTranscript(props) {
|
|
377
|
+
const {
|
|
378
|
+
messages,
|
|
379
|
+
liveParts,
|
|
380
|
+
liveStatus,
|
|
381
|
+
renderers: renderersProp,
|
|
382
|
+
fallbackRenderer = DEFAULT_FALLBACK_RENDERER,
|
|
383
|
+
affordances: affordancesProp,
|
|
384
|
+
groupConsecutive = true
|
|
385
|
+
} = props;
|
|
386
|
+
const virtualized = resolveVirtualize(props);
|
|
171
387
|
const renderers = useMemo(() => renderersProp ?? EMPTY_RENDERERS, [renderersProp]);
|
|
172
388
|
const liveMessage = useMemo(() => {
|
|
173
389
|
if (liveParts === void 0 || liveParts.length === 0) return null;
|
|
174
390
|
return { id: "__live__", role: "assistant", parts: liveParts, createdAt: 0 };
|
|
175
391
|
}, [liveParts]);
|
|
176
|
-
|
|
177
|
-
messages.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
392
|
+
const lastAssistantMessageId = useMemo(() => {
|
|
393
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
394
|
+
const candidate = messages[i];
|
|
395
|
+
if (candidate?.role === "assistant") return candidate.id;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}, [messages]);
|
|
399
|
+
const stableOnRegenerate = useStableCallback(affordancesProp?.onRegenerate);
|
|
400
|
+
const actions = affordancesProp?.actions;
|
|
401
|
+
const resolvedAffordances = useMemo(
|
|
402
|
+
() => ({
|
|
403
|
+
timestamp: affordancesProp?.timestamp ?? DEFAULT_AFFORDANCES.timestamp,
|
|
404
|
+
copy: affordancesProp?.copy ?? DEFAULT_AFFORDANCES.copy,
|
|
405
|
+
// `exactOptionalPropertyTypes` forbids assigning a possibly-`undefined` value to an optional
|
|
406
|
+
// property directly — spread each field in only when it is actually present, rather than
|
|
407
|
+
// widening `MessageAffordances` itself to accept an explicit `undefined`.
|
|
408
|
+
...stableOnRegenerate !== void 0 && { onRegenerate: stableOnRegenerate },
|
|
409
|
+
...actions !== void 0 && { actions }
|
|
410
|
+
}),
|
|
411
|
+
[affordancesProp?.timestamp, affordancesProp?.copy, stableOnRegenerate, actions]
|
|
412
|
+
);
|
|
413
|
+
const settledRows = useMemo(() => {
|
|
414
|
+
const out = [];
|
|
415
|
+
messages.forEach((message, index) => {
|
|
416
|
+
out.push({
|
|
417
|
+
key: message.id,
|
|
418
|
+
node: /* @__PURE__ */ jsx(
|
|
419
|
+
MessageBlock,
|
|
420
|
+
{
|
|
421
|
+
message,
|
|
422
|
+
renderers,
|
|
423
|
+
fallbackRenderer,
|
|
424
|
+
grouped: groupConsecutive && isConsecutiveWithPredecessor(message, messages[index - 1]),
|
|
425
|
+
isLastAssistant: message.id === lastAssistantMessageId,
|
|
426
|
+
affordances: resolvedAffordances
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
return out;
|
|
432
|
+
}, [
|
|
433
|
+
messages,
|
|
434
|
+
renderers,
|
|
435
|
+
fallbackRenderer,
|
|
436
|
+
groupConsecutive,
|
|
437
|
+
lastAssistantMessageId,
|
|
438
|
+
resolvedAffordances
|
|
439
|
+
]);
|
|
440
|
+
const liveRow = useMemo(() => {
|
|
441
|
+
if (liveMessage === null) return null;
|
|
442
|
+
return {
|
|
443
|
+
key: liveMessage.id,
|
|
444
|
+
node: /* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-atomic": "false", "aria-relevant": "additions", children: /* @__PURE__ */ jsx(
|
|
445
|
+
MessageBlock,
|
|
446
|
+
{
|
|
447
|
+
message: liveMessage,
|
|
448
|
+
streaming: liveStatus === "streaming",
|
|
449
|
+
renderers,
|
|
450
|
+
fallbackRenderer
|
|
451
|
+
}
|
|
452
|
+
) })
|
|
453
|
+
};
|
|
454
|
+
}, [liveMessage, liveStatus, renderers, fallbackRenderer]);
|
|
455
|
+
const rows = useMemo(
|
|
456
|
+
() => liveRow === null ? settledRows : [...settledRows, liveRow],
|
|
457
|
+
[settledRows, liveRow]
|
|
458
|
+
);
|
|
459
|
+
if (virtualized !== null) {
|
|
460
|
+
return /* @__PURE__ */ jsx(
|
|
461
|
+
VirtualizedTranscript,
|
|
188
462
|
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
463
|
+
rows,
|
|
464
|
+
height: virtualized.height,
|
|
465
|
+
overscan: virtualized.options.overscan ?? DEFAULT_VIRTUALIZE_OVERSCAN,
|
|
466
|
+
estimateSize: virtualized.options.estimateSize ?? DEFAULT_VIRTUALIZE_ESTIMATE_SIZE,
|
|
467
|
+
initialScroll: virtualized.options.initialScroll ?? DEFAULT_VIRTUALIZE_INITIAL_SCROLL
|
|
193
468
|
}
|
|
194
|
-
)
|
|
195
|
-
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return /* @__PURE__ */ jsx(Stack, { gap: "sm", children: rows.map((row) => /* @__PURE__ */ jsx(Fragment, { children: row.node }, row.key)) });
|
|
196
472
|
}
|
|
197
473
|
export {
|
|
474
|
+
MAX_INITIAL_SCROLL_ATTEMPTS,
|
|
198
475
|
ThreadTranscript,
|
|
476
|
+
applyInitialScroll,
|
|
199
477
|
messageBlockRenderCounter,
|
|
478
|
+
nonVirtualizedRowsFallback,
|
|
479
|
+
resolveGuardedMeasurement,
|
|
480
|
+
resolveInitialScrollAction,
|
|
200
481
|
threadPartRenderers
|
|
201
482
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread-outcome-card.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-outcome-card.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"thread-outcome-card.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-outcome-card.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,UAAU,CAAA;AAiGzD,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,QAAQ,EACR,QAAQ,GACT,EAAE,sBAAsB,GAAG,GAAG,CAAC,OAAO,CA+BtC"}
|