basalt-ui 1.12.0 → 1.14.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 (123) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +38 -35
  4. package/agent/rules/basalt-agent.md +14 -0
  5. package/configs/oxlint-plugin.js +7 -3
  6. package/configs/oxlint-plugin.test.ts +34 -5
  7. package/configs/oxlint.json +4 -4
  8. package/dist/agent/adapter.d.ts +5 -1
  9. package/dist/agent/adapter.d.ts.map +1 -1
  10. package/dist/agent/adapter.js +22 -13
  11. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  12. package/dist/agent/ai-sdk-transport.js +6 -2
  13. package/dist/agent/id.d.ts +67 -19
  14. package/dist/agent/id.d.ts.map +1 -1
  15. package/dist/agent/id.js +21 -7
  16. package/dist/agent/thread.d.ts +4 -0
  17. package/dist/agent/thread.d.ts.map +1 -1
  18. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  19. package/dist/agent/use-agent-stream.js +2 -2
  20. package/dist/agent/use-agent-thread-runs.d.ts +24 -3
  21. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  22. package/dist/agent/use-agent-thread-runs.js +55 -15
  23. package/dist/agent-chat/index.d.ts +4 -0
  24. package/dist/agent-chat/index.d.ts.map +1 -1
  25. package/dist/agent-chat/index.js +2 -0
  26. package/dist/agent-chat/message-affordances.d.ts +59 -0
  27. package/dist/agent-chat/message-affordances.d.ts.map +1 -0
  28. package/dist/agent-chat/message-affordances.js +7 -0
  29. package/dist/agent-chat/relative-time.d.ts +27 -0
  30. package/dist/agent-chat/relative-time.d.ts.map +1 -0
  31. package/dist/agent-chat/relative-time.js +27 -0
  32. package/dist/agent-chat/relative-time.test.d.ts +2 -0
  33. package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
  34. package/dist/agent-chat/thread-feed-row.d.ts +65 -0
  35. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
  36. package/dist/agent-chat/thread-feed-row.js +156 -0
  37. package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
  38. package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
  39. package/dist/agent-chat/thread-feed.d.ts +34 -5
  40. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  41. package/dist/agent-chat/thread-feed.js +52 -19
  42. package/dist/agent-chat/thread-feed.test.d.ts +2 -0
  43. package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
  44. package/dist/agent-chat/thread-message.d.ts +183 -2
  45. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  46. package/dist/agent-chat/thread-message.js +315 -34
  47. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  48. package/dist/agent-chat/thread-outcome-card.js +1 -16
  49. package/dist/agent-chat/thread-workspace.d.ts +14 -2
  50. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  51. package/dist/agent-chat/thread-workspace.js +29 -3
  52. package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
  53. package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
  54. package/dist/agent-chat/virtualize.d.ts +123 -0
  55. package/dist/agent-chat/virtualize.d.ts.map +1 -0
  56. package/dist/agent-chat/virtualize.js +9 -0
  57. package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
  58. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
  59. package/dist/data/index.d.ts +5 -1
  60. package/dist/data/index.d.ts.map +1 -1
  61. package/dist/data/virtual-list.d.ts.map +1 -1
  62. package/dist/data/virtual.d.ts +2 -1
  63. package/dist/data/virtual.d.ts.map +1 -1
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -0
  67. package/dist/shell/app-sidebar.d.ts +19 -1
  68. package/dist/shell/app-sidebar.d.ts.map +1 -1
  69. package/dist/shell/app-sidebar.js +30 -26
  70. package/dist/shell/app-sidebar.module.css +11 -0
  71. package/dist/shell/app-sidebar.module.css.d.ts +1 -0
  72. package/dist/shell/app-sidebar.test.d.ts +2 -0
  73. package/dist/shell/app-sidebar.test.d.ts.map +1 -0
  74. package/dist/shell/index.d.ts +10 -1
  75. package/dist/shell/index.d.ts.map +1 -1
  76. package/dist/shell/index.js +2 -0
  77. package/dist/shell/index.test.d.ts +2 -0
  78. package/dist/shell/index.test.d.ts.map +1 -0
  79. package/dist/surfaces.d.ts +2 -2
  80. package/dist/surfaces.d.ts.map +1 -1
  81. package/dist/surfaces.js +3 -2
  82. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  83. package/llms.txt +6 -6
  84. package/package.json +2 -2
  85. package/src/agent/adapter.test.ts +237 -0
  86. package/src/agent/adapter.ts +72 -14
  87. package/src/agent/ai-sdk-transport.test.ts +64 -0
  88. package/src/agent/ai-sdk-transport.ts +8 -2
  89. package/src/agent/id.test.ts +95 -1
  90. package/src/agent/id.ts +98 -28
  91. package/src/agent/thread.ts +4 -0
  92. package/src/agent/use-agent-stream.test.tsx +22 -0
  93. package/src/agent/use-agent-stream.ts +6 -3
  94. package/src/agent/use-agent-thread-runs.test.tsx +326 -0
  95. package/src/agent/use-agent-thread-runs.ts +159 -23
  96. package/src/agent/use-agent-thread-runs.wedge.test.tsx +80 -0
  97. package/src/agent-chat/index.ts +12 -0
  98. package/src/agent-chat/message-affordances.ts +61 -0
  99. package/src/agent-chat/relative-time.test.ts +95 -0
  100. package/src/agent-chat/relative-time.ts +55 -0
  101. package/src/agent-chat/thread-feed-row.test.tsx +510 -0
  102. package/src/agent-chat/thread-feed-row.tsx +274 -0
  103. package/src/agent-chat/thread-feed.test.tsx +241 -0
  104. package/src/agent-chat/thread-feed.tsx +119 -18
  105. package/src/agent-chat/thread-message.test.tsx +937 -3
  106. package/src/agent-chat/thread-message.tsx +766 -38
  107. package/src/agent-chat/thread-outcome-card.tsx +1 -25
  108. package/src/agent-chat/thread-workspace.test.tsx +169 -0
  109. package/src/agent-chat/thread-workspace.tsx +69 -5
  110. package/src/agent-chat/virtualize.ts +132 -0
  111. package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
  112. package/src/data/index.ts +5 -1
  113. package/src/data/virtual-list.tsx +2 -1
  114. package/src/data/virtual.ts +2 -1
  115. package/src/index.ts +5 -0
  116. package/src/shell/app-sidebar.module.css +11 -0
  117. package/src/shell/app-sidebar.module.css.d.ts +1 -0
  118. package/src/shell/app-sidebar.test.tsx +98 -0
  119. package/src/shell/app-sidebar.tsx +22 -0
  120. package/src/shell/index.test.tsx +35 -0
  121. package/src/shell/index.tsx +11 -0
  122. package/src/surfaces.ts +2 -1
  123. 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({ messages, liveParts, liveStatus, renderers: renderersProp, fallbackRenderer, }: ThreadTranscriptProps): JSX.Element;
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;AAsGjB;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,EAAE,OAAO,CAAC,kBAAkB,CAM3D,CAAA;AAqID;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;CAAe,CAAA;AAmFrD,KAAK,oBAAoB,GAAG;IAC1B,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,CAAC,cAAc,CAAC,EAAE,CAAA;IACzD,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;IAC9C,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY,CAAA;IAClC,kFAAkF;IAClF,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC;iGAC6F;IAC7F,QAAQ,CAAC,gBAAgB,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,CAAA;CACtD,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,CAAA;AAExD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EAAE,aAAa,EACxB,gBAA4C,GAC7C,EAAE,qBAAqB,GAAG,GAAG,CAAC,OAAO,CAgCrC"}
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
- backgroundColor: message.role === "user" ? VX.surface.subtle : VX.surface.panel,
129
- borderRadius: VX.radiusCard,
130
- boxShadow: VX.shadowCard
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 ThreadTranscript({
165
- messages,
166
- liveParts,
167
- liveStatus,
168
- renderers: renderersProp,
169
- fallbackRenderer = DEFAULT_FALLBACK_RENDERER
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
- return /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
177
- messages.map((message) => /* @__PURE__ */ jsx(
178
- MessageBlock,
179
- {
180
- message,
181
- renderers,
182
- fallbackRenderer
183
- },
184
- message.id
185
- )),
186
- liveMessage !== null && /* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-atomic": "false", "aria-relevant": "additions", children: /* @__PURE__ */ jsx(
187
- MessageBlock,
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
- message: liveMessage,
190
- streaming: liveStatus === "streaming",
191
- renderers,
192
- fallbackRenderer
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;AAyHzD,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"}
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"}