@assistant-ui/react 0.15.15 → 0.15.16

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 (67) hide show
  1. package/dist/devtools/DevToolsHooks.d.ts +0 -2
  2. package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
  3. package/dist/devtools/DevToolsHooks.js +10 -12
  4. package/dist/devtools/DevToolsHooks.js.map +1 -1
  5. package/dist/mcp-apps/McpAppRenderer.js +1 -1
  6. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  7. package/dist/mcp-apps/bridge.js +2 -8
  8. package/dist/mcp-apps/bridge.js.map +1 -1
  9. package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
  10. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +4 -1
  11. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  12. package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
  13. package/dist/primitives/branchPicker/BranchPickerRoot.js +20 -15
  14. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  15. package/dist/primitives/composer/ComposerDictationTranscript.d.ts +2 -2
  16. package/dist/primitives/composer/ComposerDictationTranscript.js +2 -2
  17. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  18. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  19. package/dist/primitives/composer/trigger/triggerSelectionResource.js +24 -20
  20. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  21. package/dist/primitives/message.js +1 -1
  22. package/dist/primitives/thread/ThreadRoot.d.ts +3 -0
  23. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  24. package/dist/primitives/thread/ThreadRoot.js +3 -0
  25. package/dist/primitives/thread/ThreadRoot.js.map +1 -1
  26. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  27. package/dist/sandbox-host/SandboxHost.js +4 -1
  28. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  29. package/dist/unstable/useComposerInputHistory.js +1 -1
  30. package/dist/utils/invokeCallbackSafely.d.ts +5 -0
  31. package/dist/utils/invokeCallbackSafely.d.ts.map +1 -0
  32. package/dist/utils/invokeCallbackSafely.js +15 -0
  33. package/dist/utils/invokeCallbackSafely.js.map +1 -0
  34. package/dist/utils/smooth/SmoothContext.d.ts +6 -6
  35. package/dist/utils/smooth/useSmooth.d.ts +3 -0
  36. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  37. package/dist/utils/smooth/useSmooth.js +16 -1
  38. package/dist/utils/smooth/useSmooth.js.map +1 -1
  39. package/package.json +5 -5
  40. package/src/devtools/DevToolsHooks.test.ts +40 -0
  41. package/src/devtools/DevToolsHooks.ts +10 -15
  42. package/src/mcp-apps/bridge.test.ts +21 -0
  43. package/src/mcp-apps/bridge.ts +2 -14
  44. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
  45. package/src/primitives/actionBar/ActionBarExportMarkdown.test.tsx +67 -0
  46. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +3 -1
  47. package/src/primitives/branchPicker/BranchPickerRoot.tsx +5 -3
  48. package/src/primitives/composer/ComposerDictationTranscript.tsx +2 -2
  49. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +135 -0
  50. package/src/primitives/composer/trigger/triggerSelectionResource.ts +2 -0
  51. package/src/primitives/thread/ThreadRoot.tsx +3 -0
  52. package/src/sandbox-host/SandboxHost.test.tsx +61 -0
  53. package/src/sandbox-host/SandboxHost.tsx +6 -2
  54. package/src/unstable/useLiveCompletionAdapter.test.tsx +57 -0
  55. package/src/utils/invokeCallbackSafely.ts +11 -0
  56. package/src/utils/smooth/useSmooth.test.tsx +110 -0
  57. package/src/utils/smooth/useSmooth.ts +20 -6
  58. package/dist/context/react/utils/ensureBinding.d.ts +0 -5
  59. package/dist/context/react/utils/ensureBinding.d.ts.map +0 -1
  60. package/dist/context/react/utils/ensureBinding.js +0 -24
  61. package/dist/context/react/utils/ensureBinding.js.map +0 -1
  62. package/dist/context/react/utils/useRuntimeState.d.ts +0 -13
  63. package/dist/context/react/utils/useRuntimeState.d.ts.map +0 -1
  64. package/dist/context/react/utils/useRuntimeState.js +0 -33
  65. package/dist/context/react/utils/useRuntimeState.js.map +0 -1
  66. package/src/context/react/utils/ensureBinding.ts +0 -46
  67. package/src/context/react/utils/useRuntimeState.ts +0 -46
@@ -61,7 +61,9 @@ export const ActionBarPrimitiveExportMarkdown = forwardRef<
61
61
  ref={forwardedRef}
62
62
  disabled={disabled || !callback}
63
63
  onClick={composeEventHandlers(onClick, () => {
64
- callback?.();
64
+ void callback?.().catch((error: unknown) => {
65
+ console.error("[assistant-ui] markdown export failed:", error);
66
+ });
65
67
  })}
66
68
  />
67
69
  );
@@ -6,7 +6,7 @@ import {
6
6
  forwardRef,
7
7
  type ComponentPropsWithoutRef,
8
8
  } from "react";
