mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ui): catalog continuation silently dropped staged attachments (#123581)
A continuable catalog session exposes the full attachment surface (file inputs, plus-menu, paste), and the send button counts staged attachments as composed content — but continueCatalogSession required a non-empty text draft and always handed off attachments: []. Draft+images sent text only with the images silently vanishing (payloads leaked, never released); images-only made the enabled send button do nothing at all: the worst bug class, an action with no visible outcome and no recorded reason. Carry independently-owned attachment clones through the continuation handoff, mirroring the new-session sibling (chat-pane-session-creation.ts), and treat attachments as composed content in the guard so attachment-only continuations send. Empty draft with no attachments still no-ops.
This commit is contained in:
committed by
GitHub
parent
ee6c2cb50d
commit
37a63fbf45
@@ -79,6 +79,7 @@ function createTestChatPane(params: { client: GatewayBrowserClient; sessions: Se
|
||||
const state = {
|
||||
agentsList: null,
|
||||
assistantAgentId: null,
|
||||
chatAttachments: [],
|
||||
chatError: null,
|
||||
chatHistoryPagination: { hasMore: false },
|
||||
chatLoading: false,
|
||||
@@ -717,6 +718,70 @@ describe("chat pane catalog continuation lifecycle", () => {
|
||||
expect(state.handleSendChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("carries staged attachments through the continuation handoff", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ sessionKey: "agent:main:continued-attachments" });
|
||||
const { key, pane, state } = createCatalogContinuationPane(request);
|
||||
state.chatAttachments = [
|
||||
{
|
||||
id: "att-1",
|
||||
mimeType: "image/png",
|
||||
fileName: "shot.png",
|
||||
dataUrl: "data:image/png;base64,AAAA",
|
||||
},
|
||||
];
|
||||
|
||||
await pane.continueCatalogSession(key);
|
||||
|
||||
const handoff = consumePaneSessionHandoff(
|
||||
pane.context,
|
||||
pane.paneId,
|
||||
"agent:main:continued-attachments",
|
||||
);
|
||||
expect(handoff?.send).toBe(true);
|
||||
expect(handoff?.attachments).toHaveLength(1);
|
||||
expect(handoff?.attachments[0]).toMatchObject({
|
||||
mimeType: "image/png",
|
||||
fileName: "shot.png",
|
||||
dataUrl: "data:image/png;base64,AAAA",
|
||||
});
|
||||
});
|
||||
|
||||
it("continues an attachment-only draft instead of silently ignoring the send", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ sessionKey: "agent:main:attachment-only" });
|
||||
const { key, pane, state } = createCatalogContinuationPane(request);
|
||||
state.chatMessage = "";
|
||||
state.chatAttachments = [
|
||||
{
|
||||
id: "att-2",
|
||||
mimeType: "image/png",
|
||||
fileName: "only.png",
|
||||
dataUrl: "data:image/png;base64,BBBB",
|
||||
},
|
||||
];
|
||||
|
||||
await pane.continueCatalogSession(key);
|
||||
|
||||
expect(request).toHaveBeenCalledWith("sessions.catalog.continue", key);
|
||||
const handoff = consumePaneSessionHandoff(
|
||||
pane.context,
|
||||
pane.paneId,
|
||||
"agent:main:attachment-only",
|
||||
);
|
||||
expect(handoff?.send).toBe(true);
|
||||
expect(handoff?.attachments).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("still ignores a continuation with no draft and no attachments", async () => {
|
||||
const request = vi.fn();
|
||||
const { key, pane, state } = createCatalogContinuationPane(request);
|
||||
state.chatMessage = " ";
|
||||
state.chatAttachments = [];
|
||||
|
||||
await pane.continueCatalogSession(key);
|
||||
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not stage or send a continuation rejected by its logical pane", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ sessionKey: "agent:main:rejected-continuation" });
|
||||
const { key, pane, state } = createCatalogContinuationPane(request);
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
areUiSessionKeysEquivalent,
|
||||
parseAgentSessionKey,
|
||||
} from "../../lib/sessions/session-key.ts";
|
||||
import { replaceChatAttachmentsFromEditor } from "./attachment-payload-store.ts";
|
||||
import {
|
||||
cloneChatAttachmentsForIndependentOwner,
|
||||
replaceChatAttachmentsFromEditor,
|
||||
} from "./attachment-payload-store.ts";
|
||||
import type { ChatHistoryPagination } from "./chat-history-pagination.ts";
|
||||
import {
|
||||
loadChatHistory,
|
||||
@@ -535,8 +538,14 @@ export abstract class ChatPaneHistory extends ChatPaneSession {
|
||||
const scope = this.captureConnectionScope();
|
||||
const state = scope?.state;
|
||||
const client = scope?.client;
|
||||
const draft = state?.chatMessage.trim();
|
||||
if (!scope || !state || !client || !draft || !this.catalogSession?.canContinue) {
|
||||
const draft = state?.chatMessage.trim() ?? "";
|
||||
// Attachments count as composed content: an image-only continuation must
|
||||
// send, not silently no-op while the send button looks live.
|
||||
if (!scope || !state || !client || !this.catalogSession?.canContinue) {
|
||||
return;
|
||||
}
|
||||
const attachments = cloneChatAttachmentsForIndependentOwner(state.chatAttachments);
|
||||
if (!draft && attachments.length === 0) {
|
||||
return;
|
||||
}
|
||||
const sourceSessionKey = state.sessionKey;
|
||||
@@ -573,7 +582,7 @@ export abstract class ChatPaneHistory extends ChatPaneSession {
|
||||
return;
|
||||
}
|
||||
preparePaneSessionHandoff(this.context, this.paneId, result.sessionKey, {
|
||||
attachments: [],
|
||||
attachments,
|
||||
draft,
|
||||
send: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user