fix: prevent closed pane attachment restaging

This commit is contained in:
Shakker
2026-08-10 12:53:31 +02:00
parent fb9b1aba9e
commit 6eded1bbce
2 changed files with 57 additions and 1 deletions
@@ -5,6 +5,7 @@ import type { GatewayBrowserClient } from "../../api/gateway.ts";
import { createChatAttachmentHandoff } from "../../app/chat-attachment-handoff.ts";
import type { ApplicationContext } from "../../app/context.ts";
import type { ChatAttachment } from "../../lib/chat/chat-types.ts";
import type { SessionCapability } from "../../lib/sessions/index.ts";
import {
getChatAttachmentDataUrl,
registerChatAttachmentPayload,
@@ -15,7 +16,9 @@ import {
preparePaneStagedAttachments,
restorePaneStagedAttachments,
} from "./chat-pane-attachment-handoff.ts";
import { createTestChatPane } from "./chat-pane.test-support.ts";
import type { ChatPageHost } from "./chat-state-host.ts";
import { resolveStoredChatOutboxScope, storedChatOutboxScopeKey } from "./composer-persistence.ts";
import type { ChatSplitLayout } from "./split-layout.ts";
function storedAttachment(id: string, mimeType = "image/png"): ChatAttachment {
@@ -68,6 +71,54 @@ describe("staged chat attachment pane handoff", () => {
expect(calls).toEqual(["discard", "clear"]);
});
it("does not restage a closed pane when its id is reused after disconnect", () => {
const owner = {} as GatewayBrowserClient;
const { pane, state: current } = createTestChatPane({
client: owner,
sessions: {} as SessionCapability,
});
pane.paneId = "p2";
const fallback = storedAttachment("closed-fallback");
current.chatComposerFallbackByScope = {
fallback: {
attachments: [fallback],
message: "closed pane draft",
sequence: 1,
storageFailed: false,
},
};
const root = { querySelectorAll: () => [pane] } as unknown as ParentNode;
const layout = {
columns: [
{
id: "c1",
panes: [
{ id: "p1", sessionKey: "one" },
{ id: "p2", sessionKey: current.sessionKey },
],
paneWeights: [1, 1],
},
],
columnWeights: [1],
activePaneId: "p2",
} satisfies ChatSplitLayout;
const scopeKey = storedChatOutboxScopeKey(
resolveStoredChatOutboxScope(current, current.sessionKey),
);
closePaneStagedAttachments(pane.context, root, layout, pane.paneId);
pane.disconnectedCallback();
expect(getChatAttachmentDataUrl(fallback)).toBeNull();
expect(
pane.context.chatAttachmentHandoff.consume({
owner,
paneId: "p2",
scopeKey,
}),
).toBeNull();
});
it("deduplicates current and fallback payload release", () => {
const shared = storedAttachment("shared");
const fallback = storedAttachment("fallback", "application/pdf");
+6 -1
View File
@@ -72,8 +72,12 @@ const COMPOSER_PREFILL_ATTENTION_CLASS = "agent-chat__input--prefill-attention";
export abstract class ChatPaneLifecycle extends ChatPaneBoard {
private stagedAttachmentGatewayOwner: ChatAttachmentGatewayOwner = null;
private suppressStagedAttachmentHandoffOnDisconnect = false;
public discardStagedAttachments(): void {
// Explicit pane disposal is terminal. The DOM disconnect that follows must
// not recreate an empty fallback handoff under a later reused pane id.
this.suppressStagedAttachmentHandoffOnDisconnect = true;
discardStateStagedAttachments(this.state);
}
@@ -438,6 +442,7 @@ export abstract class ChatPaneLifecycle extends ChatPaneBoard {
override connectedCallback() {
this.boardProviderLifecycleConnected = true;
this.suppressStagedAttachmentHandoffOnDisconnect = false;
super.connectedCallback();
const mountGatewayOwner = this.context.gateway.snapshot.client;
this.stagedAttachmentGatewayOwner = mountGatewayOwner;
@@ -665,7 +670,7 @@ export abstract class ChatPaneLifecycle extends ChatPaneBoard {
}
override disconnectedCallback() {
if (this.state) {
if (this.state && !this.suppressStagedAttachmentHandoffOnDisconnect) {
preparePaneStagedAttachments(
this.context,
this.paneId,