






















@@ -6,6 +6,7 @@ import {
66getChatAttachmentDataUrl,
77getChatAttachmentPreviewUrl,
88registerChatAttachmentPayload,
9+releaseChatAttachmentPayloads,
910resetChatAttachmentPayloadStoreForTest,
1011} from "./chat/attachment-payload-store.ts";
1112import type { executeSlashCommand } from "./chat/slash-command-executor.ts";
@@ -99,6 +100,7 @@ function makeHost(overrides?: Partial<ChatHost>): ChatHost {
99100chatSideResult: null,
100101chatSideResultTerminalRuns: new Set<string>(),
101102chatModelOverrides: {},
103+chatModelSwitchPromises: {},
102104chatModelsLoading: false,
103105chatModelCatalog: [],
104106refreshSessionsAfterChat: new Set<string>(),
@@ -631,6 +633,286 @@ describe("handleSendChat", () => {
631633expect(host.chatMessage).toBe("");
632634});
633635636+it("waits for an in-flight model picker update before sending chat", async () => {
637+const switchUpdate = createDeferred<boolean>();
638+const request = vi.fn(async (method: string) => {
639+if (method === "chat.send") {
640+return { status: "started" };
641+}
642+throw new Error(`Unexpected request: ${method}`);
643+});
644+const host = makeHost({
645+client: { request } as unknown as ChatHost["client"],
646+chatMessage: "use the newly selected model",
647+chatModelSwitchPromises: { "agent:main": switchUpdate.promise },
648+});
649+650+const send = handleSendChat(host);
651+await Promise.resolve();
652+653+expect(request).not.toHaveBeenCalled();
654+expect(host.chatMessage).toBe("use the newly selected model");
655+656+switchUpdate.resolve(true);
657+await send;
658+659+expect(request).toHaveBeenCalledWith(
660+"chat.send",
661+expect.objectContaining({
662+sessionKey: "agent:main",
663+message: "use the newly selected model",
664+}),
665+);
666+expect(host.chatMessage).toBe("");
667+});
668+669+it("preserves draft edits made while waiting for a model picker update", async () => {
670+const switchUpdate = createDeferred<boolean>();
671+const request = vi.fn(async (method: string) => {
672+if (method === "chat.send") {
673+return { status: "started" };
674+}
675+throw new Error(`Unexpected request: ${method}`);
676+});
677+const host = makeHost({
678+client: { request } as unknown as ChatHost["client"],
679+chatMessage: "send this",
680+chatModelSwitchPromises: { "agent:main": switchUpdate.promise },
681+});
682+683+const send = handleSendChat(host);
684+await Promise.resolve();
685+host.chatMessage = "keep typing";
686+687+switchUpdate.resolve(true);
688+await send;
689+690+expect(request).toHaveBeenCalledWith(
691+"chat.send",
692+expect.objectContaining({
693+sessionKey: "agent:main",
694+message: "send this",
695+}),
696+);
697+expect(host.chatMessage).toBe("keep typing");
698+});
699+700+it("preserves attachment payloads for edited drafts after a delayed send", async () => {
701+const switchUpdate = createDeferred<boolean>();
702+const request = vi.fn(async (method: string) => {
703+if (method === "chat.send") {
704+return { status: "started" };
705+}
706+throw new Error(`Unexpected request: ${method}`);
707+});
708+const file = new File(["%PDF-1.4\n"], "brief.pdf", { type: "application/pdf" });
709+const attachment = registerChatAttachmentPayload({
710+attachment: {
711+id: "delayed-att",
712+mimeType: "application/pdf",
713+fileName: "brief.pdf",
714+sizeBytes: file.size,
715+},
716+dataUrl: "data:application/pdf;base64,JVBERi0xLjQK",
717+ file,
718+});
719+const host = makeHost({
720+client: { request } as unknown as ChatHost["client"],
721+chatAttachments: [attachment],
722+chatMessage: "send this",
723+chatModelSwitchPromises: { "agent:main": switchUpdate.promise },
724+});
725+726+const send = handleSendChat(host);
727+await Promise.resolve();
728+host.chatMessage = "keep typing with the attachment";
729+730+switchUpdate.resolve(true);
731+await send;
732+733+expect(request).toHaveBeenCalledWith(
734+"chat.send",
735+expect.objectContaining({
736+attachments: [
737+expect.objectContaining({
738+content: "JVBERi0xLjQK",
739+fileName: "brief.pdf",
740+mimeType: "application/pdf",
741+type: "file",
742+}),
743+],
744+message: "send this",
745+}),
746+);
747+expect(host.chatMessage).toBe("keep typing with the attachment");
748+expect(host.chatAttachments).toEqual([attachment]);
749+expect(getChatAttachmentDataUrl(attachment)).toBe("data:application/pdf;base64,JVBERi0xLjQK");
750+});
751+752+it("preserves draft text when only attachments change during a delayed send", async () => {
753+const switchUpdate = createDeferred<boolean>();
754+const request = vi.fn(async (method: string) => {
755+if (method === "chat.send") {
756+return { status: "started" };
757+}
758+throw new Error(`Unexpected request: ${method}`);
759+});
760+const originalFile = new File(["original"], "original.pdf", { type: "application/pdf" });
761+const editedFile = new File(["edited"], "edited.pdf", { type: "application/pdf" });
762+const originalAttachment = registerChatAttachmentPayload({
763+attachment: {
764+id: "original-att",
765+mimeType: "application/pdf",
766+fileName: "original.pdf",
767+sizeBytes: originalFile.size,
768+},
769+dataUrl: "data:application/pdf;base64,b3JpZ2luYWw=",
770+file: originalFile,
771+});
772+const editedAttachment = registerChatAttachmentPayload({
773+attachment: {
774+id: "edited-att",
775+mimeType: "application/pdf",
776+fileName: "edited.pdf",
777+sizeBytes: editedFile.size,
778+},
779+dataUrl: "data:application/pdf;base64,ZWRpdGVk",
780+file: editedFile,
781+});
782+const host = makeHost({
783+client: { request } as unknown as ChatHost["client"],
784+chatAttachments: [originalAttachment],
785+chatMessage: "send this",
786+chatModelSwitchPromises: { "agent:main": switchUpdate.promise },
787+});
788+789+const send = handleSendChat(host);
790+await Promise.resolve();
791+host.chatAttachments = [editedAttachment];
792+793+switchUpdate.resolve(true);
794+await send;
795+796+expect(request).toHaveBeenCalledWith(
797+"chat.send",
798+expect.objectContaining({
799+attachments: [
800+expect.objectContaining({
801+content: "b3JpZ2luYWw=",
802+fileName: "original.pdf",
803+mimeType: "application/pdf",
804+type: "file",
805+}),
806+],
807+message: "send this",
808+}),
809+);
810+expect(host.chatMessage).toBe("send this");
811+expect(host.chatAttachments).toEqual([editedAttachment]);
812+expect(getChatAttachmentDataUrl(originalAttachment)).toBeNull();
813+expect(getChatAttachmentDataUrl(editedAttachment)).toBe("data:application/pdf;base64,ZWRpdGVk");
814+});
815+816+it("sends snapshotted attachment payloads when the composer removes them during a wait", async () => {
817+const switchUpdate = createDeferred<boolean>();
818+const request = vi.fn(async (method: string) => {
819+if (method === "chat.send") {
820+return { status: "started" };
821+}
822+throw new Error(`Unexpected request: ${method}`);
823+});
824+const file = new File(["original"], "original.pdf", { type: "application/pdf" });
825+const attachment = registerChatAttachmentPayload({
826+attachment: {
827+id: "removed-att",
828+mimeType: "application/pdf",
829+fileName: "original.pdf",
830+sizeBytes: file.size,
831+},
832+dataUrl: "data:application/pdf;base64,b3JpZ2luYWw=",
833+ file,
834+});
835+const host = makeHost({
836+client: { request } as unknown as ChatHost["client"],
837+chatAttachments: [attachment],
838+chatMessage: "send this",
839+chatModelSwitchPromises: { "agent:main": switchUpdate.promise },
840+});
841+842+const send = handleSendChat(host);
843+await Promise.resolve();
844+host.chatAttachments = [];
845+releaseChatAttachmentPayloads([attachment]);
846+847+switchUpdate.resolve(true);
848+await send;
849+850+expect(request).toHaveBeenCalledWith(
851+"chat.send",
852+expect.objectContaining({
853+attachments: [
854+expect.objectContaining({
855+content: "b3JpZ2luYWw=",
856+fileName: "original.pdf",
857+mimeType: "application/pdf",
858+type: "file",
859+}),
860+],
861+message: "send this",
862+}),
863+);
864+expect(host.chatMessage).toBe("send this");
865+expect(host.chatAttachments).toEqual([]);
866+expect(getChatAttachmentDataUrl(attachment)).toBeNull();
867+});
868+869+it("does not wait on model picker updates from another session", async () => {
870+const otherSessionSwitch = createDeferred<boolean>();
871+const request = vi.fn(async (method: string) => {
872+if (method === "chat.send") {
873+return { status: "started" };
874+}
875+throw new Error(`Unexpected request: ${method}`);
876+});
877+const host = makeHost({
878+client: { request } as unknown as ChatHost["client"],
879+sessionKey: "agent:other",
880+chatMessage: "send in other session",
881+chatModelSwitchPromises: { "agent:main": otherSessionSwitch.promise },
882+});
883+884+await handleSendChat(host);
885+886+expect(request).toHaveBeenCalledWith(
887+"chat.send",
888+expect.objectContaining({
889+sessionKey: "agent:other",
890+message: "send in other session",
891+}),
892+);
893+otherSessionSwitch.resolve(false);
894+});
895+896+it("keeps the draft when a pending model picker update fails", async () => {
897+const switchUpdate = createDeferred<boolean>();
898+const request = vi.fn(async (method: string) => {
899+throw new Error(`Unexpected request: ${method}`);
900+});
901+const host = makeHost({
902+client: { request } as unknown as ChatHost["client"],
903+chatMessage: "do not send on rollback",
904+chatModelSwitchPromises: { "agent:main": switchUpdate.promise },
905+});
906+907+const send = handleSendChat(host);
908+await Promise.resolve();
909+switchUpdate.resolve(false);
910+await send;
911+912+expect(request).not.toHaveBeenCalled();
913+expect(host.chatMessage).toBe("do not send on rollback");
914+});
915+634916it("keeps slash-command model changes in sync with the chat header cache", async () => {
635917vi.stubGlobal(
636918"fetch",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。