@assistant-ui/core 0.2.7 → 0.2.9
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/dist/adapters/attachment.d.ts.map +1 -1
- package/dist/adapters/speech.d.ts.map +1 -1
- package/dist/adapters/speech.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/duplicate-detection.d.ts.map +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +2 -2
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/registry.d.ts.map +1 -1
- package/dist/model-context/tool.d.ts.map +1 -1
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +26 -15
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/index.d.ts +6 -3
- package/dist/react/index.js +4 -1
- package/dist/react/model-context/define-toolkit.d.ts +20 -0
- package/dist/react/model-context/define-toolkit.d.ts.map +1 -0
- package/dist/react/model-context/define-toolkit.js +21 -0
- package/dist/react/model-context/define-toolkit.js.map +1 -0
- package/dist/react/model-context/hitl.d.ts +19 -0
- package/dist/react/model-context/hitl.d.ts.map +1 -0
- package/dist/react/model-context/hitl.js +22 -0
- package/dist/react/model-context/hitl.js.map +1 -0
- package/dist/react/model-context/toolbox.d.ts +29 -2
- package/dist/react/model-context/toolbox.d.ts.map +1 -1
- package/dist/react/model-context/toolbox.js +18 -0
- package/dist/react/model-context/toolbox.js.map +1 -0
- package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantTool.js +6 -3
- package/dist/react/model-context/useAssistantTool.js.map +1 -1
- package/dist/react/model-context/useAssistantToolUI.d.ts +6 -0
- package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantToolUI.js +4 -2
- package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
- package/dist/react/model-context/useInlineRender.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +49 -7
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +28 -3
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +2 -7
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RuntimeAdapterProvider.d.ts.map +1 -1
- package/dist/react/runtimes/RuntimeAdapterProvider.js +6 -5
- package/dist/react/runtimes/RuntimeAdapterProvider.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -0
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts +7 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts.map +1 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.js +21 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -0
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/types/scopes/tools.d.ts +19 -2
- package/dist/react/types/scopes/tools.d.ts.map +1 -1
- package/dist/react/utils/groupParts.d.ts +32 -11
- package/dist/react/utils/groupParts.d.ts.map +1 -1
- package/dist/react/utils/groupParts.js +13 -6
- package/dist/react/utils/groupParts.js.map +1 -1
- package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/base/base-assistant-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts +9 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +34 -14
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts +1 -0
- package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +2 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-shared-options.d.ts +8 -0
- package/dist/runtimes/external-store/external-store-shared-options.d.ts.map +1 -0
- package/dist/runtimes/external-store/external-store-shared-options.js +11 -0
- package/dist/runtimes/external-store/external-store-shared-options.js.map +1 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +0 -2
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +12 -23
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/thread-message-converter.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
- package/dist/types/message.d.ts +6 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/composite-context-provider.d.ts.map +1 -1
- package/dist/utils/id.d.ts +1 -3
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +1 -4
- package/dist/utils/id.js.map +1 -1
- package/package.json +10 -10
- package/src/adapters/speech.ts +0 -1
- package/src/index.ts +2 -0
- package/src/internal.ts +0 -2
- package/src/model-context/frame/host.ts +0 -1
- package/src/model-context/frame/provider.ts +0 -1
- package/src/react/client/Interactables.ts +0 -1
- package/src/react/client/Tools.ts +50 -25
- package/src/react/index.ts +10 -2
- package/src/react/model-context/define-toolkit.test.ts +13 -0
- package/src/react/model-context/define-toolkit.ts +23 -0
- package/src/react/model-context/hitl.ts +22 -0
- package/src/react/model-context/toolbox.ts +46 -1
- package/src/react/model-context/useAssistantTool.ts +8 -3
- package/src/react/model-context/useAssistantToolUI.ts +9 -2
- package/src/react/model-context/useInlineRender.ts +0 -1
- package/src/react/primitives/message/MessageGroupedParts.tsx +101 -12
- package/src/react/primitives/message/MessageParts.tsx +4 -7
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +0 -3
- package/src/react/runtimes/RuntimeAdapterProvider.tsx +12 -7
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +0 -3
- package/src/react/runtimes/external-message-converter.ts +4 -0
- package/src/react/runtimes/useExternalStoreSharedOptions.ts +23 -0
- package/src/react/runtimes/useLocalRuntime.ts +0 -10
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +0 -6
- package/src/react/types/scopes/tools.ts +20 -1
- package/src/react/utils/groupParts.ts +49 -18
- package/src/runtime/utils/message-repository.ts +57 -16
- package/src/runtime/utils/thread-message-like.ts +2 -0
- package/src/runtimes/external-store/external-store-shared-options.ts +18 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +18 -33
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +0 -1
- package/src/tests/MessageRepository.test.ts +83 -52
- package/src/tests/OptimisticState-list-race.test.ts +0 -4
- package/src/tests/external-store-thread-runtime-core.test.ts +105 -73
- package/src/tests/groupParts.test.ts +70 -0
- package/src/tests/remote-thread-list-isLoading.test.ts +0 -5
- package/src/types/message.ts +6 -0
- package/src/utils/id.ts +0 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Toolkit, ToolkitDeclaration } from "./toolbox";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authoring helper for a `"use generative"` toolkit. Accepts the permissive
|
|
5
|
+
* {@link ToolkitDeclaration} (a `backend` tool may carry its server `execute`)
|
|
6
|
+
* and types the result as the canonical {@link Toolkit}.
|
|
7
|
+
*
|
|
8
|
+
* It has **no runtime implementation**. A `"use generative"` compiler (e.g.
|
|
9
|
+
* `@assistant-ui/next` or `@assistant-ui/vite`) strips the `defineToolkit(...)`
|
|
10
|
+
* wrapper (and its import) per build, so a correctly compiled
|
|
11
|
+
* `export default defineToolkit({...})` never calls this. If it *does* run, the
|
|
12
|
+
* module was not compiled by a use-generative loader — e.g. `defineToolkit` used
|
|
13
|
+
* outside a `"use generative"` file — which would ship a backend `execute` to the
|
|
14
|
+
* client. So it throws instead of silently leaking.
|
|
15
|
+
*/
|
|
16
|
+
export function defineToolkit(_declaration: ToolkitDeclaration): Toolkit {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"[assistant-ui] defineToolkit() has no runtime implementation — it is " +
|
|
19
|
+
"stripped at build time by the use-generative compiler. Reaching it means " +
|
|
20
|
+
'this module was not compiled (e.g. defineToolkit used outside a "use ' +
|
|
21
|
+
'generative" file). Add the directive, or do not use defineToolkit here.',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks a tool as **human-in-the-loop**: the agent pauses and the UI (`render`)
|
|
3
|
+
* supplies the result instead of code. Use it as the tool's `execute`:
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* confirm: { execute: hitl(), render: (props) => <Confirm {...props} /> }
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* Like {@link defineToolkit}, it has **no runtime implementation**: a
|
|
10
|
+
* `"use generative"` compiler (e.g. `@assistant-ui/next` or `@assistant-ui/vite`)
|
|
11
|
+
* detects `execute: hitl()`, drops it, and stamps the tool `type: "human"`.
|
|
12
|
+
* Reaching it at runtime means the module wasn't compiled (used outside a
|
|
13
|
+
* `"use generative"` file), so it throws.
|
|
14
|
+
*/
|
|
15
|
+
export function hitl(): never {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"[assistant-ui] hitl() has no runtime implementation — it marks a " +
|
|
18
|
+
"human-in-the-loop tool and is stripped at build time by the " +
|
|
19
|
+
"use-generative compiler. Reaching it means this module was not compiled " +
|
|
20
|
+
'(e.g. hitl() used outside a "use generative" file).',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import type { Tool } from "assistant-stream";
|
|
1
|
+
import type { Tool, ToolDeclaration } from "assistant-stream";
|
|
2
2
|
import type { ToolCallMessagePartComponent } from "../types/MessagePartComponentTypes";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Resolves whether a tool's UI should be presented standalone (outside the
|
|
6
|
+
* chain-of-thought grouping), applying the type-based defaults.
|
|
7
|
+
*
|
|
8
|
+
* An explicit `display` wins. Otherwise `human` tools default to standalone
|
|
9
|
+
* (they prompt the user), and every other tool defaults to inline (a trace of
|
|
10
|
+
* what the model is doing). MCP-app tool calls are detected separately from
|
|
11
|
+
* the part itself and are not resolved here.
|
|
12
|
+
*/
|
|
13
|
+
export const isStandaloneToolDisplay = (
|
|
14
|
+
tool: Pick<Tool<any, any>, "type" | "display">,
|
|
15
|
+
): boolean => {
|
|
16
|
+
if (tool.display !== undefined) return tool.display === "standalone";
|
|
17
|
+
return tool.type === "human";
|
|
18
|
+
};
|
|
19
|
+
|
|
4
20
|
type WithRender<T, TArgs extends Record<string, unknown>, TResult> = T extends {
|
|
5
21
|
type: "frontend" | "human";
|
|
6
22
|
}
|
|
@@ -43,6 +59,35 @@ export type ToolDefinition<
|
|
|
43
59
|
*/
|
|
44
60
|
export type Toolkit = Record<string, ToolDefinition<any, any>>;
|
|
45
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A tool as authored, before the build splits it: like {@link ToolDefinition}
|
|
64
|
+
* but it may declare `description`, `parameters`, and a server-side `execute`
|
|
65
|
+
* alongside its `render`. The `type` field is **not** authored — the
|
|
66
|
+
* `"use generative"` compiler infers it (`execute: hitl()` → human; `execute`
|
|
67
|
+
* with a `"use client"` directive → frontend; otherwise backend) and writes it
|
|
68
|
+
* back — so declaring it here is a type error.
|
|
69
|
+
*/
|
|
70
|
+
export type ToolkitDeclarationDefinition<
|
|
71
|
+
TArgs extends Record<string, unknown>,
|
|
72
|
+
TResult,
|
|
73
|
+
> = WithRender<
|
|
74
|
+
Omit<ToolDeclaration<TArgs, TResult>, "type">,
|
|
75
|
+
TArgs,
|
|
76
|
+
TResult
|
|
77
|
+
> & {
|
|
78
|
+
type?: never;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The permissive, authoring-time counterpart to {@link Toolkit} — the input to
|
|
83
|
+
* {@link defineToolkit}. Backend entries may carry their server `execute` here;
|
|
84
|
+
* the canonical {@link Toolkit} keeps those fields `undefined`.
|
|
85
|
+
*/
|
|
86
|
+
export type ToolkitDeclaration = Record<
|
|
87
|
+
string,
|
|
88
|
+
ToolkitDeclarationDefinition<any, any>
|
|
89
|
+
>;
|
|
90
|
+
|
|
46
91
|
/** Configuration for the {@link Tools} resource. */
|
|
47
92
|
export type ToolsConfig = {
|
|
48
93
|
/** Tools to register with model context and, when provided, message renderers. */
|
|
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|
|
2
2
|
import { useAui } from "@assistant-ui/store";
|
|
3
3
|
import type { ToolCallMessagePartComponent } from "../types/MessagePartComponentTypes";
|
|
4
4
|
import type { AssistantToolProps as CoreAssistantToolProps } from "../..";
|
|
5
|
+
import { isStandaloneToolDisplay } from "./toolbox";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Props used to register a tool from React.
|
|
@@ -52,13 +53,17 @@ export const useAssistantTool = <
|
|
|
52
53
|
) => {
|
|
53
54
|
const aui = useAui();
|
|
54
55
|
|
|
56
|
+
const standalone = isStandaloneToolDisplay(tool);
|
|
57
|
+
|
|
55
58
|
useEffect(() => {
|
|
56
59
|
if (!tool.render) return undefined;
|
|
57
|
-
return aui.tools().setToolUI(tool.toolName, tool.render);
|
|
58
|
-
}, [aui, tool.toolName, tool.render]);
|
|
60
|
+
return aui.tools().setToolUI(tool.toolName, tool.render, { standalone });
|
|
61
|
+
}, [aui, tool.toolName, tool.render, standalone]);
|
|
59
62
|
|
|
60
63
|
useEffect(() => {
|
|
61
|
-
|
|
64
|
+
// `render` and `display` are client-only presentation concerns and never
|
|
65
|
+
// reach the model.
|
|
66
|
+
const { toolName, render, display, ...rest } = tool;
|
|
62
67
|
const context = {
|
|
63
68
|
tools: {
|
|
64
69
|
[toolName]: rest,
|
|
@@ -8,6 +8,12 @@ export type AssistantToolUIProps<TArgs, TResult> = {
|
|
|
8
8
|
toolName: string;
|
|
9
9
|
/** Component rendered for matching tool-call message parts. */
|
|
10
10
|
render: ToolCallMessagePartComponent<TArgs, TResult>;
|
|
11
|
+
/**
|
|
12
|
+
* How the UI is presented relative to the chain-of-thought trace. Set
|
|
13
|
+
* `"standalone"` to surface it on its own (e.g. human-in-the-loop or
|
|
14
|
+
* generative UI for a backend/MCP tool). Defaults to `"inline"`.
|
|
15
|
+
*/
|
|
16
|
+
display?: "standalone" | "inline";
|
|
11
17
|
};
|
|
12
18
|
|
|
13
19
|
/**
|
|
@@ -23,8 +29,9 @@ export const useAssistantToolUI = (
|
|
|
23
29
|
tool: AssistantToolUIProps<any, any> | null,
|
|
24
30
|
) => {
|
|
25
31
|
const aui = useAui();
|
|
32
|
+
const standalone = tool?.display === "standalone";
|
|
26
33
|
useEffect(() => {
|
|
27
34
|
if (!tool?.toolName || !tool?.render) return undefined;
|
|
28
|
-
return aui.tools().setToolUI(tool.toolName, tool.render);
|
|
29
|
-
}, [aui, tool?.toolName, tool?.render]);
|
|
35
|
+
return aui.tools().setToolUI(tool.toolName, tool.render, { standalone });
|
|
36
|
+
}, [aui, tool?.toolName, tool?.render, standalone]);
|
|
30
37
|
};
|
|
@@ -17,7 +17,6 @@ export const useInlineRender = <TArgs, TResult>(
|
|
|
17
17
|
|
|
18
18
|
return useCallback(
|
|
19
19
|
function ToolUI(args) {
|
|
20
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
21
20
|
const store = useToolUIStore();
|
|
22
21
|
return store.toolUI(args);
|
|
23
22
|
},
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import {
|
|
12
12
|
buildGroupTree,
|
|
13
13
|
GROUPBY_MEMO_KEY,
|
|
14
|
+
type GroupByContext,
|
|
14
15
|
type GroupNode,
|
|
15
16
|
} from "../../utils/groupParts";
|
|
16
17
|
import { MessagePartChildren, type EnrichedPartState } from "./MessageParts";
|
|
@@ -28,13 +29,40 @@ export namespace MessagePrimitiveGroupedParts {
|
|
|
28
29
|
readonly indices: readonly number[];
|
|
29
30
|
};
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Synthetic trailing slot for a streaming/loading affordance (a
|
|
34
|
+
* "thinking…" dot, etc.). Surfaced through the same `{ part }` channel
|
|
35
|
+
* as groups and leaf parts so a single `switch (part.type)` renders it
|
|
36
|
+
* via `case "indicator"`.
|
|
37
|
+
*
|
|
38
|
+
* It is only ever emitted while the message is running, so its presence
|
|
39
|
+
* alone means "render your loading UI here" — there's no `status` to
|
|
40
|
+
* branch on.
|
|
41
|
+
*/
|
|
42
|
+
export type IndicatorPart = {
|
|
43
|
+
readonly type: "indicator";
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* When to emit the synthetic {@link IndicatorPart}. It is **only** emitted
|
|
48
|
+
* while the message is running (streaming); the mode further restricts
|
|
49
|
+
* which running states qualify:
|
|
50
|
+
* - `"never"` — never.
|
|
51
|
+
* - `"empty"` — only when the message has no parts yet.
|
|
52
|
+
* - `"no-text"` (default) — when the last part isn't `text`/`reasoning`
|
|
53
|
+
* (e.g. it ended on a tool call, so the assistant likely isn't done).
|
|
54
|
+
* - `"always"` — whenever the message is running, regardless of parts.
|
|
55
|
+
*/
|
|
56
|
+
export type IndicatorMode = "never" | "empty" | "no-text" | "always";
|
|
57
|
+
|
|
31
58
|
export type RenderInfo<TKey extends `group-${string}` = `group-${string}`> = {
|
|
32
59
|
/**
|
|
33
60
|
* Either a coalesced group ({@link GroupPart}, identified by a
|
|
34
|
-
* `group-…` `type`)
|
|
35
|
-
* `
|
|
61
|
+
* `group-…` `type`), a single enriched leaf part, or the synthetic
|
|
62
|
+
* {@link IndicatorPart} (`type: "indicator"`). Use one switch over
|
|
63
|
+
* `part.type` to handle all three.
|
|
36
64
|
*/
|
|
37
|
-
readonly part: GroupPart<TKey> | EnrichedPartState;
|
|
65
|
+
readonly part: GroupPart<TKey> | EnrichedPartState | IndicatorPart;
|
|
38
66
|
/**
|
|
39
67
|
* For group nodes: the recursively-rendered subtree (subgroups +
|
|
40
68
|
* leaf parts). For leaf parts: a sentinel that throws when rendered
|
|
@@ -59,6 +87,9 @@ export namespace MessagePrimitiveGroupedParts {
|
|
|
59
87
|
* the helper isn't expressive enough (e.g. branching on
|
|
60
88
|
* `part.toolName` or part metadata).
|
|
61
89
|
*
|
|
90
|
+
* The second argument is a {@link GroupByContext} carrying the tool-UI
|
|
91
|
+
* registry, for grouping that depends on it (e.g. standalone tool calls).
|
|
92
|
+
*
|
|
62
93
|
* @example
|
|
63
94
|
* ```tsx
|
|
64
95
|
* import { groupPartByType } from "@assistant-ui/react";
|
|
@@ -71,12 +102,27 @@ export namespace MessagePrimitiveGroupedParts {
|
|
|
71
102
|
* >
|
|
72
103
|
* ```
|
|
73
104
|
*/
|
|
74
|
-
readonly groupBy: (
|
|
105
|
+
readonly groupBy: (
|
|
106
|
+
part: PartState,
|
|
107
|
+
context: GroupByContext,
|
|
108
|
+
) => readonly TKey[] | null;
|
|
75
109
|
|
|
76
110
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
111
|
+
* Controls emission of the synthetic {@link IndicatorPart} — a
|
|
112
|
+
* trailing `{ part: { type: "indicator", status } }` render call you
|
|
113
|
+
* handle with `case "indicator"` to show loading/status UI.
|
|
114
|
+
*
|
|
115
|
+
* @default "no-text"
|
|
116
|
+
* @see IndicatorMode
|
|
117
|
+
*/
|
|
118
|
+
readonly indicator?: IndicatorMode;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Render function called once per group node, once per leaf part, and
|
|
122
|
+
* (when the `indicator` condition is met) once for the trailing
|
|
123
|
+
* {@link IndicatorPart}. Switch on `part.type`: `"group-…"` cases wrap
|
|
124
|
+
* `children`; real part types (`"text"`, `"tool-call"`, …) render the
|
|
125
|
+
* part directly; `"indicator"` renders status/loading UI.
|
|
80
126
|
*
|
|
81
127
|
* Leaf parts receive the same {@link EnrichedPartState} that
|
|
82
128
|
* `<MessagePrimitive.Parts>` would produce (`toolUI`, `addResult`,
|
|
@@ -88,6 +134,31 @@ export namespace MessagePrimitiveGroupedParts {
|
|
|
88
134
|
|
|
89
135
|
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({ type: "complete" });
|
|
90
136
|
|
|
137
|
+
const shouldShowIndicator = (
|
|
138
|
+
mode: MessagePrimitiveGroupedParts.IndicatorMode,
|
|
139
|
+
parts: readonly PartState[],
|
|
140
|
+
isRunning: boolean,
|
|
141
|
+
): boolean => {
|
|
142
|
+
// The indicator is a streaming affordance — never show it on a settled
|
|
143
|
+
// message, whatever the mode.
|
|
144
|
+
if (!isRunning) return false;
|
|
145
|
+
|
|
146
|
+
switch (mode) {
|
|
147
|
+
case "never":
|
|
148
|
+
return false;
|
|
149
|
+
case "always":
|
|
150
|
+
return true;
|
|
151
|
+
case "empty":
|
|
152
|
+
return parts.length === 0;
|
|
153
|
+
case "no-text": {
|
|
154
|
+
const last = parts[parts.length - 1];
|
|
155
|
+
return (
|
|
156
|
+
last !== undefined && last.type !== "text" && last.type !== "reasoning"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
91
162
|
/**
|
|
92
163
|
* `children` placeholder passed for leaf-part renders. Leaf parts have no
|
|
93
164
|
* inner subtree; rendering this sentinel signals the consumer wrote
|
|
@@ -161,6 +232,7 @@ const renderNode = <TKey extends `group-${string}`>(
|
|
|
161
232
|
* case "group-tool": return <ToolStack>{children}</ToolStack>;
|
|
162
233
|
* case "text": return <MarkdownText />;
|
|
163
234
|
* case "tool-call": return part.toolUI ?? <ToolFallback {...part} />;
|
|
235
|
+
* case "indicator": return <LoadingDots />;
|
|
164
236
|
* default: return null;
|
|
165
237
|
* }
|
|
166
238
|
* }}
|
|
@@ -169,9 +241,17 @@ const renderNode = <TKey extends `group-${string}`>(
|
|
|
169
241
|
*/
|
|
170
242
|
export const MessagePrimitiveGroupedParts = <TKey extends `group-${string}`>({
|
|
171
243
|
groupBy,
|
|
244
|
+
indicator = "no-text",
|
|
172
245
|
children,
|
|
173
246
|
}: MessagePrimitiveGroupedParts.Props<TKey>): ReactNode => {
|
|
174
247
|
const parts = useAuiState(useShallow((s) => s.message.parts));
|
|
248
|
+
// Handed to `groupBy` as its `context` argument (see GroupByContext).
|
|
249
|
+
const toolUIs = useAuiState((s) => s.tools.toolUIs);
|
|
250
|
+
// Subscribe to a boolean, not the status object: the tree only needs to
|
|
251
|
+
// re-render when running-ness flips, and `"never"` opts out entirely.
|
|
252
|
+
const isRunning = useAuiState((s) =>
|
|
253
|
+
indicator === "never" ? false : s.message.status?.type === "running",
|
|
254
|
+
);
|
|
175
255
|
|
|
176
256
|
// Helpers like `groupPartByType` tag the function with `GROUPBY_MEMO_KEY`
|
|
177
257
|
// (a stable string fingerprint of the helper config). When present,
|
|
@@ -182,13 +262,22 @@ export const MessagePrimitiveGroupedParts = <TKey extends `group-${string}`>({
|
|
|
182
262
|
GROUPBY_MEMO_KEY
|
|
183
263
|
];
|
|
184
264
|
const memoDep = memoKey ?? groupBy;
|
|
185
|
-
const tree = useMemo(
|
|
186
|
-
|
|
265
|
+
const tree = useMemo(() => {
|
|
266
|
+
const context: GroupByContext = { toolUIs };
|
|
267
|
+
return buildGroupTree(parts.map((part) => groupBy(part, context) ?? []));
|
|
187
268
|
// oxlint-disable-next-line tap-hooks/exhaustive-deps -- groupBy is captured via memoDep (either its identity or the helper's memoKey fingerprint); listing it directly would defeat the helper-tagged memo path
|
|
188
|
-
|
|
189
|
-
);
|
|
269
|
+
}, [parts, memoDep, toolUIs]);
|
|
190
270
|
|
|
191
|
-
return
|
|
271
|
+
return (
|
|
272
|
+
<>
|
|
273
|
+
{tree.map((node) => renderNode(node, parts, children))}
|
|
274
|
+
{shouldShowIndicator(indicator, parts, isRunning) &&
|
|
275
|
+
children({
|
|
276
|
+
part: { type: "indicator" },
|
|
277
|
+
children: <PartChildrenSentinel />,
|
|
278
|
+
})}
|
|
279
|
+
</>
|
|
280
|
+
);
|
|
192
281
|
};
|
|
193
282
|
|
|
194
283
|
MessagePrimitiveGroupedParts.displayName = "MessagePrimitive.GroupedParts";
|
|
@@ -308,11 +308,9 @@ const ToolUIDisplay = ({
|
|
|
308
308
|
}: {
|
|
309
309
|
Fallback: ToolCallMessagePartComponent | undefined;
|
|
310
310
|
} & ToolCallMessagePartProps) => {
|
|
311
|
-
const Render = useAuiState(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return Render;
|
|
315
|
-
});
|
|
311
|
+
const Render = useAuiState(
|
|
312
|
+
(s) => s.tools.toolUIs[props.toolName]?.[0]?.render ?? Fallback,
|
|
313
|
+
);
|
|
316
314
|
if (!Render) return null;
|
|
317
315
|
return <Render {...props} />;
|
|
318
316
|
};
|
|
@@ -574,8 +572,7 @@ function resolveToolRender(
|
|
|
574
572
|
toolsState: ToolsState,
|
|
575
573
|
part: Extract<PartState, { type: "tool-call" }>,
|
|
576
574
|
): ToolCallMessagePartComponent | null {
|
|
577
|
-
const
|
|
578
|
-
const named = Array.isArray(entry) ? (entry[0] ?? null) : (entry ?? null);
|
|
575
|
+
const named = toolsState.toolUIs[part.toolName]?.[0]?.render ?? null;
|
|
579
576
|
if (named) return named;
|
|
580
577
|
if (isMcpAppUri(part.mcp?.app?.resourceUri) && toolsState.mcpApp) {
|
|
581
578
|
return toolsState.mcpApp.render;
|
|
@@ -62,7 +62,6 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
62
62
|
isLoading: true,
|
|
63
63
|
};
|
|
64
64
|
},
|
|
65
|
-
// biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
|
|
66
65
|
then: (state, l) => {
|
|
67
66
|
if (generation !== this._loadGeneration) return state;
|
|
68
67
|
const fresh = classifyThreads(l.threads, {
|
|
@@ -120,7 +119,6 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
120
119
|
.optimisticUpdate({
|
|
121
120
|
execute: () => adapter.list({ after: cursor }),
|
|
122
121
|
loading: (state) => ({ ...state, isLoadingMore: true }),
|
|
123
|
-
// biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
|
|
124
122
|
then: (state, l) => {
|
|
125
123
|
if (generation !== this._loadGeneration) return state;
|
|
126
124
|
if (adapter !== this._options.adapter) return state;
|
|
@@ -423,7 +421,6 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
423
421
|
},
|
|
424
422
|
};
|
|
425
423
|
},
|
|
426
|
-
// biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
|
|
427
424
|
then: (state, { remoteId, externalId }) => {
|
|
428
425
|
const data = getThreadData(state, threadId);
|
|
429
426
|
if (!data) return state;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type FC,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useContext,
|
|
6
|
+
useMemo,
|
|
7
|
+
} from "react";
|
|
2
8
|
import type { ThreadHistoryAdapter } from "../../adapters/thread-history";
|
|
3
9
|
import type { AttachmentAdapter } from "../../adapters/attachment";
|
|
4
10
|
import type { ModelContextProvider } from "../../model-context/types";
|
|
@@ -23,13 +29,12 @@ export const RuntimeAdapterProvider: FC<RuntimeAdapterProvider.Props> = ({
|
|
|
23
29
|
children,
|
|
24
30
|
}) => {
|
|
25
31
|
const context = useContext(RuntimeAdaptersContext);
|
|
32
|
+
const value = useMemo(
|
|
33
|
+
() => ({ ...context, ...adapters }),
|
|
34
|
+
[context, adapters],
|
|
35
|
+
);
|
|
26
36
|
return (
|
|
27
|
-
<RuntimeAdaptersContext.Provider
|
|
28
|
-
value={{
|
|
29
|
-
...context,
|
|
30
|
-
...adapters,
|
|
31
|
-
}}
|
|
32
|
-
>
|
|
37
|
+
<RuntimeAdaptersContext.Provider value={value}>
|
|
33
38
|
{children}
|
|
34
39
|
</RuntimeAdaptersContext.Provider>
|
|
35
40
|
);
|
|
@@ -43,20 +43,17 @@ export const useCloudThreadListAdapter = (
|
|
|
43
43
|
|
|
44
44
|
const unstable_Provider = useCallback<FC<PropsWithChildren>>(
|
|
45
45
|
function Provider({ children }) {
|
|
46
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
47
46
|
const history = useAssistantCloudThreadHistoryAdapter({
|
|
48
47
|
get current() {
|
|
49
48
|
return adapterRef.current.cloud ?? autoCloud!;
|
|
50
49
|
},
|
|
51
50
|
});
|
|
52
51
|
const cloudInstance = adapterRef.current.cloud ?? autoCloud!;
|
|
53
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
54
52
|
const attachments = useMemo(
|
|
55
53
|
() => new CloudFileAttachmentAdapter(cloudInstance),
|
|
56
54
|
[cloudInstance],
|
|
57
55
|
);
|
|
58
56
|
|
|
59
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
60
57
|
const adapters = useMemo(
|
|
61
58
|
() => ({
|
|
62
59
|
history,
|
|
@@ -198,6 +198,10 @@ const joinExternalMessages = (
|
|
|
198
198
|
assistantMessage.metadata.submittedFeedback =
|
|
199
199
|
output.metadata.submittedFeedback;
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
if (output.metadata.isOptimistic) {
|
|
203
|
+
assistantMessage.metadata.isOptimistic = true;
|
|
204
|
+
}
|
|
201
205
|
}
|
|
202
206
|
// TODO keep this in sync
|
|
203
207
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type { ExternalStoreSharedOptions } from "../../runtimes/external-store/external-store-shared-options";
|
|
5
|
+
|
|
6
|
+
export const useExternalStoreSharedOptions = (
|
|
7
|
+
options: ExternalStoreSharedOptions,
|
|
8
|
+
): ExternalStoreSharedOptions => {
|
|
9
|
+
const { isDisabled, isSendDisabled, unstable_capabilities, suggestions } =
|
|
10
|
+
options;
|
|
11
|
+
return useMemo(
|
|
12
|
+
() =>
|
|
13
|
+
({
|
|
14
|
+
isDisabled,
|
|
15
|
+
isSendDisabled,
|
|
16
|
+
unstable_capabilities,
|
|
17
|
+
suggestions,
|
|
18
|
+
}) satisfies {
|
|
19
|
+
[K in keyof Required<ExternalStoreSharedOptions>]: ExternalStoreSharedOptions[K];
|
|
20
|
+
},
|
|
21
|
+
[isDisabled, isSendDisabled, unstable_capabilities, suggestions],
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -22,7 +22,6 @@ const useLocalThreadRuntime = (
|
|
|
22
22
|
chatModel: ChatModelAdapter,
|
|
23
23
|
{ initialMessages, ...options }: LocalRuntimeOptions,
|
|
24
24
|
): AssistantRuntime => {
|
|
25
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
26
25
|
const { modelContext, ...threadListAdapters } = useRuntimeAdapters() ?? {};
|
|
27
26
|
const opt = {
|
|
28
27
|
...options,
|
|
@@ -33,41 +32,33 @@ const useLocalThreadRuntime = (
|
|
|
33
32
|
},
|
|
34
33
|
};
|
|
35
34
|
|
|
36
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
37
35
|
const [runtime] = useState(() => new LocalRuntimeCore(opt, initialMessages));
|
|
38
36
|
|
|
39
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
40
37
|
const threadIdRef = useRef<string | undefined>(undefined);
|
|
41
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
42
38
|
threadIdRef.current = useAuiState((s) => s.threadListItem.remoteId);
|
|
43
39
|
|
|
44
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
45
40
|
useEffect(() => {
|
|
46
41
|
runtime.threads
|
|
47
42
|
.getMainThreadRuntimeCore()
|
|
48
43
|
.__internal_setGetThreadId(() => threadIdRef.current);
|
|
49
44
|
}, [runtime]);
|
|
50
45
|
|
|
51
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
52
46
|
useEffect(() => {
|
|
53
47
|
return () => {
|
|
54
48
|
runtime.threads.getMainThreadRuntimeCore().detach();
|
|
55
49
|
};
|
|
56
50
|
}, [runtime]);
|
|
57
51
|
|
|
58
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
59
52
|
useEffect(() => {
|
|
60
53
|
runtime.threads.getMainThreadRuntimeCore().__internal_setOptions(opt);
|
|
61
54
|
runtime.threads.getMainThreadRuntimeCore().__internal_load();
|
|
62
55
|
});
|
|
63
56
|
|
|
64
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
65
57
|
useEffect(() => {
|
|
66
58
|
if (!modelContext) return undefined;
|
|
67
59
|
return runtime.registerModelContextProvider(modelContext);
|
|
68
60
|
}, [modelContext, runtime]);
|
|
69
61
|
|
|
70
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
71
62
|
return useMemo(() => new AssistantRuntimeImpl(runtime), [runtime]);
|
|
72
63
|
};
|
|
73
64
|
|
|
@@ -102,7 +93,6 @@ export const useLocalRuntime = (
|
|
|
102
93
|
const cloudAdapter = useCloudThreadListAdapter({ cloud });
|
|
103
94
|
return useRemoteThreadListRuntime({
|
|
104
95
|
runtimeHook: function RuntimeHook() {
|
|
105
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
106
96
|
return useLocalThreadRuntime(chatModel, options);
|
|
107
97
|
},
|
|
108
98
|
adapter: cloudAdapter,
|
|
@@ -29,15 +29,12 @@ class RemoteThreadListRuntimeCore
|
|
|
29
29
|
const useRemoteThreadListRuntimeImpl = (
|
|
30
30
|
options: RemoteThreadListOptions,
|
|
31
31
|
): AssistantRuntime => {
|
|
32
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
33
32
|
const [runtime] = useState(() => new RemoteThreadListRuntimeCore(options));
|
|
34
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
35
33
|
useEffect(() => {
|
|
36
34
|
runtime.threads.__internal_setOptions(options);
|
|
37
35
|
runtime.threads.__internal_load();
|
|
38
36
|
}, [runtime, options]);
|
|
39
37
|
|
|
40
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
41
38
|
return useMemo(() => new AssistantRuntimeImpl(runtime), [runtime]);
|
|
42
39
|
};
|
|
43
40
|
|
|
@@ -80,12 +77,9 @@ export const useRemoteThreadListRuntime = (
|
|
|
80
77
|
return stableRuntimeHook();
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
84
80
|
const runtime = useRemoteThreadListRuntimeImpl(stableOptions);
|
|
85
81
|
|
|
86
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
87
82
|
const prevThreadIdRef = useRef(options.threadId);
|
|
88
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
89
83
|
useEffect(() => {
|
|
90
84
|
if (options.threadId === prevThreadIdRef.current) return;
|
|
91
85
|
prevThreadIdRef.current = options.threadId;
|
|
@@ -5,9 +5,27 @@ export type McpAppResourceOutput = {
|
|
|
5
5
|
readonly render: ToolCallMessagePartComponent;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A single tool-UI registration: the renderer plus its presentation options.
|
|
10
|
+
* Stored as a per-tool-name list so the name stays registered while any
|
|
11
|
+
* registration of it is mounted.
|
|
12
|
+
*/
|
|
13
|
+
type ToolRegistration = {
|
|
14
|
+
readonly render: ToolCallMessagePartComponent;
|
|
15
|
+
/** Whether this UI renders standalone, outside the chain-of-thought trace. */
|
|
16
|
+
readonly standalone: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
8
19
|
export type ToolsState = {
|
|
9
|
-
|
|
20
|
+
/** Registered tool UIs (renderer + presentation options) keyed by tool name. */
|
|
21
|
+
toolUIs: Record<string, readonly ToolRegistration[]>;
|
|
10
22
|
mcpApp?: McpAppResourceOutput | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Use {@link toolUIs} instead, whose entries carry the renderer
|
|
25
|
+
* alongside its presentation options. This component-only map is kept for
|
|
26
|
+
* back-compat and will be removed in v0.15.
|
|
27
|
+
*/
|
|
28
|
+
tools: Record<string, ToolCallMessagePartComponent[]>;
|
|
11
29
|
};
|
|
12
30
|
|
|
13
31
|
export type ToolsMethods = {
|
|
@@ -15,6 +33,7 @@ export type ToolsMethods = {
|
|
|
15
33
|
setToolUI(
|
|
16
34
|
toolName: string,
|
|
17
35
|
render: ToolCallMessagePartComponent,
|
|
36
|
+
options?: { standalone?: boolean },
|
|
18
37
|
): Unsubscribe;
|
|
19
38
|
};
|
|
20
39
|
|