@agent-native/toolkit 0.203.0-nightly-20261007064822 → 0.203.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentkit.d.ts +2 -1
- package/dist/agentkit.d.ts.map +1 -1
- package/dist/agentkit.js +2 -1
- package/dist/agentkit.js.map +1 -1
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +5 -5
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/context.d.ts +1 -0
- package/dist/app/agentkit/react/context.d.ts.map +1 -1
- package/dist/app/agentkit/react/context.js +1 -0
- package/dist/app/agentkit/react/context.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +6 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +94 -5
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.js +4 -1
- package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts +1 -0
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +13 -1
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +2 -2
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +9 -6
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/package.json +4 -4
- package/src/agentkit.ts +2 -0
- package/src/app/agentkit/react/components.interaction.spec.tsx +68 -0
- package/src/app/agentkit/react/components.tsx +11 -4
- package/src/app/agentkit/react/context.tsx +2 -0
- package/src/app/agentkit/react/styles.css +6 -1
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +38 -1
- package/src/app/chat/AgentKitAssistantChat.tsx +111 -4
- package/src/app/chat/MultiTabAssistantChat.spec.tsx +82 -0
- package/src/app/chat/MultiTabAssistantChat.tsx +4 -1
- package/src/composer/PromptComposer.spec.ts +80 -0
- package/src/composer/PromptComposer.tsx +15 -0
- package/src/composer/TiptapComposer.spec.ts +23 -0
- package/src/composer/TiptapComposer.tsx +18 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/toolkit",
|
|
3
|
-
"version": "0.203.0
|
|
3
|
+
"version": "0.203.0",
|
|
4
4
|
"description": "Reusable app-building UI and helpers for Agent-Native apps.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -673,7 +673,7 @@
|
|
|
673
673
|
"y-protocols": "^1.0.7",
|
|
674
674
|
"yjs": "^13.6.33",
|
|
675
675
|
"zod": "^4.3.6",
|
|
676
|
-
"@agent-native/agentkit": "^0.203.0
|
|
676
|
+
"@agent-native/agentkit": "^0.203.0"
|
|
677
677
|
},
|
|
678
678
|
"devDependencies": {
|
|
679
679
|
"@excalidraw/excalidraw": "0.18.1",
|
|
@@ -697,10 +697,10 @@
|
|
|
697
697
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
698
698
|
"vite": "8.1.0",
|
|
699
699
|
"vitest": "^4.1.5",
|
|
700
|
-
"@agent-native/core": "^0.203.0
|
|
700
|
+
"@agent-native/core": "^0.203.0"
|
|
701
701
|
},
|
|
702
702
|
"peerDependencies": {
|
|
703
|
-
"@agent-native/core": "0.
|
|
703
|
+
"@agent-native/core": ">=0.196.0",
|
|
704
704
|
"@excalidraw/excalidraw": ">=0.18",
|
|
705
705
|
"@excalidraw/mermaid-to-excalidraw": ">=2",
|
|
706
706
|
"@tanstack/react-query": "^5.99.2",
|
package/src/agentkit.ts
CHANGED
|
@@ -3,6 +3,7 @@ export {
|
|
|
3
3
|
AgentSuggestionBar,
|
|
4
4
|
agentSuggestionPrompt,
|
|
5
5
|
} from "./composer/AgentSuggestionBar.js";
|
|
6
|
+
export { formatAttachmentError } from "./composer/attachment-accept.js";
|
|
6
7
|
export { MessageQueueDrawer } from "./composer/MessageQueueDrawer.js";
|
|
7
8
|
export {
|
|
8
9
|
ComposerContextMenu,
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
type ComposerContextPickerFooterAction,
|
|
22
23
|
} from "./composer/ComposerContextMenu.js";
|
|
23
24
|
export {
|
|
25
|
+
createChatAttachmentAdapter,
|
|
24
26
|
PromptComposer,
|
|
25
27
|
type PromptComposerFile,
|
|
26
28
|
type PromptComposerProps,
|
|
@@ -870,6 +870,74 @@ describe("AgentKitChat interactions", () => {
|
|
|
870
870
|
}
|
|
871
871
|
});
|
|
872
872
|
|
|
873
|
+
it("attaches dropped SVGs and names rejected drops without the accept list", async () => {
|
|
874
|
+
const transport: AgentTransport = {
|
|
875
|
+
capabilities: { uploads: true },
|
|
876
|
+
async startRun() {
|
|
877
|
+
return { runId: "run-svg" };
|
|
878
|
+
},
|
|
879
|
+
async *subscribeToRun() {},
|
|
880
|
+
async cancelRun() {},
|
|
881
|
+
};
|
|
882
|
+
const client = new AgentKitClient({ transport });
|
|
883
|
+
const onAttachmentError = vi.fn();
|
|
884
|
+
const container = document.createElement("div");
|
|
885
|
+
document.body.appendChild(container);
|
|
886
|
+
const root = createRoot(container);
|
|
887
|
+
const actEnvironment = globalThis as typeof globalThis & {
|
|
888
|
+
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
889
|
+
};
|
|
890
|
+
const previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
|
|
891
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
892
|
+
const dropOnChat = async (file: File) => {
|
|
893
|
+
const drop = new Event("drop", { bubbles: true, cancelable: true });
|
|
894
|
+
Object.defineProperty(drop, "dataTransfer", {
|
|
895
|
+
value: { types: ["Files"], files: [file], dropEffect: "none" },
|
|
896
|
+
});
|
|
897
|
+
await act(async () => {
|
|
898
|
+
container.querySelector(".agentkit-chat")?.dispatchEvent(drop);
|
|
899
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
900
|
+
});
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
await act(async () => {
|
|
905
|
+
root.render(
|
|
906
|
+
<AgentKitProvider controller={client} threadId="thread-svg">
|
|
907
|
+
<AgentKitChat composerProps={{ onAttachmentError }} />
|
|
908
|
+
</AgentKitProvider>,
|
|
909
|
+
);
|
|
910
|
+
await Promise.resolve();
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
await dropOnChat(
|
|
914
|
+
new File(['<svg xmlns="http://www.w3.org/2000/svg"/>'], "logo.svg", {
|
|
915
|
+
type: "image/svg+xml",
|
|
916
|
+
}),
|
|
917
|
+
);
|
|
918
|
+
expect(container.textContent).toContain("logo.svg");
|
|
919
|
+
expect(onAttachmentError).not.toHaveBeenCalled();
|
|
920
|
+
|
|
921
|
+
await dropOnChat(
|
|
922
|
+
new File(["zip"], "archive.zip", { type: "application/zip" }),
|
|
923
|
+
);
|
|
924
|
+
expect(onAttachmentError).toHaveBeenCalledWith(
|
|
925
|
+
"archive.zip: Could not add the dropped file. Try a different format.",
|
|
926
|
+
);
|
|
927
|
+
} finally {
|
|
928
|
+
await act(async () => {
|
|
929
|
+
root.unmount();
|
|
930
|
+
await Promise.resolve();
|
|
931
|
+
});
|
|
932
|
+
container.remove();
|
|
933
|
+
if (previousActEnvironment === undefined) {
|
|
934
|
+
delete actEnvironment.IS_REACT_ACT_ENVIRONMENT;
|
|
935
|
+
} else {
|
|
936
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
873
941
|
it("disables transcript file drops when the host disables uploads", async () => {
|
|
874
942
|
const transport: AgentTransport = {
|
|
875
943
|
capabilities: { uploads: true },
|
|
@@ -68,6 +68,8 @@ import {
|
|
|
68
68
|
Surface,
|
|
69
69
|
TextField,
|
|
70
70
|
agentSuggestionPrompt,
|
|
71
|
+
createChatAttachmentAdapter,
|
|
72
|
+
formatAttachmentError,
|
|
71
73
|
type PromptComposerFile,
|
|
72
74
|
type PromptComposerProps,
|
|
73
75
|
type TiptapComposerHandle,
|
|
@@ -4065,6 +4067,10 @@ export function AgentKitComposer({
|
|
|
4065
4067
|
}, []);
|
|
4066
4068
|
const [uncontrolledMode, setUncontrolledMode] = useState(defaultMode);
|
|
4067
4069
|
const [attachmentError, setAttachmentError] = useState<string | null>(null);
|
|
4070
|
+
const composerAttachmentAdapter = useMemo(
|
|
4071
|
+
() => attachmentAdapter ?? createChatAttachmentAdapter(),
|
|
4072
|
+
[attachmentAdapter],
|
|
4073
|
+
);
|
|
4068
4074
|
const executionMode = mode ?? uncontrolledMode;
|
|
4069
4075
|
const active = hasActiveRuns(thread);
|
|
4070
4076
|
const composerInitialText = editingMessage
|
|
@@ -4095,12 +4101,13 @@ export function AgentKitComposer({
|
|
|
4095
4101
|
await composer.addAttachment(file);
|
|
4096
4102
|
setAttachmentError(null);
|
|
4097
4103
|
} catch (error) {
|
|
4098
|
-
|
|
4099
|
-
|
|
4104
|
+
reportAttachmentError(
|
|
4105
|
+
`${file.name}: ${formatAttachmentError(error, labels.dropFileFailed)}`,
|
|
4106
|
+
);
|
|
4100
4107
|
throw error;
|
|
4101
4108
|
}
|
|
4102
4109
|
},
|
|
4103
|
-
[labels.error, reportAttachmentError],
|
|
4110
|
+
[labels.dropFileFailed, labels.error, reportAttachmentError],
|
|
4104
4111
|
);
|
|
4105
4112
|
useEffect(() => {
|
|
4106
4113
|
const receiveAttachments = (event: Event) => {
|
|
@@ -4494,7 +4501,7 @@ export function AgentKitComposer({
|
|
|
4494
4501
|
onInspectContextItem={onInspectContextItem}
|
|
4495
4502
|
onRetryContextItem={onRetryContextItem}
|
|
4496
4503
|
contextMenuItems={contextMenuItems}
|
|
4497
|
-
attachmentAdapter={
|
|
4504
|
+
attachmentAdapter={composerAttachmentAdapter}
|
|
4498
4505
|
inlineTextAttachments={inlineTextAttachments}
|
|
4499
4506
|
rootClassName="agentkit-composer"
|
|
4500
4507
|
className={
|
|
@@ -274,6 +274,7 @@ export interface AgentKitLabels {
|
|
|
274
274
|
imagePreview: string;
|
|
275
275
|
closePreview: string;
|
|
276
276
|
dropFilesToAttach: string;
|
|
277
|
+
dropFileFailed: string;
|
|
277
278
|
scrollToBottom: string;
|
|
278
279
|
formatTimestamp?: (createdAt: string) => string;
|
|
279
280
|
error: string;
|
|
@@ -384,6 +385,7 @@ export const defaultAgentKitLabels: AgentKitLabels = {
|
|
|
384
385
|
imagePreview: "Image preview",
|
|
385
386
|
closePreview: "Close preview",
|
|
386
387
|
dropFilesToAttach: "Drop files to attach",
|
|
388
|
+
dropFileFailed: "Could not add the dropped file. Try a different format.",
|
|
387
389
|
scrollToBottom: "Scroll to bottom",
|
|
388
390
|
error: "Something went wrong",
|
|
389
391
|
renderError: "This content couldn’t be displayed.",
|
|
@@ -1715,11 +1715,16 @@ button.agentkit-object {
|
|
|
1715
1715
|
|
|
1716
1716
|
.agentkit-composer-error {
|
|
1717
1717
|
display: flex;
|
|
1718
|
-
align-items:
|
|
1718
|
+
align-items: flex-start;
|
|
1719
1719
|
gap: 0.375rem;
|
|
1720
1720
|
padding: 0.375rem 0.625rem 0;
|
|
1721
1721
|
}
|
|
1722
1722
|
|
|
1723
|
+
.agentkit-composer-error > span {
|
|
1724
|
+
min-inline-size: 0;
|
|
1725
|
+
overflow-wrap: anywhere;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1723
1728
|
.agentkit-file {
|
|
1724
1729
|
display: inline-flex;
|
|
1725
1730
|
align-items: center;
|
|
@@ -52,6 +52,7 @@ const chatMocks = vi.hoisted(() => ({
|
|
|
52
52
|
reasoningProps: null as any,
|
|
53
53
|
thinkingDisplay: null as any,
|
|
54
54
|
requestComposerFocus: vi.fn(),
|
|
55
|
+
persistThreadSnapshot: vi.fn(async () => undefined),
|
|
55
56
|
readiness: { canChat: true, missing: false, state: "configured" },
|
|
56
57
|
fetchProviderState: vi.fn(async () => chatMocks.readiness.state),
|
|
57
58
|
fileUploadStatus: {
|
|
@@ -239,7 +240,10 @@ vi.mock("../agentkit/react/index.js", async () => {
|
|
|
239
240
|
: {
|
|
240
241
|
threadId: chatMocks.threadId,
|
|
241
242
|
requestComposerFocus: chatMocks.requestComposerFocus,
|
|
242
|
-
controller: {
|
|
243
|
+
controller: {
|
|
244
|
+
getThread: () => chatMocks.readThread(),
|
|
245
|
+
persistThreadSnapshot: chatMocks.persistThreadSnapshot,
|
|
246
|
+
},
|
|
243
247
|
},
|
|
244
248
|
useAgentKitControl: () =>
|
|
245
249
|
chatMocks.useRealChat ? useAgentKitControl() : chatMocks.control,
|
|
@@ -755,6 +759,7 @@ beforeEach(() => {
|
|
|
755
759
|
chatMocks.reasoningProps = null;
|
|
756
760
|
chatMocks.thinkingDisplay = null;
|
|
757
761
|
chatMocks.requestComposerFocus.mockReset();
|
|
762
|
+
chatMocks.persistThreadSnapshot.mockReset().mockResolvedValue(undefined);
|
|
758
763
|
chatMocks.readiness = {
|
|
759
764
|
canChat: true,
|
|
760
765
|
missing: false,
|
|
@@ -4862,6 +4867,38 @@ describe("AgentKitAssistantChat host behavior", () => {
|
|
|
4862
4867
|
window.removeEventListener("agentNative.chatRunning", onRunning);
|
|
4863
4868
|
});
|
|
4864
4869
|
|
|
4870
|
+
it("persists changed custom-transport snapshots through the protocol save", async () => {
|
|
4871
|
+
const createTransport = () => chatMocks.transport;
|
|
4872
|
+
const onSaveThread = vi.fn();
|
|
4873
|
+
const message = {
|
|
4874
|
+
id: "custom-user-message",
|
|
4875
|
+
role: "user",
|
|
4876
|
+
status: "complete",
|
|
4877
|
+
createdAt: "2026-10-07T12:00:00.000Z",
|
|
4878
|
+
parts: [{ type: "text", text: "Save this message" }],
|
|
4879
|
+
};
|
|
4880
|
+
await mount(baseProps({ createTransport, onSaveThread }));
|
|
4881
|
+
|
|
4882
|
+
chatMocks.thread = {
|
|
4883
|
+
...chatMocks.thread,
|
|
4884
|
+
thread: null,
|
|
4885
|
+
messages: [message],
|
|
4886
|
+
};
|
|
4887
|
+
await act(async () => {
|
|
4888
|
+
root.render(
|
|
4889
|
+
<AgentKitAssistantChat
|
|
4890
|
+
{...baseProps({ createTransport, onSaveThread })}
|
|
4891
|
+
/>,
|
|
4892
|
+
);
|
|
4893
|
+
});
|
|
4894
|
+
await flush();
|
|
4895
|
+
|
|
4896
|
+
expect(chatMocks.persistThreadSnapshot).toHaveBeenCalledWith("thread-1", [
|
|
4897
|
+
expect.objectContaining({ id: "custom-user-message" }),
|
|
4898
|
+
]);
|
|
4899
|
+
expect(onSaveThread).toHaveBeenCalledOnce();
|
|
4900
|
+
});
|
|
4901
|
+
|
|
4865
4902
|
it("shows an expired-session card and emits the session-expired event", async () => {
|
|
4866
4903
|
chatMocks.failureError = { code: "unauthorized", message: "HTTP 401" };
|
|
4867
4904
|
vi.stubGlobal(
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
AgentEvent,
|
|
13
13
|
AgentMessage,
|
|
14
14
|
AgentStreamIntegrityReport,
|
|
15
|
+
AgentTransport,
|
|
15
16
|
AgentThreadSnapshot,
|
|
16
17
|
AgentToolCall,
|
|
17
18
|
AgentUploadTarget,
|
|
@@ -211,6 +212,65 @@ const deferredProviderSubmissionOperations = new Map<
|
|
|
211
212
|
const RECOVERY_CONTINUE_PROMPT =
|
|
212
213
|
"Continue from where you left off and finish my last request. Do not repeat completed work.";
|
|
213
214
|
|
|
215
|
+
function withCoreSnapshotPersistence(
|
|
216
|
+
customTransport: AgentTransport,
|
|
217
|
+
coreTransport: AgentTransport,
|
|
218
|
+
): AgentTransport {
|
|
219
|
+
if (customTransport === coreTransport) return customTransport;
|
|
220
|
+
const corePersist = coreTransport.persistThreadSnapshot;
|
|
221
|
+
if (!corePersist) {
|
|
222
|
+
throw new TypeError("Core transport must persist thread snapshots.");
|
|
223
|
+
}
|
|
224
|
+
const boundMethods = new Map<PropertyKey, Function>();
|
|
225
|
+
let disposed = false;
|
|
226
|
+
const persistThreadSnapshot: NonNullable<
|
|
227
|
+
AgentTransport["persistThreadSnapshot"]
|
|
228
|
+
> = async (...args) => {
|
|
229
|
+
const writes = [
|
|
230
|
+
...(customTransport.persistThreadSnapshot
|
|
231
|
+
? [customTransport.persistThreadSnapshot.call(customTransport, ...args)]
|
|
232
|
+
: []),
|
|
233
|
+
corePersist.call(coreTransport, ...args),
|
|
234
|
+
];
|
|
235
|
+
const results = await Promise.allSettled(writes);
|
|
236
|
+
const errors = results.flatMap((result) =>
|
|
237
|
+
result.status === "rejected" ? [result.reason] : [],
|
|
238
|
+
);
|
|
239
|
+
if (errors.length === 1) throw errors[0];
|
|
240
|
+
if (errors.length > 1) {
|
|
241
|
+
throw new AggregateError(errors, "Thread snapshot persistence failed.");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const dispose = async () => {
|
|
245
|
+
if (disposed) return;
|
|
246
|
+
disposed = true;
|
|
247
|
+
const results = await Promise.allSettled([
|
|
248
|
+
Promise.resolve(customTransport.dispose?.()),
|
|
249
|
+
Promise.resolve(coreTransport.dispose?.()),
|
|
250
|
+
]);
|
|
251
|
+
const errors = results.flatMap((result) =>
|
|
252
|
+
result.status === "rejected" ? [result.reason] : [],
|
|
253
|
+
);
|
|
254
|
+
if (errors.length === 1) throw errors[0];
|
|
255
|
+
if (errors.length > 1) {
|
|
256
|
+
throw new AggregateError(errors, "Chat transport disposal failed.");
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
return new Proxy(customTransport, {
|
|
260
|
+
get(target, property) {
|
|
261
|
+
if (property === "persistThreadSnapshot") return persistThreadSnapshot;
|
|
262
|
+
if (property === "dispose") return dispose;
|
|
263
|
+
const value = Reflect.get(target, property, target);
|
|
264
|
+
if (typeof value !== "function") return value;
|
|
265
|
+
const cached = boundMethods.get(property);
|
|
266
|
+
if (cached) return cached;
|
|
267
|
+
const bound = value.bind(target);
|
|
268
|
+
boundMethods.set(property, bound);
|
|
269
|
+
return bound;
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
214
274
|
type ThreadRestoreState =
|
|
215
275
|
| { status: "ready" | "loading" }
|
|
216
276
|
| { status: "error"; notFound: boolean };
|
|
@@ -827,6 +887,7 @@ export const AgentKitAssistantChat = forwardRef<
|
|
|
827
887
|
imagePreview: t("agentChat.composer.imagePreview"),
|
|
828
888
|
closePreview: t("agentChat.composer.closePreview"),
|
|
829
889
|
dropFilesToAttach: t("agentChat.composer.dropToAttach"),
|
|
890
|
+
dropFileFailed: t("agentChat.composer.droppedFileError"),
|
|
830
891
|
scrollToBottom: t("agentChat.composer.scrollToBottom"),
|
|
831
892
|
formatTimestamp: (createdAt: string) => {
|
|
832
893
|
const date = new Date(createdAt);
|
|
@@ -982,8 +1043,9 @@ export const AgentKitAssistantChat = forwardRef<
|
|
|
982
1043
|
: props.agentChatSurface === "desktop"
|
|
983
1044
|
? "desktop"
|
|
984
1045
|
: "app";
|
|
1046
|
+
const apiUrl = props.apiUrl ?? agentNativePath("/_agent-native/agent-chat");
|
|
985
1047
|
const adapterContext: AssistantChatAdapterContext = {
|
|
986
|
-
apiUrl
|
|
1048
|
+
apiUrl,
|
|
987
1049
|
streamingUrl: props.streamingUrl,
|
|
988
1050
|
tabId: props.tabId,
|
|
989
1051
|
threadId,
|
|
@@ -998,7 +1060,27 @@ export const AgentKitAssistantChat = forwardRef<
|
|
|
998
1060
|
surface,
|
|
999
1061
|
};
|
|
1000
1062
|
const customTransport = createTransportRef.current;
|
|
1001
|
-
if (customTransport)
|
|
1063
|
+
if (customTransport) {
|
|
1064
|
+
const persistenceTransport = createAgentNativeAgentKitTransport({
|
|
1065
|
+
apiUrl,
|
|
1066
|
+
browserTabId: props.browserTabId,
|
|
1067
|
+
get threadId() {
|
|
1068
|
+
return transportThreadIdRef.current;
|
|
1069
|
+
},
|
|
1070
|
+
surface,
|
|
1071
|
+
get scope() {
|
|
1072
|
+
return scopeRef.current;
|
|
1073
|
+
},
|
|
1074
|
+
get isolateHistoryByScope() {
|
|
1075
|
+
return isolateHistoryByScopeRef.current;
|
|
1076
|
+
},
|
|
1077
|
+
adapter: { textFormat: "markdown" },
|
|
1078
|
+
});
|
|
1079
|
+
return withCoreSnapshotPersistence(
|
|
1080
|
+
customTransport(adapterContext),
|
|
1081
|
+
persistenceTransport,
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1002
1084
|
const runtimeOptions: CreateAgentNativeChatRuntimeOptions = {
|
|
1003
1085
|
apiUrl: props.apiUrl ?? agentNativePath("/_agent-native/agent-chat"),
|
|
1004
1086
|
streamingUrl: props.streamingUrl,
|
|
@@ -1890,6 +1972,12 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
1890
1972
|
if (!props.onSaveThread) return;
|
|
1891
1973
|
if (snapshot.threadData === lastSavedThreadDataRef.current) return;
|
|
1892
1974
|
lastSavedThreadDataRef.current = snapshot.threadData;
|
|
1975
|
+
if (props.createTransport) {
|
|
1976
|
+
void controller.persistThreadSnapshot(
|
|
1977
|
+
threadId,
|
|
1978
|
+
agentKitMessagesFromThreadSnapshot(snapshot),
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1893
1981
|
props.onSaveThread(threadId, snapshot);
|
|
1894
1982
|
};
|
|
1895
1983
|
|
|
@@ -1930,11 +2018,17 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
1930
2018
|
);
|
|
1931
2019
|
if (props.onSaveThread) {
|
|
1932
2020
|
lastSavedThreadDataRef.current = snapshot.threadData;
|
|
2021
|
+
if (props.createTransport) {
|
|
2022
|
+
void controller.persistThreadSnapshot(
|
|
2023
|
+
threadId,
|
|
2024
|
+
agentKitMessagesFromThreadSnapshot(snapshot),
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
1933
2027
|
props.onSaveThread(threadId, snapshot);
|
|
1934
2028
|
}
|
|
1935
2029
|
return true;
|
|
1936
2030
|
},
|
|
1937
|
-
[isRestoring, isRunning, props, thread, threadId],
|
|
2031
|
+
[controller, isRestoring, isRunning, props, thread, threadId],
|
|
1938
2032
|
);
|
|
1939
2033
|
|
|
1940
2034
|
useEffect(() => {
|
|
@@ -4146,7 +4240,9 @@ function AgentKitComposerSurface({
|
|
|
4146
4240
|
className="mx-3 mb-1.5 flex shrink-0 items-start gap-2 rounded-md border border-border bg-muted/70 px-3 py-2 text-xs text-foreground shadow-sm"
|
|
4147
4241
|
>
|
|
4148
4242
|
<IconAlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
4149
|
-
<span className="flex-1 leading-snug">
|
|
4243
|
+
<span className="min-w-0 flex-1 break-words leading-snug">
|
|
4244
|
+
{composerError}
|
|
4245
|
+
</span>
|
|
4150
4246
|
<button
|
|
4151
4247
|
type="button"
|
|
4152
4248
|
aria-label={t("agentChat.common.dismissError")}
|
|
@@ -5265,6 +5361,17 @@ function createAgentKitThreadSnapshot(thread: AgentThreadState) {
|
|
|
5265
5361
|
};
|
|
5266
5362
|
}
|
|
5267
5363
|
|
|
5364
|
+
function agentKitMessagesFromThreadSnapshot(
|
|
5365
|
+
snapshot: ReturnType<typeof createAgentKitThreadSnapshot>,
|
|
5366
|
+
): AgentMessage[] {
|
|
5367
|
+
const repository = asRecord(JSON.parse(snapshot.threadData));
|
|
5368
|
+
const agentKit = asRecord(repository?.agentKit);
|
|
5369
|
+
if (!Array.isArray(agentKit?.messages)) {
|
|
5370
|
+
throw new TypeError("AgentKit thread snapshot is missing its messages.");
|
|
5371
|
+
}
|
|
5372
|
+
return agentKit.messages as AgentMessage[];
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5268
5375
|
function createAgentKitThreadHandoffKey(
|
|
5269
5376
|
props: Pick<AssistantChatProps, "apiUrl" | "browserTabId" | "contextScope">,
|
|
5270
5377
|
threadId: string,
|
|
@@ -32,6 +32,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
32
32
|
import {
|
|
33
33
|
MultiTabAssistantChat,
|
|
34
34
|
type MultiTabAssistantChatHeaderProps,
|
|
35
|
+
type MultiTabAssistantChatProps,
|
|
35
36
|
} from "./MultiTabAssistantChat.js";
|
|
36
37
|
|
|
37
38
|
afterEach(() => {
|
|
@@ -84,6 +85,17 @@ const assistantChatMockState = vi.hoisted(() => ({
|
|
|
84
85
|
onRetryModelList: undefined as (() => void) | undefined,
|
|
85
86
|
onSlashCommand: undefined as ((command: string) => void) | undefined,
|
|
86
87
|
onForkedThread: undefined as ((threadId: string) => void) | undefined,
|
|
88
|
+
onSaveThread: undefined as
|
|
89
|
+
| ((
|
|
90
|
+
threadId: string,
|
|
91
|
+
data: {
|
|
92
|
+
threadData: string;
|
|
93
|
+
title: string;
|
|
94
|
+
preview: string;
|
|
95
|
+
messageCount: number;
|
|
96
|
+
},
|
|
97
|
+
) => void)
|
|
98
|
+
| undefined,
|
|
87
99
|
branchNavigation: undefined as
|
|
88
100
|
| {
|
|
89
101
|
index: number;
|
|
@@ -339,6 +351,7 @@ vi.mock("./AgentKitAssistantChat.js", async () => {
|
|
|
339
351
|
onRetryModelList?: () => void;
|
|
340
352
|
onSlashCommand?: (command: string) => void;
|
|
341
353
|
onForkedThread?: (threadId: string) => void;
|
|
354
|
+
onSaveThread?: typeof assistantChatMockState.onSaveThread;
|
|
342
355
|
branchNavigation?: typeof assistantChatMockState.branchNavigation;
|
|
343
356
|
};
|
|
344
357
|
assistantChatMockState.onThreadRestoreNotFound =
|
|
@@ -346,6 +359,7 @@ vi.mock("./AgentKitAssistantChat.js", async () => {
|
|
|
346
359
|
assistantChatMockState.onRetryModelList = props.onRetryModelList;
|
|
347
360
|
assistantChatMockState.onSlashCommand = props.onSlashCommand;
|
|
348
361
|
assistantChatMockState.onForkedThread = props.onForkedThread;
|
|
362
|
+
assistantChatMockState.onSaveThread = props.onSaveThread;
|
|
349
363
|
assistantChatMockState.branchNavigation = props.branchNavigation;
|
|
350
364
|
React.useImperativeHandle(ref, () => ({
|
|
351
365
|
sendMessage: chatHandleMocks.sendMessage,
|
|
@@ -399,6 +413,7 @@ function resetThreadMocks() {
|
|
|
399
413
|
assistantChatMockState.onRetryModelList = undefined;
|
|
400
414
|
assistantChatMockState.onSlashCommand = undefined;
|
|
401
415
|
assistantChatMockState.onForkedThread = undefined;
|
|
416
|
+
assistantChatMockState.onSaveThread = undefined;
|
|
402
417
|
assistantChatMockState.branchNavigation = undefined;
|
|
403
418
|
threadMocks.activeThreadId = "thread-1";
|
|
404
419
|
threadMocks.evictedThreadIds = [];
|
|
@@ -501,6 +516,73 @@ describe("MultiTabAssistantChat postMessage bridge", () => {
|
|
|
501
516
|
vi.clearAllMocks();
|
|
502
517
|
});
|
|
503
518
|
|
|
519
|
+
it("keeps thread saves metadata-only for built-in, runtime, and custom transports", async () => {
|
|
520
|
+
const snapshot = {
|
|
521
|
+
threadData: JSON.stringify({ messages: [{ id: "message-1" }] }),
|
|
522
|
+
title: "Saved chat",
|
|
523
|
+
preview: "Latest request",
|
|
524
|
+
messageCount: 1,
|
|
525
|
+
};
|
|
526
|
+
window.history.replaceState(null, "", "/");
|
|
527
|
+
|
|
528
|
+
await act(async () => {
|
|
529
|
+
root.render(
|
|
530
|
+
<MultiTabAssistantChat storageKey="bridge-test" threadUrlSync />,
|
|
531
|
+
);
|
|
532
|
+
await Promise.resolve();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
act(() => assistantChatMockState.onSaveThread?.("thread-1", snapshot));
|
|
536
|
+
|
|
537
|
+
expect(threadMocks.saveThreadData).toHaveBeenLastCalledWith("thread-1", {
|
|
538
|
+
...snapshot,
|
|
539
|
+
threadData: "",
|
|
540
|
+
});
|
|
541
|
+
expect(window.location.search).toBe("?thread=thread-1");
|
|
542
|
+
|
|
543
|
+
const createTransport = (() => ({}) as never) as NonNullable<
|
|
544
|
+
MultiTabAssistantChatProps["createTransport"]
|
|
545
|
+
>;
|
|
546
|
+
await act(async () => {
|
|
547
|
+
root.render(
|
|
548
|
+
<MultiTabAssistantChat
|
|
549
|
+
storageKey="bridge-test"
|
|
550
|
+
threadUrlSync
|
|
551
|
+
createTransport={createTransport}
|
|
552
|
+
/>,
|
|
553
|
+
);
|
|
554
|
+
await Promise.resolve();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
act(() => assistantChatMockState.onSaveThread?.("thread-1", snapshot));
|
|
558
|
+
|
|
559
|
+
expect(threadMocks.saveThreadData).toHaveBeenLastCalledWith("thread-1", {
|
|
560
|
+
...snapshot,
|
|
561
|
+
threadData: "",
|
|
562
|
+
});
|
|
563
|
+
expect(window.location.search).toBe("?thread=thread-1");
|
|
564
|
+
|
|
565
|
+
const runtime = {} as NonNullable<MultiTabAssistantChatProps["runtime"]>;
|
|
566
|
+
await act(async () => {
|
|
567
|
+
root.render(
|
|
568
|
+
<MultiTabAssistantChat
|
|
569
|
+
storageKey="bridge-test"
|
|
570
|
+
threadUrlSync
|
|
571
|
+
runtime={runtime}
|
|
572
|
+
/>,
|
|
573
|
+
);
|
|
574
|
+
await Promise.resolve();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
act(() => assistantChatMockState.onSaveThread?.("thread-1", snapshot));
|
|
578
|
+
|
|
579
|
+
expect(threadMocks.saveThreadData).toHaveBeenLastCalledWith("thread-1", {
|
|
580
|
+
...snapshot,
|
|
581
|
+
threadData: "",
|
|
582
|
+
});
|
|
583
|
+
expect(window.location.search).toBe("?thread=thread-1");
|
|
584
|
+
});
|
|
585
|
+
|
|
504
586
|
it("prefills the active composer without submitting when submit is false", () => {
|
|
505
587
|
act(() => {
|
|
506
588
|
dispatchSubmitChat({
|
|
@@ -2749,7 +2749,10 @@ export function MultiTabAssistantChat({
|
|
|
2749
2749
|
messageCount: number;
|
|
2750
2750
|
},
|
|
2751
2751
|
) => {
|
|
2752
|
-
void saveThreadData(threadId,
|
|
2752
|
+
void saveThreadData(threadId, {
|
|
2753
|
+
...data,
|
|
2754
|
+
threadData: "",
|
|
2755
|
+
});
|
|
2753
2756
|
if (
|
|
2754
2757
|
data.messageCount > 0 &&
|
|
2755
2758
|
threadId === activeThreadIdRef.current &&
|
|
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
buildPromptComposerSubmission,
|
|
9
|
+
createChatAttachmentAdapter,
|
|
9
10
|
PromptComposer,
|
|
10
11
|
resolveComposerModelStatusChecksEnabled,
|
|
11
12
|
shouldGateComposerForEngine,
|
|
@@ -234,3 +235,82 @@ describe("PromptComposer scoped runtime", () => {
|
|
|
234
235
|
expect(attachedFiles).toHaveLength(0);
|
|
235
236
|
});
|
|
236
237
|
});
|
|
238
|
+
|
|
239
|
+
describe("PromptComposer attachment policy", () => {
|
|
240
|
+
it("keeps SVGs out of standalone prompts while chat stages them as documents", async () => {
|
|
241
|
+
await act(async () => {
|
|
242
|
+
root.render(
|
|
243
|
+
React.createElement(PromptComposer, {
|
|
244
|
+
attachmentsEnabled: true,
|
|
245
|
+
includeDefaultSlashSkills: false,
|
|
246
|
+
onSubmit: () => {},
|
|
247
|
+
plusMenuMode: "upload-only",
|
|
248
|
+
showModelSelector: false,
|
|
249
|
+
voiceEnabled: false,
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
253
|
+
});
|
|
254
|
+
const standaloneAccept = container
|
|
255
|
+
.querySelector<HTMLInputElement>('input[type="file"]')
|
|
256
|
+
?.getAttribute("accept")
|
|
257
|
+
?.split(",");
|
|
258
|
+
expect(standaloneAccept).toContain(".pdf");
|
|
259
|
+
expect(standaloneAccept).not.toContain(".svg");
|
|
260
|
+
|
|
261
|
+
const chatAdapter = createChatAttachmentAdapter();
|
|
262
|
+
expect(chatAdapter.accept.split(",")).toEqual(
|
|
263
|
+
expect.arrayContaining(["image/svg+xml", ".svg", ".png", ".pdf"]),
|
|
264
|
+
);
|
|
265
|
+
await expect(
|
|
266
|
+
chatAdapter.add({
|
|
267
|
+
file: new File(["<svg/>"], "logo.svg", { type: "image/svg+xml" }),
|
|
268
|
+
}),
|
|
269
|
+
).resolves.toMatchObject({
|
|
270
|
+
type: "document",
|
|
271
|
+
name: "logo.svg",
|
|
272
|
+
contentType: "image/svg+xml",
|
|
273
|
+
});
|
|
274
|
+
await expect(
|
|
275
|
+
chatAdapter.add({
|
|
276
|
+
file: new File(["png"], "logo.png", { type: "image/png" }),
|
|
277
|
+
}),
|
|
278
|
+
).resolves.toMatchObject({ type: "image", name: "logo.png" });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("shows why a file was rejected when the host passes no error handler", async () => {
|
|
282
|
+
await act(async () => {
|
|
283
|
+
root.render(
|
|
284
|
+
React.createElement(PromptComposer, {
|
|
285
|
+
attachmentsEnabled: true,
|
|
286
|
+
includeDefaultSlashSkills: false,
|
|
287
|
+
onSubmit: () => {},
|
|
288
|
+
plusMenuMode: "upload-only",
|
|
289
|
+
showModelSelector: false,
|
|
290
|
+
voiceEnabled: false,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
294
|
+
});
|
|
295
|
+
const editor = container.querySelector<HTMLElement>(
|
|
296
|
+
".agent-composer-prosemirror",
|
|
297
|
+
);
|
|
298
|
+
expect(editor).not.toBeNull();
|
|
299
|
+
const pasteEvent = new Event("paste", { bubbles: true, cancelable: true });
|
|
300
|
+
Object.defineProperty(pasteEvent, "clipboardData", {
|
|
301
|
+
value: {
|
|
302
|
+
files: [new File(["<svg/>"], "logo.svg", { type: "image/svg+xml" })],
|
|
303
|
+
getData: () => "",
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await act(async () => {
|
|
308
|
+
editor?.dispatchEvent(pasteEvent);
|
|
309
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(container.querySelector('[role="alert"]')?.textContent).toBe(
|
|
313
|
+
"Could not attach the pasted image. Try a different format.",
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
});
|