@assistant-ui/core 0.3.2 → 0.3.3
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/index.d.ts +4 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +6 -6
- package/dist/internal.js +5 -5
- package/dist/react/primitive-hooks/useActionBarCopy.js +51 -24
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +2 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +24 -2
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +24 -5
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +5 -1
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/internal.d.ts +4 -4
- package/dist/runtime/internal.js +4 -4
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.js +29 -23
- package/dist/store/clients/chain-of-thought-client.js.map +1 -1
- package/dist/store/clients/thread-message-client.d.ts.map +1 -1
- package/dist/store/clients/thread-message-client.js +101 -89
- package/dist/store/clients/thread-message-client.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/message.d.ts +11 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/getGroupStatus.d.ts +9 -0
- package/dist/utils/getGroupStatus.d.ts.map +1 -0
- package/dist/utils/getGroupStatus.js +15 -0
- package/dist/utils/getGroupStatus.js.map +1 -0
- package/dist/utils/normalizePartStatus.d.ts +8 -0
- package/dist/utils/normalizePartStatus.d.ts.map +1 -0
- package/dist/utils/normalizePartStatus.js +39 -0
- package/dist/utils/normalizePartStatus.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/internal.ts +1 -0
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +109 -14
- package/src/react/primitive-hooks/useActionBarCopy.ts +27 -2
- package/src/react/primitives/message/MessageGroupedParts.tsx +4 -4
- package/src/react/runtimes/cloud/auiV0.ts +51 -12
- package/src/runtime/api/message-runtime.test.ts +140 -0
- package/src/runtime/api/message-runtime.ts +10 -5
- package/src/store/clients/chain-of-thought-client.ts +2 -7
- package/src/store/clients/thread-message-client.test.ts +84 -0
- package/src/store/clients/thread-message-client.ts +14 -4
- package/src/tests/auiV0Encode.test.ts +93 -0
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +19 -0
- package/src/utils/getGroupStatus.test.ts +36 -0
- package/src/utils/getGroupStatus.ts +31 -0
- package/src/utils/normalizePartStatus.test.ts +59 -0
- package/src/utils/normalizePartStatus.ts +55 -0
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
DataMessagePart,
|
|
3
|
-
FileMessagePart,
|
|
4
|
-
ImageMessagePart,
|
|
5
2
|
MessageStatus,
|
|
6
3
|
SourceProviderMetadata,
|
|
7
4
|
ThreadMessage,
|
|
8
|
-
TextMessagePart,
|
|
9
5
|
ToolApprovalOption,
|
|
10
|
-
Unstable_AudioMessagePart,
|
|
11
6
|
} from "../../../types/message";
|
|
12
7
|
import type { CompleteAttachment } from "../../../types/attachment";
|
|
13
8
|
import { fromThreadMessageLike } from "../../../runtime/utils/thread-message-like";
|
|
@@ -85,11 +80,33 @@ type AuiV0MessagePart =
|
|
|
85
80
|
};
|
|
86
81
|
|
|
87
82
|
type AuiV0AttachmentPart =
|
|
88
|
-
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
|
83
|
+
| {
|
|
84
|
+
readonly type: "text";
|
|
85
|
+
readonly text: string;
|
|
86
|
+
}
|
|
87
|
+
| {
|
|
88
|
+
readonly type: "image";
|
|
89
|
+
readonly image: string;
|
|
90
|
+
readonly filename?: string;
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
readonly type: "file";
|
|
94
|
+
readonly data: string;
|
|
95
|
+
readonly mimeType: string;
|
|
96
|
+
readonly filename?: string;
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
readonly type: "audio";
|
|
100
|
+
readonly audio: {
|
|
101
|
+
readonly data: string;
|
|
102
|
+
readonly format: "mp3" | "wav";
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
| {
|
|
106
|
+
readonly type: "data";
|
|
107
|
+
readonly name: string;
|
|
108
|
+
readonly data: ReadonlyJSONValue;
|
|
109
|
+
};
|
|
93
110
|
|
|
94
111
|
type AuiV0Attachment = {
|
|
95
112
|
readonly id: string;
|
|
@@ -125,16 +142,38 @@ const encodeAttachmentPart = (
|
|
|
125
142
|
const type = part.type;
|
|
126
143
|
switch (type) {
|
|
127
144
|
case "text":
|
|
145
|
+
return { type: "text", text: part.text };
|
|
146
|
+
|
|
128
147
|
case "image":
|
|
148
|
+
return {
|
|
149
|
+
type: "image",
|
|
150
|
+
image: part.image,
|
|
151
|
+
...(part.filename != null ? { filename: part.filename } : undefined),
|
|
152
|
+
};
|
|
153
|
+
|
|
129
154
|
case "file":
|
|
155
|
+
return {
|
|
156
|
+
type: "file",
|
|
157
|
+
data: part.data,
|
|
158
|
+
mimeType: part.mimeType,
|
|
159
|
+
...(part.filename != null ? { filename: part.filename } : undefined),
|
|
160
|
+
};
|
|
161
|
+
|
|
130
162
|
case "audio":
|
|
131
|
-
return
|
|
163
|
+
return {
|
|
164
|
+
type: "audio",
|
|
165
|
+
audio: { data: part.audio.data, format: part.audio.format },
|
|
166
|
+
};
|
|
132
167
|
|
|
133
168
|
case "data": {
|
|
134
169
|
if (!isJSONValue(part.data)) {
|
|
135
170
|
console.warn(`attachment data is not JSON! ${JSON.stringify(part)}`);
|
|
136
171
|
}
|
|
137
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
type: "data",
|
|
174
|
+
name: part.name,
|
|
175
|
+
data: part.data as ReadonlyJSONValue,
|
|
176
|
+
};
|
|
138
177
|
}
|
|
139
178
|
|
|
140
179
|
default: {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { CompleteAttachment } from "../../types/attachment";
|
|
3
|
+
import type { ThreadAssistantMessage } from "../../types/message";
|
|
3
4
|
import type { ThreadRuntimeCoreBinding } from "./thread-runtime";
|
|
4
5
|
import {
|
|
5
6
|
MessageRuntimeImpl,
|
|
7
|
+
toMessagePartStatus,
|
|
6
8
|
type MessageState,
|
|
7
9
|
type MessageStateBinding,
|
|
8
10
|
} from "./message-runtime";
|
|
@@ -63,6 +65,144 @@ const threadBinding = {
|
|
|
63
65
|
subscribe: () => () => {},
|
|
64
66
|
} as unknown as ThreadRuntimeCoreBinding;
|
|
65
67
|
|
|
68
|
+
const createAssistantMessage = (
|
|
69
|
+
content: ThreadAssistantMessage["content"],
|
|
70
|
+
status: ThreadAssistantMessage["status"],
|
|
71
|
+
): ThreadAssistantMessage => ({
|
|
72
|
+
id: "message-1",
|
|
73
|
+
role: "assistant",
|
|
74
|
+
createdAt: new Date(0),
|
|
75
|
+
content,
|
|
76
|
+
status,
|
|
77
|
+
metadata: {
|
|
78
|
+
unstable_state: null,
|
|
79
|
+
unstable_annotations: [],
|
|
80
|
+
unstable_data: [],
|
|
81
|
+
steps: [],
|
|
82
|
+
custom: {},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("toMessagePartStatus", () => {
|
|
87
|
+
it("honours a supplied running status on a non-last part", () => {
|
|
88
|
+
const message = createAssistantMessage(
|
|
89
|
+
[
|
|
90
|
+
{ type: "text", text: "first", status: { type: "running" } },
|
|
91
|
+
{ type: "text", text: "last" },
|
|
92
|
+
],
|
|
93
|
+
{ type: "running" },
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
|
|
97
|
+
type: "running",
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("honours a supplied complete status on the last part", () => {
|
|
102
|
+
const message = createAssistantMessage(
|
|
103
|
+
[{ type: "reasoning", text: "done", status: { type: "complete" } }],
|
|
104
|
+
{ type: "running" },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
|
|
108
|
+
type: "complete",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("ignores supplied statuses after the message completes", () => {
|
|
113
|
+
const message = createAssistantMessage(
|
|
114
|
+
[{ type: "text", text: "truncated", status: { type: "running" } }],
|
|
115
|
+
{ type: "complete", reason: "stop" },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
|
|
119
|
+
type: "complete",
|
|
120
|
+
reason: "stop",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("falls back to positional statuses for statusless running parts", () => {
|
|
125
|
+
const message = createAssistantMessage(
|
|
126
|
+
[
|
|
127
|
+
{ type: "text", text: "first" },
|
|
128
|
+
{ type: "reasoning", text: "last" },
|
|
129
|
+
],
|
|
130
|
+
{ type: "running" },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
|
|
134
|
+
type: "complete",
|
|
135
|
+
});
|
|
136
|
+
expect(toMessagePartStatus(message, 1, message.content[1]!)).toEqual({
|
|
137
|
+
type: "running",
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("preserves tool-call status derivation", () => {
|
|
142
|
+
const unresolved = createAssistantMessage(
|
|
143
|
+
[
|
|
144
|
+
{
|
|
145
|
+
type: "tool-call",
|
|
146
|
+
toolCallId: "call-1",
|
|
147
|
+
toolName: "weather",
|
|
148
|
+
args: {},
|
|
149
|
+
argsText: "{}",
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
{ type: "running" },
|
|
153
|
+
);
|
|
154
|
+
const resolved = createAssistantMessage(
|
|
155
|
+
[
|
|
156
|
+
{
|
|
157
|
+
type: "tool-call",
|
|
158
|
+
toolCallId: "call-1",
|
|
159
|
+
toolName: "weather",
|
|
160
|
+
args: {},
|
|
161
|
+
argsText: "{}",
|
|
162
|
+
result: "sunny",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
{ type: "running" },
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(toMessagePartStatus(unresolved, 0, unresolved.content[0]!)).toEqual({
|
|
169
|
+
type: "running",
|
|
170
|
+
});
|
|
171
|
+
expect(toMessagePartStatus(resolved, 0, resolved.content[0]!)).toEqual({
|
|
172
|
+
type: "complete",
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("normalizes supplied upstream statuses", () => {
|
|
177
|
+
const upstreamComplete = {
|
|
178
|
+
type: "text",
|
|
179
|
+
text: "done",
|
|
180
|
+
status: { type: "complete", reason: "unknown" },
|
|
181
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
182
|
+
const upstreamIncomplete = {
|
|
183
|
+
type: "reasoning",
|
|
184
|
+
text: "interrupted",
|
|
185
|
+
status: {
|
|
186
|
+
type: "incomplete",
|
|
187
|
+
reason: "unknown",
|
|
188
|
+
error: "upstream error",
|
|
189
|
+
},
|
|
190
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
191
|
+
const message = createAssistantMessage(
|
|
192
|
+
[upstreamComplete, upstreamIncomplete],
|
|
193
|
+
{ type: "running" },
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
|
|
197
|
+
type: "complete",
|
|
198
|
+
});
|
|
199
|
+
expect(toMessagePartStatus(message, 1, message.content[1]!)).toEqual({
|
|
200
|
+
type: "incomplete",
|
|
201
|
+
reason: "other",
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
66
206
|
describe("MessageRuntimeImpl paths", () => {
|
|
67
207
|
it("appends nested selectors to the message path", () => {
|
|
68
208
|
const runtime = new MessageRuntimeImpl(messageBinding, threadBinding);
|
|
@@ -8,6 +8,10 @@ import type {
|
|
|
8
8
|
} from "../../types/message";
|
|
9
9
|
import type { Unsubscribe } from "../../types/unsubscribe";
|
|
10
10
|
import type { MessagePartStatus, RunConfig } from "../../types/message";
|
|
11
|
+
import {
|
|
12
|
+
COMPLETE_STATUS,
|
|
13
|
+
normalizePartStatus,
|
|
14
|
+
} from "../../utils/normalizePartStatus";
|
|
11
15
|
import { getThreadMessageText } from "../../utils/text";
|
|
12
16
|
import { NestedSubscriptionSubject } from "../../subscribable/subscribable";
|
|
13
17
|
import {
|
|
@@ -32,10 +36,6 @@ import type { MessageRuntimePath } from "./paths";
|
|
|
32
36
|
import type { ThreadRuntimeCoreBinding } from "./thread-runtime";
|
|
33
37
|
import type { MessageStateBinding } from "./bindings";
|
|
34
38
|
|
|
35
|
-
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
|
|
36
|
-
type: "complete",
|
|
37
|
-
});
|
|
38
|
-
|
|
39
39
|
export const toMessagePartStatus = (
|
|
40
40
|
message: ThreadMessage,
|
|
41
41
|
partIndex: number,
|
|
@@ -51,6 +51,11 @@ export const toMessagePartStatus = (
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
if (message.status.type === "running") {
|
|
55
|
+
const status = normalizePartStatus(part);
|
|
56
|
+
if (status) return status;
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
const isLastPart = partIndex === Math.max(0, message.content.length - 1);
|
|
55
60
|
if (message.status.type === "requires-action") return COMPLETE_STATUS;
|
|
56
61
|
return isLastPart ? (message.status as MessagePartStatus) : COMPLETE_STATUS;
|
|
@@ -70,7 +75,7 @@ const getMessagePartState = (
|
|
|
70
75
|
return Object.freeze({
|
|
71
76
|
...part,
|
|
72
77
|
...{ [symbolInnerMessage]: (part as any)[symbolInnerMessage] },
|
|
73
|
-
status,
|
|
78
|
+
status: status as MessagePartStatus,
|
|
74
79
|
});
|
|
75
80
|
};
|
|
76
81
|
|
|
@@ -5,12 +5,8 @@ import type {
|
|
|
5
5
|
ChainOfThoughtState,
|
|
6
6
|
ChainOfThoughtPart,
|
|
7
7
|
} from "../scopes/chain-of-thought";
|
|
8
|
-
import type { MessagePartStatus } from "../../types/message";
|
|
9
8
|
import type { PartMethods } from "../scopes/part";
|
|
10
|
-
|
|
11
|
-
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
|
|
12
|
-
type: "complete",
|
|
13
|
-
});
|
|
9
|
+
import { getGroupStatus } from "../../utils/getGroupStatus";
|
|
14
10
|
|
|
15
11
|
const useChainOfThoughtClient = ({
|
|
16
12
|
parts,
|
|
@@ -22,8 +18,7 @@ const useChainOfThoughtClient = ({
|
|
|
22
18
|
const [collapsed, setCollapsed] = useState(true);
|
|
23
19
|
|
|
24
20
|
const status = useMemo(() => {
|
|
25
|
-
|
|
26
|
-
return lastPart?.status ?? COMPLETE_STATUS;
|
|
21
|
+
return getGroupStatus(parts);
|
|
27
22
|
}, [parts]);
|
|
28
23
|
|
|
29
24
|
const state = useMemo<ChainOfThoughtState>(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createTapRoot, useResource } from "@assistant-ui/tap";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import type { ThreadAssistantMessage } from "../../types/message";
|
|
4
|
+
import { ThreadMessageClient } from "./thread-message-client";
|
|
5
|
+
|
|
6
|
+
describe("ThreadMessageClient", () => {
|
|
7
|
+
const getPartStatus = (
|
|
8
|
+
part: ThreadAssistantMessage["content"][number],
|
|
9
|
+
status: ThreadAssistantMessage["status"],
|
|
10
|
+
) => {
|
|
11
|
+
const message: ThreadAssistantMessage = {
|
|
12
|
+
id: "message-1",
|
|
13
|
+
role: "assistant",
|
|
14
|
+
createdAt: new Date(0),
|
|
15
|
+
content: [part],
|
|
16
|
+
status,
|
|
17
|
+
metadata: {
|
|
18
|
+
unstable_state: null,
|
|
19
|
+
unstable_annotations: [],
|
|
20
|
+
unstable_data: [],
|
|
21
|
+
steps: [],
|
|
22
|
+
custom: {},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const root = createTapRoot(function ThreadMessageRoot() {
|
|
26
|
+
return useResource(ThreadMessageClient({ message, index: 0 }));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return root.getValue().getState().parts[0]?.status;
|
|
31
|
+
} finally {
|
|
32
|
+
root.unmount();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
it("preserves a running part status on a running detached message", () => {
|
|
37
|
+
const part = {
|
|
38
|
+
type: "text",
|
|
39
|
+
text: "done",
|
|
40
|
+
status: { type: "running" },
|
|
41
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
42
|
+
|
|
43
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
44
|
+
type: "running",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("normalizes an unknown incomplete reason on a running detached message", () => {
|
|
49
|
+
const part = {
|
|
50
|
+
type: "text",
|
|
51
|
+
text: "done",
|
|
52
|
+
status: { type: "incomplete", reason: "unknown" },
|
|
53
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
54
|
+
|
|
55
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
56
|
+
type: "incomplete",
|
|
57
|
+
reason: "other",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("normalizes an upstream complete reason on a running detached message", () => {
|
|
62
|
+
const part = {
|
|
63
|
+
type: "text",
|
|
64
|
+
text: "done",
|
|
65
|
+
status: { type: "complete", reason: "unknown" },
|
|
66
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
67
|
+
|
|
68
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
69
|
+
type: "complete",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("marks parts complete on a non-running detached message", () => {
|
|
74
|
+
const part = {
|
|
75
|
+
type: "text",
|
|
76
|
+
text: "done",
|
|
77
|
+
status: { type: "running" },
|
|
78
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
79
|
+
|
|
80
|
+
expect(getPartStatus(part, { type: "complete", reason: "stop" })).toEqual({
|
|
81
|
+
type: "complete",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -11,19 +11,27 @@ import { useClientLookup } from "@assistant-ui/store";
|
|
|
11
11
|
import type { MessageState } from "../scopes/message";
|
|
12
12
|
import type { PartState } from "../scopes/part";
|
|
13
13
|
import { NoOpComposerClient } from "./no-op-composer-client";
|
|
14
|
+
import {
|
|
15
|
+
COMPLETE_STATUS,
|
|
16
|
+
normalizePartStatus,
|
|
17
|
+
} from "../../utils/normalizePartStatus";
|
|
14
18
|
import { getThreadMessageText } from "../../utils/text";
|
|
15
19
|
|
|
16
20
|
const useThreadMessagePartClient = ({
|
|
17
21
|
part,
|
|
22
|
+
isMessageRunning,
|
|
18
23
|
}: {
|
|
19
24
|
part: ThreadAssistantMessagePart | ThreadUserMessagePart;
|
|
25
|
+
isMessageRunning: boolean;
|
|
20
26
|
}): ClientOutput<"part"> => {
|
|
21
27
|
const state = useMemo<PartState>(() => {
|
|
22
28
|
return {
|
|
23
29
|
...part,
|
|
24
|
-
status:
|
|
30
|
+
status: isMessageRunning
|
|
31
|
+
? (normalizePartStatus(part) ?? COMPLETE_STATUS)
|
|
32
|
+
: COMPLETE_STATUS,
|
|
25
33
|
};
|
|
26
|
-
}, [part]);
|
|
34
|
+
}, [part, isMessageRunning]);
|
|
27
35
|
|
|
28
36
|
return {
|
|
29
37
|
getState: () => state,
|
|
@@ -74,6 +82,8 @@ const useThreadMessageClient = ({
|
|
|
74
82
|
}: ThreadMessageClientProps): ClientOutput<"message"> => {
|
|
75
83
|
const [isCopiedState, setIsCopied] = useState(false);
|
|
76
84
|
const [isHoveringState, setIsHovering] = useState(false);
|
|
85
|
+
const isMessageRunning =
|
|
86
|
+
message.role === "assistant" && message.status.type === "running";
|
|
77
87
|
|
|
78
88
|
const parts = useClientLookup(
|
|
79
89
|
message.content.map((part, idx) =>
|
|
@@ -81,8 +91,8 @@ const useThreadMessageClient = ({
|
|
|
81
91
|
"toolCallId" in part && part.toolCallId != null
|
|
82
92
|
? `toolCallId-${part.toolCallId}`
|
|
83
93
|
: `index-${idx}`,
|
|
84
|
-
ThreadMessagePartClient({ part }),
|
|
85
|
-
[part],
|
|
94
|
+
ThreadMessagePartClient({ part, isMessageRunning }),
|
|
95
|
+
[part, isMessageRunning],
|
|
86
96
|
),
|
|
87
97
|
),
|
|
88
98
|
);
|
|
@@ -155,6 +155,99 @@ describe("auiV0Encode", () => {
|
|
|
155
155
|
},
|
|
156
156
|
]);
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
it("drops per-part status from message parts in the core cloud encoder", () => {
|
|
160
|
+
const encoded = auiV0Encode({
|
|
161
|
+
id: "m1",
|
|
162
|
+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
|
|
163
|
+
role: "assistant",
|
|
164
|
+
status: { type: "complete", reason: "stop" },
|
|
165
|
+
metadata: {
|
|
166
|
+
unstable_state: null,
|
|
167
|
+
unstable_annotations: [],
|
|
168
|
+
unstable_data: [],
|
|
169
|
+
steps: [],
|
|
170
|
+
custom: {},
|
|
171
|
+
},
|
|
172
|
+
content: [
|
|
173
|
+
{ type: "reasoning", text: "thinking", status: { type: "complete" } },
|
|
174
|
+
{ type: "text", text: "answer", status: { type: "running" } },
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(encoded.content).toEqual([
|
|
179
|
+
{ type: "reasoning", text: "thinking" },
|
|
180
|
+
{ type: "text", text: "answer" },
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("drops per-part status from attachment content in the core cloud encoder", () => {
|
|
185
|
+
const encoded = auiV0Encode({
|
|
186
|
+
id: "m1",
|
|
187
|
+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
|
|
188
|
+
role: "user",
|
|
189
|
+
metadata: { custom: {} },
|
|
190
|
+
content: [{ type: "text", text: "please review this" }],
|
|
191
|
+
attachments: [
|
|
192
|
+
{
|
|
193
|
+
id: "att-1",
|
|
194
|
+
type: "document",
|
|
195
|
+
name: "notes.txt",
|
|
196
|
+
status: { type: "complete" },
|
|
197
|
+
content: [
|
|
198
|
+
{ type: "text", text: "notes", status: { type: "running" } },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(encoded.attachments?.[0]?.content).toEqual([
|
|
205
|
+
{ type: "text", text: "notes" },
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("preserves every attachment content field the wire shape carries in the core cloud encoder", () => {
|
|
210
|
+
const encoded = auiV0Encode({
|
|
211
|
+
id: "m1",
|
|
212
|
+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
|
|
213
|
+
role: "user",
|
|
214
|
+
metadata: { custom: {} },
|
|
215
|
+
content: [{ type: "text", text: "please review these" }],
|
|
216
|
+
attachments: [
|
|
217
|
+
{
|
|
218
|
+
id: "att-1",
|
|
219
|
+
type: "file",
|
|
220
|
+
name: "bundle",
|
|
221
|
+
status: { type: "complete" },
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "image",
|
|
225
|
+
image: "data:image/png;base64,iVBORw0KGgo=",
|
|
226
|
+
filename: "shot.png",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: "audio",
|
|
230
|
+
audio: { data: "data:audio/mp3;base64,SUQzAw==", format: "mp3" },
|
|
231
|
+
},
|
|
232
|
+
{ type: "data", name: "telemetry", data: { runs: 3 } },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(encoded.attachments?.[0]?.content).toEqual([
|
|
239
|
+
{
|
|
240
|
+
type: "image",
|
|
241
|
+
image: "data:image/png;base64,iVBORw0KGgo=",
|
|
242
|
+
filename: "shot.png",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
type: "audio",
|
|
246
|
+
audio: { data: "data:audio/mp3;base64,SUQzAw==", format: "mp3" },
|
|
247
|
+
},
|
|
248
|
+
{ type: "data", name: "telemetry", data: { runs: 3 } },
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
158
251
|
});
|
|
159
252
|
|
|
160
253
|
describe("auiV0Decode", () => {
|
package/src/types/index.ts
CHANGED
package/src/types/message.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type PartProviderMetadata = {
|
|
|
14
14
|
export type TextMessagePart = {
|
|
15
15
|
readonly type: "text";
|
|
16
16
|
readonly text: string;
|
|
17
|
+
readonly status?: MessagePartStreamStatus;
|
|
17
18
|
readonly providerMetadata?: PartProviderMetadata;
|
|
18
19
|
readonly parentId?: string;
|
|
19
20
|
};
|
|
@@ -21,6 +22,7 @@ export type TextMessagePart = {
|
|
|
21
22
|
export type ReasoningMessagePart = {
|
|
22
23
|
readonly type: "reasoning";
|
|
23
24
|
readonly text: string;
|
|
25
|
+
readonly status?: MessagePartStreamStatus;
|
|
24
26
|
readonly providerMetadata?: PartProviderMetadata;
|
|
25
27
|
readonly parentId?: string;
|
|
26
28
|
};
|
|
@@ -266,6 +268,23 @@ export type MessagePartStatus =
|
|
|
266
268
|
readonly error?: unknown;
|
|
267
269
|
};
|
|
268
270
|
|
|
271
|
+
export type MessagePartStreamStatus =
|
|
272
|
+
| {
|
|
273
|
+
readonly type: "running";
|
|
274
|
+
}
|
|
275
|
+
| {
|
|
276
|
+
readonly type: "complete";
|
|
277
|
+
}
|
|
278
|
+
| {
|
|
279
|
+
readonly type: "incomplete";
|
|
280
|
+
readonly reason:
|
|
281
|
+
| "cancelled"
|
|
282
|
+
| "length"
|
|
283
|
+
| "content-filter"
|
|
284
|
+
| "other"
|
|
285
|
+
| "error";
|
|
286
|
+
};
|
|
287
|
+
|
|
269
288
|
export type ToolCallMessagePartStatus =
|
|
270
289
|
| {
|
|
271
290
|
/** The tool call is waiting for UI or human input before continuing. */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getGroupStatus } from "./getGroupStatus";
|
|
3
|
+
|
|
4
|
+
describe("getGroupStatus", () => {
|
|
5
|
+
it("reports running when the first member is running and the last is complete", () => {
|
|
6
|
+
const parts = [
|
|
7
|
+
{ status: { type: "running" as const } },
|
|
8
|
+
{ status: { type: "complete" as const } },
|
|
9
|
+
];
|
|
10
|
+
const status = getGroupStatus(parts);
|
|
11
|
+
|
|
12
|
+
expect(status).toEqual({ type: "running" });
|
|
13
|
+
expect(getGroupStatus(parts, [0, 1])).toBe(status);
|
|
14
|
+
expect(status).toBe(getGroupStatus([{ status: { type: "running" } }]));
|
|
15
|
+
expect(Object.isFrozen(status)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("reports the last status when every member is complete", () => {
|
|
19
|
+
const status = { type: "complete" } as const;
|
|
20
|
+
|
|
21
|
+
expect(getGroupStatus([{ status }, { status }])).toBe(status);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("reports complete for an empty group", () => {
|
|
25
|
+
expect(getGroupStatus([])).toEqual({ type: "complete" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("matches the positional outcome for statusless adapters", () => {
|
|
29
|
+
expect(
|
|
30
|
+
getGroupStatus(
|
|
31
|
+
[{ status: { type: "complete" } }, { status: { type: "running" } }],
|
|
32
|
+
[0, 1],
|
|
33
|
+
),
|
|
34
|
+
).toEqual({ type: "running" });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MessagePartStatus,
|
|
3
|
+
ToolCallMessagePartStatus,
|
|
4
|
+
} from "../types/message";
|
|
5
|
+
import { COMPLETE_STATUS, RUNNING_STATUS } from "./normalizePartStatus";
|
|
6
|
+
|
|
7
|
+
type PartWithStatus = {
|
|
8
|
+
readonly status: MessagePartStatus | ToolCallMessagePartStatus;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getGroupStatus = (
|
|
12
|
+
parts: readonly (PartWithStatus | undefined)[],
|
|
13
|
+
indices?: readonly number[],
|
|
14
|
+
): MessagePartStatus | ToolCallMessagePartStatus => {
|
|
15
|
+
if (indices) {
|
|
16
|
+
for (const index of indices) {
|
|
17
|
+
if (parts[index]?.status.type === "running") return RUNNING_STATUS;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lastIndex = indices.at(-1);
|
|
21
|
+
return lastIndex === undefined
|
|
22
|
+
? COMPLETE_STATUS
|
|
23
|
+
: (parts[lastIndex]?.status ?? COMPLETE_STATUS);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
if (part?.status.type === "running") return RUNNING_STATUS;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return parts.at(-1)?.status ?? COMPLETE_STATUS;
|
|
31
|
+
};
|