9
- import { MessagePrimitiveIf as If } from "../message/MessageIf";
9
+ import { AuiIf } from "@assistant-ui/store";
10
10
 
11
11
  export namespace BranchPickerPrimitiveRoot {
12
12
  export type Element = ComponentRef<typeof Primitive.div>;
@@ -42,9 +42,11 @@ export const BranchPickerPrimitiveRoot = forwardRef<
42
42
  BranchPickerPrimitiveRoot.Props
43
43
  >(({ hideWhenSingleBranch, ...rest }, ref) => {
44
44
  return (
45
- <If hasBranches={hideWhenSingleBranch ? true : undefined}>
45
+ <AuiIf
46
+ condition={(s) => !hideWhenSingleBranch || s.message.branchCount >= 2}
47
+ >
46
48
  <Primitive.div {...rest} ref={ref} />
47
- </If>
49
+ </AuiIf>
48
50
  );
49
51
  });
50
52
 
@@ -21,11 +21,11 @@ export namespace ComposerPrimitiveDictationTranscript {
21
21
  *
22
22
  * @example
23
23
  * ```tsx
24
- * <ComposerPrimitive.If dictation>
24
+ * <AuiIf condition={(s) => s.composer.dictation != null}>
25
25
  * <div className="dictation-preview">
26
26
  * <ComposerPrimitive.DictationTranscript />
27
27
  * </div>
28
- * </ComposerPrimitive.If>
28
+ * </AuiIf>
29
29
  * ```
30
30
  */
31
31
  export const ComposerPrimitiveDictationTranscript = forwardRef<
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createTapRoot, flushTapSync, useResource } from "@assistant-ui/tap";
3
+ import type {
4
+ Unstable_DirectiveFormatter,
5
+ Unstable_TriggerItem,
6
+ } from "@assistant-ui/core";
7
+ import type { AssistantClient } from "@assistant-ui/store";
8
+ import { TriggerDetectionResource } from "./triggerDetectionResource";
9
+ import {
10
+ TriggerSelectionResource,
11
+ type TriggerBehavior,
12
+ } from "./triggerSelectionResource";
13
+
14
+ const item: Unstable_TriggerItem = {
15
+ id: "file",
16
+ type: "file",
17
+ label: "file.txt",
18
+ };
19
+
20
+ const formatter: Unstable_DirectiveFormatter = {
21
+ serialize: () => "@file.txt",
22
+ parse: () => [],
23
+ };
24
+
25
+ const makeAui = (textRef: { value: string }) => {
26
+ const setText = vi.fn((value: string) => {
27
+ textRef.value = value;
28
+ });
29
+
30
+ return {
31
+ aui: {
32
+ composer: {
33
+ getState: () => ({ text: textRef.value }),
34
+ setText,
35
+ },
36
+ } as unknown as AssistantClient,
37
+ setText,
38
+ };
39
+ };
40
+
41
+ const render = ({
42
+ behavior,
43
+ text,
44
+ trigger,
45
+ }: {
46
+ behavior: TriggerBehavior;
47
+ text: string;
48
+ trigger: { offset: number; query: string };
49
+ }) => {
50
+ const textRef = { value: text };
51
+ const { aui, setText } = makeAui(textRef);
52
+ const setCursorPosition = vi.fn();
53
+ const onSelected = vi.fn();
54
+ const root = createTapRoot(function Root() {
55
+ return useResource(
56
+ TriggerSelectionResource({
57
+ behavior,
58
+ trigger,
59
+ aui,
60
+ triggerChar: "@",
61
+ setCursorPosition,
62
+ onSelected,
63
+ }),
64
+ );
65
+ });
66
+
67
+ return { root, setText, setCursorPosition, onSelected };
68
+ };
69
+
70
+ describe("TriggerSelectionResource", () => {
71
+ it("resynchronizes the cursor after inserting a directive", () => {
72
+ const { root, setText, setCursorPosition, onSelected } = render({
73
+ behavior: { kind: "directive", formatter },
74
+ text: "@file tail",
75
+ trigger: { offset: 0, query: "file" },
76
+ });
77
+
78
+ root.getValue().selectItem(item);
79
+
80
+ expect(setText).toHaveBeenCalledWith("@file.txt tail");
81
+ expect(setCursorPosition).toHaveBeenCalledWith("@file.txt".length + 1);
82
+ expect(onSelected).toHaveBeenCalledOnce();
83
+ });
84
+
85
+ it("resynchronizes the cursor after removing an action", () => {
86
+ const { root, setText, setCursorPosition } = render({
87
+ behavior: {
88
+ kind: "action",
89
+ formatter,
90
+ onExecute: vi.fn(),
91
+ removeOnExecute: true,
92
+ },
93
+ text: "before @file tail",
94
+ trigger: { offset: 7, query: "file" },
95
+ });
96
+
97
+ root.getValue().selectItem(item);
98
+
99
+ expect(setText).toHaveBeenCalledWith("before tail");
100
+ expect(setCursorPosition).toHaveBeenCalledWith(7);
101
+ });
102
+
103
+ it("does not rewrite composer text on a second select after insertion", () => {
104
+ const textRef = { value: "@fil" };
105
+ const { aui, setText } = makeAui(textRef);
106
+ const onSelected = vi.fn();
107
+ const root = createTapRoot(function Root() {
108
+ const detection = useResource(
109
+ TriggerDetectionResource({
110
+ text: textRef.value,
111
+ triggerChar: "@",
112
+ }),
113
+ );
114
+ return useResource(
115
+ TriggerSelectionResource({
116
+ behavior: { kind: "directive", formatter },
117
+ trigger: detection.trigger,
118
+ aui,
119
+ triggerChar: "@",
120
+ setCursorPosition: detection.setCursorPosition,
121
+ onSelected,
122
+ }),
123
+ );
124
+ });
125
+
126
+ flushTapSync(() => {
127
+ root.getValue().selectItem(item);
128
+ });
129
+ root.getValue().selectItem(item);
130
+
131
+ expect(setText).toHaveBeenCalledOnce();
132
+ expect(setText).toHaveBeenCalledWith("@file.txt ");
133
+ expect(textRef.value).toBe("@file.txt ");
134
+ });
135
+ });
@@ -83,6 +83,7 @@ const useTriggerSelectionResource = ({
83
83
  aui.composer.setText(
84
84
  before + directive + (after.startsWith(" ") ? after : ` ${after}`),
85
85
  );
86
+ setCursorPosition(before.length + directive.length + 1);
86
87
  };
87
88
 
88
89
  if (behavior.kind === "directive") {
@@ -93,6 +94,7 @@ const useTriggerSelectionResource = ({
93
94
  aui.composer.setText(
94
95
  before + (after.startsWith(" ") ? after.slice(1) : after),
95
96
  );
97
+ setCursorPosition(before.length);
96
98
  } else {
97
99
  // Leave directive chip in the composer as an audit trail
98
100
  insertDirective();
@@ -24,6 +24,9 @@ export namespace ThreadPrimitiveRoot {
24
24
  * This component serves as the foundational wrapper for all thread-related components.
25
25
  * It provides the basic structure and context needed for thread functionality.
26
26
  *
27
+ * While this component is mounted, an unhandled Escape keydown stops active speech, even if
28
+ * the action bar that started it is no longer mounted.
29
+ *
27
30
  * @example
28
31
  * ```tsx
29
32
  * <ThreadPrimitive.Root>
@@ -229,6 +229,67 @@ describe("SandboxHost", () => {
229
229
  expect(onError.mock.calls[0]![0].message).toBe("boom");
230
230
  });
231
231
 
232
+ it("does not report render failures after unmount", async () => {
233
+ let rejectRender!: (error: Error) => void;
234
+ renderHtmlMock.mockReturnValue(
235
+ new Promise((_, reject) => {
236
+ rejectRender = reject;
237
+ }),
238
+ );
239
+ const onError = vi.fn();
240
+
241
+ await act(async () => {
242
+ root.render(
243
+ <SandboxHost
244
+ content={{ html: "" }}
245
+ contentKey="k"
246
+ createBridge={() => ({ onMessage: vi.fn(), dispose: vi.fn() })}
247
+ onError={onError}
248
+ />,
249
+ );
250
+ });
251
+
252
+ await act(async () => {
253
+ root.unmount();
254
+ });
255
+ rejectRender(new Error("late failure"));
256
+ await flush();
257
+
258
+ expect(onError).not.toHaveBeenCalled();
259
+ });
260
+
261
+ it("contains failures thrown by onError", async () => {
262
+ const renderError = new Error("render failed");
263
+ const callbackError = new Error("error callback failed");
264
+ renderHtmlMock.mockRejectedValue(renderError);
265
+ const consoleError = vi
266
+ .spyOn(console, "error")
267
+ .mockImplementation(() => {});
268
+
269
+ try {
270
+ await act(async () => {
271
+ root.render(
272
+ <SandboxHost
273
+ content={{ html: "" }}
274
+ contentKey="k"
275
+ createBridge={() => ({ onMessage: vi.fn(), dispose: vi.fn() })}
276
+ onError={() => {
277
+ throw callbackError;
278
+ }}
279
+ />,
280
+ );
281
+ });
282
+ await flush();
283
+
284
+ expect(consoleError).toHaveBeenCalledWith(
285
+ "[assistant-ui] SandboxHost onError callback threw an error",
286
+ callbackError,
287
+ );
288
+ } finally {
289
+ consoleError.mockRestore();
290
+ }
291
+ });
292
+
232
293
  it("disposes the rendered frame when bridge creation fails", async () => {
233
294
  const rendered = fakeRendered();
234
295
  renderHtmlMock.mockResolvedValue(rendered);
@@ -6,6 +6,7 @@ import {
6
6
  SafeContentFrame,
7
7
  type SandboxOption,
8
8
  } from "safe-content-frame";
9
+ import { invokeCallbackSafely } from "../utils/invokeCallbackSafely";
9
10
 
10
11
  const DEFAULT_PRODUCT = "assistant-ui-sandbox";
11
12
  const DEFAULT_MAX_HEIGHT = 800;
@@ -147,10 +148,13 @@ export function SandboxHost({
147
148
  window.addEventListener("message", onMessage);
148
149
  })
149
150
  .catch((err) => {
151
+ if (cancelled) return;
150
152
  frame?.dispose();
151
153
  frame = null;
152
- liveRef.current.onError?.(
153
- err instanceof Error ? err : new Error(String(err)),
154
+ const error = err instanceof Error ? err : new Error(String(err));
155
+ invokeCallbackSafely(
156
+ () => liveRef.current.onError?.(error),
157
+ "SandboxHost onError",
154
158
  );
155
159
  });
156
160
 
@@ -203,6 +203,63 @@ describe("unstable_useLiveCompletionAdapter", () => {
203
203
  ]);
204
204
  });
205
205
 
206
+ it("keeps cached results when only the fetcher identity changes", async () => {
207
+ const first = vi.fn(async () => [item("alice")]);
208
+ const second = vi.fn(async () => [item("bob")]);
209
+ const { result, rerender } = renderHook(
210
+ ({ fetcher }) =>
211
+ unstable_useLiveCompletionAdapter({
212
+ fetcher,
213
+ cacheKey: "workspace-a",
214
+ debounceMs: 0,
215
+ }),
216
+ { initialProps: { fetcher: first } },
217
+ );
218
+
219
+ await act(async () => {
220
+ result.current.adapter.search!("alice");
221
+ await vi.advanceTimersByTimeAsync(0);
222
+ });
223
+ expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
224
+
225
+ await act(async () => {
226
+ rerender({ fetcher: second });
227
+ await vi.advanceTimersByTimeAsync(0);
228
+ });
229
+ expect(second).not.toHaveBeenCalled();
230
+ expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
231
+ });
232
+
233
+ it("drops a pending result when only the cache key changes", async () => {
234
+ let resolveFirst!: (items: readonly Unstable_TriggerItem[]) => void;
235
+ const first = new Promise<readonly Unstable_TriggerItem[]>((resolve) => {
236
+ resolveFirst = resolve;
237
+ });
238
+ const fetcher = vi.fn(() => first);
239
+ const { result, rerender } = renderHook(
240
+ ({ cacheKey }) =>
241
+ unstable_useLiveCompletionAdapter({ fetcher, cacheKey, debounceMs: 0 }),
242
+ { initialProps: { cacheKey: "workspace-a" } },
243
+ );
244
+
245
+ await act(async () => {
246
+ result.current.adapter.search!("alice");
247
+ await vi.advanceTimersByTimeAsync(0);
248
+ });
249
+ expect(fetcher).toHaveBeenCalledTimes(1);
250
+
251
+ await act(async () => {
252
+ rerender({ cacheKey: "workspace-b" });
253
+ });
254
+ await act(async () => {
255
+ resolveFirst([item("workspace-a")]);
256
+ });
257
+
258
+ expect(result.current.isLoading).toBe(false);
259
+ expect(fetcher).toHaveBeenCalledTimes(1);
260
+ expect(result.current.adapter.search!("alice")).toEqual([]);
261
+ });
262
+
206
263
  it("allows a failed query to be retried", async () => {
207
264
  const fetcher = vi
208
265
  .fn<(query: string) => Promise<readonly Unstable_TriggerItem[]>>()
@@ -0,0 +1,11 @@
1
+ export const invokeCallbackSafely = (invoke: () => unknown, name: string) => {
2
+ const reportFailure = (error: unknown) => {
3
+ console.error(`[assistant-ui] ${name} callback threw an error`, error);
4
+ };
5
+
6
+ try {
7
+ void Promise.resolve(invoke()).catch(reportFailure);
8
+ } catch (error) {
9
+ reportFailure(error);
10
+ }
11
+ };
@@ -222,4 +222,114 @@ describe("useSmooth", () => {
222
222
  frame();
223
223
  expect(result.current.text).toBe("z");
224
224
  });
225
+
226
+ it("drains completed text when a queued frame never runs", () => {
227
+ vi.spyOn(globalThis, "requestAnimationFrame").mockReturnValue(1);
228
+ vi.spyOn(globalThis, "cancelAnimationFrame").mockImplementation(() => {});
229
+
230
+ const { result, rerender } = renderHook((state) => useSmooth(state, true), {
231
+ initialProps: runningState(""),
232
+ });
233
+
234
+ rerender(runningState("hello"));
235
+ rerender(textState("hello"));
236
+
237
+ expect(result.current.text).toBe("hello");
238
+ expect(result.current.status.type).toBe("complete");
239
+ });
240
+
241
+ it("keeps draining after the source settles when frames are flowing", () => {
242
+ const raf: FrameRequestCallback[] = [];
243
+ vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((cb) => {
244
+ raf.push(cb);
245
+ return raf.length;
246
+ });
247
+ vi.spyOn(globalThis, "cancelAnimationFrame").mockImplementation(() => {});
248
+ let now = 1000;
249
+ vi.spyOn(Date, "now").mockImplementation(() => now);
250
+
251
+ const frame = () => {
252
+ if (raf.length === 0) return;
253
+ const cb = raf.shift()!;
254
+ now += 16;
255
+ act(() => {
256
+ cb(now);
257
+ });
258
+ };
259
+
260
+ const { result, rerender } = renderHook(
261
+ (state) =>
262
+ useSmooth(state, {
263
+ maxCharsPerFrame: 1,
264
+ maxCharIntervalMs: 1,
265
+ drainMs: 250,
266
+ }),
267
+ { initialProps: runningState("hello") },
268
+ );
269
+
270
+ frame();
271
+ const mid = result.current.text;
272
+ expect(mid.length).toBeGreaterThan(0);
273
+ expect(mid.length).toBeLessThan("hello".length);
274
+ expect(result.current.status.type).toBe("running");
275
+
276
+ rerender(textState("hello"));
277
+ expect(result.current.text).toBe(mid);
278
+ expect(result.current.status.type).toBe("running");
279
+
280
+ frame();
281
+ expect(result.current.text.length).toBeGreaterThan(mid.length);
282
+
283
+ let guard = 0;
284
+ while (result.current.text !== "hello" && guard < 20) {
285
+ guard++;
286
+ frame();
287
+ }
288
+ expect(result.current.text).toBe("hello");
289
+ expect(result.current.status.type).toBe("complete");
290
+ });
291
+
292
+ it("snaps when a settled part replaces the text instead of looping the animator", () => {
293
+ const raf: FrameRequestCallback[] = [];
294
+ vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((cb) => {
295
+ raf.push(cb);
296
+ return raf.length;
297
+ });
298
+ vi.spyOn(globalThis, "cancelAnimationFrame").mockImplementation(() => {});
299
+ let now = 1000;
300
+ vi.spyOn(Date, "now").mockImplementation(() => now);
301
+
302
+ const frame = () => {
303
+ if (raf.length === 0) return;
304
+ const cb = raf.shift()!;
305
+ now += 16;
306
+ act(() => {
307
+ cb(now);
308
+ });
309
+ };
310
+
311
+ const { result, rerender } = renderHook(
312
+ (state) =>
313
+ useSmooth(state, {
314
+ maxCharsPerFrame: 1,
315
+ maxCharIntervalMs: 1,
316
+ drainMs: 250,
317
+ }),
318
+ { initialProps: runningState("hello world") },
319
+ );
320
+
321
+ frame();
322
+ expect(result.current.text.length).toBeGreaterThan(0);
323
+ const queued = raf.length;
324
+
325
+ rerender(textState("hi"));
326
+ expect(result.current.text).toBe("hi");
327
+ expect(result.current.status.type).toBe("complete");
328
+ expect(raf.length).toBeLessThanOrEqual(queued);
329
+
330
+ const after = raf.length;
331
+ frame();
332
+ expect(result.current.text).toBe("hi");
333
+ expect(raf.length).toBeLessThanOrEqual(after);
334
+ });
225
335
  });
@@ -137,6 +137,9 @@ const positiveOr = (value: number | undefined, fallback: number): number =>
137
137
  * `true` uses the default rate, and a {@link SmoothOptions} object tunes
138
138
  * the reveal. Returns the part state with `text` replaced by the revealed
139
139
  * prefix and `status` reporting `running` until the reveal catches up.
140
+ * If the source settles before any character has been revealed, the text
141
+ * is committed immediately so a missed animation frame cannot leave an
142
+ * empty bubble.
140
143
  *
141
144
  * The reveal auto-disables under `prefers-reduced-motion: reduce`,
142
145
  * committing the full text immediately; this takes precedence over an
@@ -229,13 +232,10 @@ export const useSmooth = (
229
232
  return;
230
233
  }
231
234
 
232
- // Discontinuity: part flipped, or new text breaks continuation
233
- // of the animator's current target. Either case requires
234
- // resetting the cursor — without the part check, a new part
235
- // whose text happens to share a prefix with the previous target
236
- // would keep the stale cursor and flicker.
237
235
  const partChanged = animatorPartRef.current !== part;
238
236
  animatorPartRef.current = part;
237
+ // A new part whose text shares a prefix with the previous target would
238
+ // keep the stale cursor and flicker without this reset.
239
239
  if (partChanged || !text.startsWith(animatorRef.targetText)) {
240
240
  if (state.status.type === "running") {
241
241
  animatorRef.currentText = "";
@@ -246,13 +246,27 @@ export const useSmooth = (
246
246
  animatorRef.currentText = text;
247
247
  animatorRef.targetText = text;
248
248
  animatorRef.stop();
249
+ setText(text);
249
250
  }
250
251
  return;
251
252
  }
252
253
 
253
254
  animatorRef.targetText = text;
255
+ if (state.status.type !== "running") {
256
+ // No character has painted. A pending frame that never runs would
257
+ // leave an empty bubble after settle.
258
+ if (animatorRef.currentText === "") {
259
+ animatorRef.currentText = text;
260
+ animatorRef.stop();
261
+ setText(text);
262
+ return;
263
+ }
264
+ animatorRef.start();
265
+ return;
266
+ }
267
+
254
268
  animatorRef.start();
255
- }, [animatorRef, enabled, text, state.status.type, part]);
269
+ }, [animatorRef, enabled, text, state.status.type, part, setText]);
256
270
 
257
271
  useEffect(() => {
258
272
  return () => {
@@ -1,5 +0,0 @@
1
- //#region src/context/react/utils/ensureBinding.d.ts
2
- declare const ensureBinding: (r: unknown) => void;
3
- //#endregion
4
- export { ensureBinding };
5
- //# sourceMappingURL=ensureBinding.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ensureBinding.d.ts","names":[],"sources":["../../../../src/context/react/utils/ensureBinding.ts"],"mappings":";cAmCa,gBAAiB"}
@@ -1,24 +0,0 @@
1
- //#region src/context/react/utils/ensureBinding.ts
2
- const debugVerifyPrototype = (runtime, prototype) => {
3
- const unboundMethods = Object.getOwnPropertyNames(prototype).filter((methodStr) => {
4
- const descriptor = Object.getOwnPropertyDescriptor(prototype, methodStr);
5
- const isMethod = descriptor && typeof descriptor.value === "function";
6
- if (!isMethod) return false;
7
- const methodName = methodStr;
8
- return isMethod && !methodName.startsWith("_") && methodName !== "constructor" && prototype[methodName] === runtime[methodName];
9
- });
10
- if (unboundMethods.length > 0) throw new Error(`The following methods are not bound: ${JSON.stringify(unboundMethods)}`);
11
- const prototypePrototype = Object.getPrototypeOf(prototype);
12
- if (prototypePrototype && prototypePrototype !== Object.prototype) debugVerifyPrototype(runtime, prototypePrototype);
13
- };
14
- const ensureBinding = (r) => {
15
- const runtime = r;
16
- if (runtime.__isBound) return;
17
- runtime.__internal_bindMethods?.();
18
- runtime.__isBound = true;
19
- if (process.env.NODE_ENV !== "production") debugVerifyPrototype(runtime, Object.getPrototypeOf(runtime));
20
- };
21
- //#endregion
22
- export { ensureBinding };
23
-
24
- //# sourceMappingURL=ensureBinding.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ensureBinding.js","names":["Bindable","__internal_bindMethods","__isBound","debugVerifyPrototype","runtime","Record","prototype","unboundMethods","Object","getOwnPropertyNames","filter","methodStr","descriptor","getOwnPropertyDescriptor","isMethod","value","methodName","startsWith","length","Error","JSON","stringify","prototypePrototype","getPrototypeOf","ensureBinding","r","process","env","NODE_ENV"],"sources":["../../../../src/context/react/utils/ensureBinding.ts"],"sourcesContent":["type Bindable = {\n __internal_bindMethods?: () => void;\n __isBound?: boolean;\n};\nconst debugVerifyPrototype = (\n runtime: Record<string, unknown>,\n prototype: any,\n) => {\n const unboundMethods = Object.getOwnPropertyNames(prototype).filter(\n (methodStr) => {\n const descriptor = Object.getOwnPropertyDescriptor(prototype, methodStr);\n const isMethod = descriptor && typeof descriptor.value === \"function\";\n if (!isMethod) return false;\n\n const methodName = methodStr as keyof typeof runtime | \"constructor\";\n return (\n isMethod &&\n !methodName.startsWith(\"_\") &&\n methodName !== \"constructor\" &&\n prototype[methodName] === runtime[methodName]\n );\n },\n );\n\n if (unboundMethods.length > 0) {\n throw new Error(\n `The following methods are not bound: ${JSON.stringify(unboundMethods)}`,\n );\n }\n\n const prototypePrototype = Object.getPrototypeOf(prototype);\n if (prototypePrototype && prototypePrototype !== Object.prototype) {\n debugVerifyPrototype(runtime, prototypePrototype);\n }\n};\nexport const ensureBinding = (r: unknown) => {\n const runtime = r as Bindable;\n if (runtime.__isBound) return;\n\n runtime.__internal_bindMethods?.();\n runtime.__isBound = true;\n\n if (process.env.NODE_ENV !== \"production\") {\n debugVerifyPrototype(runtime, Object.getPrototypeOf(runtime));\n }\n};\n"],"mappings":";AAIA,MAAMG,wBACJC,SACAE,cACG;CACH,MAAMC,iBAAiBC,OAAOC,oBAAoBH,SAAS,CAAC,CAACI,QAC1DC,cAAc;EACb,MAAMC,aAAaJ,OAAOK,yBAAyBP,WAAWK,SAAS;EACvE,MAAMG,WAAWF,cAAc,OAAOA,WAAWG,UAAU;EAC3D,IAAI,CAACD,UAAU,OAAO;EAEtB,MAAME,aAAaL;EACnB,OACEG,YACA,CAACE,WAAWC,WAAW,GAAG,KAC1BD,eAAe,iBACfV,UAAUU,gBAAgBZ,QAAQY;CAEtC,CACF;CAEA,IAAIT,eAAeW,SAAS,GAC1B,MAAM,IAAIC,MACR,wCAAwCC,KAAKC,UAAUd,cAAc,GACvE;CAGF,MAAMe,qBAAqBd,OAAOe,eAAejB,SAAS;CAC1D,IAAIgB,sBAAsBA,uBAAuBd,OAAOF,WACtDH,qBAAqBC,SAASkB,kBAAkB;AAEpD;AACA,MAAaE,iBAAiBC,MAAe;CAC3C,MAAMrB,UAAUqB;CAChB,IAAIrB,QAAQF,WAAW;CAEvBE,QAAQH,yBAAyB;CACjCG,QAAQF,YAAY;CAEpB,IAAIwB,QAAQC,IAAIC,aAAa,cAC3BzB,qBAAqBC,SAASI,OAAOe,eAAenB,OAAO,CAAC;AAEhE"}
@@ -1,13 +0,0 @@
1
- import { Unsubscribe } from "@assistant-ui/core";
2
- //#region src/context/react/utils/useRuntimeState.d.ts
3
- type SubscribableRuntime<TState> = {
4
- getState: () => TState;
5
- subscribe: (callback: () => void) => Unsubscribe;
6
- };
7
- declare function useRuntimeStateInternal<TState, TSelected>(runtime: SubscribableRuntime<TState>, selector?: ((state: TState) => TSelected | TState) | undefined): TSelected | TState;
8
- declare function useRuntimeState<TState>(runtime: SubscribableRuntime<TState>): TState;
9
- declare function useRuntimeState<TState, TSelected>(runtime: SubscribableRuntime<TState>, selector: (state: TState) => TSelected): TSelected;
10
- declare function useRuntimeState<TState, TSelected>(runtime: SubscribableRuntime<TState>, selector: ((state: TState) => TSelected) | undefined): TSelected | TState;
11
- //#endregion
12
- export { SubscribableRuntime, useRuntimeState, useRuntimeStateInternal };
13
- //# sourceMappingURL=useRuntimeState.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useRuntimeState.d.ts","names":[],"sources":["../../../../src/context/react/utils/useRuntimeState.ts"],"mappings":";;KAIY,oBAAoB;EAC9B,gBAAgB;EAChB,YAAY,yBAAyB;;iBAGvB,wBAAwB,QAAQ,WAC9C,SAAS,oBAAoB,SAC7B,aAAY,OAAO,WAAW,YAAY,sBACzC,YAAY;iBAcC,gBAAgB,QAC9B,SAAS,oBAAoB,UAC5B;iBACa,gBAAgB,QAAQ,WACtC,SAAS,oBAAoB,SAC7B,WAAW,OAAO,WAAW,YAC5B;iBACa,gBAAgB,QAAQ,WACtC,SAAS,oBAAoB,SAC7B,YAAY,OAAO,WAAW,yBAC7B,YAAY"}
@@ -1,33 +0,0 @@
1
- import { ensureBinding } from "./ensureBinding.js";
2
- import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
3
- import { useDebugValue, useSyncExternalStore } from "@assistant-ui/tap/react-shim";
4
- //#region src/context/react/utils/useRuntimeState.ts
5
- function useRuntimeStateInternal(runtime, t0) {
6
- const $ = c(4);
7
- const selector = t0 === void 0 ? identity : t0;
8
- ensureBinding(runtime);
9
- let t1;
10
- let t2;
11
- if ($[0] !== runtime || $[1] !== selector) {
12
- t1 = () => selector(runtime.getState());
13
- t2 = () => selector(runtime.getState());
14
- $[0] = runtime;
15
- $[1] = selector;
16
- $[2] = t1;
17
- $[3] = t2;
18
- } else {
19
- t1 = $[2];
20
- t2 = $[3];
21
- }
22
- const slice = useSyncExternalStore(runtime.subscribe, t1, t2);
23
- useDebugValue(slice);
24
- return slice;
25
- }
26
- const identity = (arg) => arg;
27
- function useRuntimeState(runtime, selector) {
28
- return useRuntimeStateInternal(runtime, selector);
29
- }
30
- //#endregion
31
- export { useRuntimeState, useRuntimeStateInternal };
32
-
33
- //# sourceMappingURL=useRuntimeState.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useRuntimeState.js","names":["Unsubscribe","useDebugValue","useSyncExternalStore","ensureBinding","SubscribableRuntime","getState","TState","subscribe","callback","useRuntimeStateInternal","runtime","t0","$","_c","selector","undefined","identity","t1","t2","slice","arg","T","useRuntimeState","state","TSelected"],"sources":["../../../../src/context/react/utils/useRuntimeState.ts"],"sourcesContent":["import type { Unsubscribe } from \"@assistant-ui/core\";\nimport { useDebugValue, useSyncExternalStore } from \"react\";\nimport { ensureBinding } from \"./ensureBinding\";\n\nexport type SubscribableRuntime<TState> = {\n getState: () => TState;\n subscribe: (callback: () => void) => Unsubscribe;\n};\n\nexport function useRuntimeStateInternal<TState, TSelected>(\n runtime: SubscribableRuntime<TState>,\n selector: ((state: TState) => TSelected | TState) | undefined = identity,\n): TSelected | TState {\n // TODO move to useRuntimeState\n ensureBinding(runtime);\n\n const slice = useSyncExternalStore(\n runtime.subscribe,\n () => selector(runtime.getState()),\n () => selector(runtime.getState()),\n );\n useDebugValue(slice);\n return slice;\n}\n\nconst identity = <T>(arg: T): T => arg;\nexport function useRuntimeState<TState>(\n runtime: SubscribableRuntime<TState>,\n): TState;\nexport function useRuntimeState<TState, TSelected>(\n runtime: SubscribableRuntime<TState>,\n selector: (state: TState) => TSelected,\n): TSelected;\nexport function useRuntimeState<TState, TSelected>(\n runtime: SubscribableRuntime<TState>,\n selector: ((state: TState) => TSelected) | undefined,\n): TSelected | TState;\nexport function useRuntimeState<TState, TSelected>(\n runtime: SubscribableRuntime<TState>,\n selector?: ((state: TState) => TSelected) | undefined,\n): TSelected | TState {\n // ensure that the runtime is bound\n // ensureBinding(runtime);\n\n return useRuntimeStateInternal(runtime, selector);\n}\n"],"mappings":";;;;AASA,SAAOS,wBAAAC,SAAAC,IAAA;CAAA,MAAAC,IAAAC,EAAA,CAAA;CAEL,MAAAC,WAAAH,OAAAI,KAAAA,IAAAC,WAAAL;CAGAR,cAAcO,OAAO;CAAC,IAAAO;CAAA,IAAAC;CAAA,IAAAN,EAAA,OAAAF,WAAAE,EAAA,OAAAE,UAAA;EAIpBG,WAAMH,SAASJ,QAAOL,SAAU,CAAC;EACjCa,WAAMJ,SAASJ,QAAOL,SAAU,CAAC;EAACO,EAAA,KAAAF;EAAAE,EAAA,KAAAE;EAAAF,EAAA,KAAAK;EAAAL,EAAA,KAAAM;CAAA,OAAA;EAAAD,KAAAL,EAAA;EAAAM,KAAAN,EAAA;CAAA;CAHpC,MAAAO,QAAcjB,qBACZQ,QAAOH,WACPU,IACAC,EACF;CACAjB,cAAckB,KAAK;CAAC,OACbA;AAAK;AAGd,MAAMH,YAAeI,QAAcA;AAYnC,SAAOE,gBAAAZ,SAAAI,UAAA;CAAA,OAOEL,wBAAwBC,SAASI,QAAQ;AAAC"}