forked from molecule-ai/molecule-core
Merge pull request #2135 from Molecule-AI/fix/chat-user-attachments-hydration
fix(chat): hydrate user-side file attachments on chat reload
This commit is contained in:
commit
9a75c0fcbe
@ -112,6 +112,84 @@ describe("activityRowToMessages", () => {
|
||||
const msgs = activityRowToMessages(row, NEVER_INTERNAL);
|
||||
expect(msgs.find((m) => m.role === "user")).toBeUndefined();
|
||||
});
|
||||
|
||||
// Reviewer follow-up: the pre-fix loader didn't extract user-side
|
||||
// file parts, so a chat reload after a session where the user
|
||||
// dragged in a file showed the text bubble but lost the chip.
|
||||
// Symmetric to the agent attachment hydration below.
|
||||
|
||||
it("hydrates user-side file attachments from request_body.params.message.parts", () => {
|
||||
const row = makeRow({
|
||||
request_body: {
|
||||
params: {
|
||||
message: {
|
||||
parts: [
|
||||
{ kind: "text", text: "here's the screenshot" },
|
||||
{
|
||||
kind: "file",
|
||||
file: {
|
||||
name: "shot.png",
|
||||
mimeType: "image/png",
|
||||
uri: "workspace:/uploads/shot.png",
|
||||
size: 4096,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const msgs = activityRowToMessages(row, NEVER_INTERNAL);
|
||||
const user = msgs.find((m) => m.role === "user")!;
|
||||
expect(user.content).toBe("here's the screenshot");
|
||||
expect(user.attachments).toEqual([
|
||||
{ name: "shot.png", mimeType: "image/png", uri: "workspace:/uploads/shot.png", size: 4096 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits an attachments-only user bubble when text is empty (drag-drop without caption)", () => {
|
||||
// Some users drop a file with no message — the bubble should
|
||||
// still render so the file appears in history. Pre-fix the
|
||||
// empty userText short-circuited and the row was dropped.
|
||||
const row = makeRow({
|
||||
request_body: {
|
||||
params: {
|
||||
message: {
|
||||
parts: [
|
||||
{ kind: "file", file: { name: "report.pdf", uri: "workspace:/uploads/report.pdf" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const msgs = activityRowToMessages(row, NEVER_INTERNAL);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].role).toBe("user");
|
||||
expect(msgs[0].content).toBe("");
|
||||
expect(msgs[0].attachments).toHaveLength(1);
|
||||
expect(msgs[0].attachments![0].name).toBe("report.pdf");
|
||||
});
|
||||
|
||||
it("internal-self predicate suppresses the row even if it carries attachments", () => {
|
||||
// Defence-in-depth: heartbeat self-trigger never produces
|
||||
// attachments, but if a future internal trigger DID, we still
|
||||
// want to suppress (otherwise it'd render as the user attaching
|
||||
// something they never touched).
|
||||
const row = makeRow({
|
||||
request_body: {
|
||||
params: {
|
||||
message: {
|
||||
parts: [
|
||||
{ kind: "text", text: "Delegation results are ready..." },
|
||||
{ kind: "file", file: { name: "x.zip", uri: "workspace:/x.zip" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const msgs = activityRowToMessages(row, (t) => t.startsWith("Delegation results are ready"));
|
||||
expect(msgs.find((m) => m.role === "user")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent-message extraction", () => {
|
||||
|
||||
@ -35,8 +35,26 @@ export function activityRowToMessages(
|
||||
const out: ChatMessage[] = [];
|
||||
|
||||
const userText = extractRequestText(row.request_body);
|
||||
if (userText && !isInternalSelfMessage(userText)) {
|
||||
out.push({ ...createMessage("user", userText), timestamp: row.created_at });
|
||||
// Hydrate user-side file attachments out of the same A2A envelope.
|
||||
// Without this, a chat reload after a session where the user dragged
|
||||
// in a file shows the text bubble but loses the download chip — the
|
||||
// pre-fix loader only walked text via extractRequestText. Mirrors
|
||||
// the agent branch below. Wire shape from ChatTab's outbound POST:
|
||||
// request_body = {params: {message: {parts: [{kind:"text"}, {kind:"file", file:{...}}]}}}
|
||||
// extractFilesFromTask walks `task.parts`, so we feed it `params.message`.
|
||||
const userMsg = (row.request_body?.params as Record<string, unknown> | undefined)
|
||||
?.message as Record<string, unknown> | undefined;
|
||||
const userAttachments = userMsg ? extractFilesFromTask(userMsg) : [];
|
||||
// Internal-self messages (e.g. heartbeat self-trigger) take precedence
|
||||
// — drop the row even if it carries attachments, since the heartbeat
|
||||
// path doesn't produce attachments anyway and keeping the bubble would
|
||||
// misattribute it to the user.
|
||||
const isInternal = !!userText && isInternalSelfMessage(userText);
|
||||
if (!isInternal && (userText || userAttachments.length > 0)) {
|
||||
out.push({
|
||||
...createMessage("user", userText, userAttachments),
|
||||
timestamp: row.created_at,
|
||||
});
|
||||
}
|
||||
|
||||
if (row.response_body) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user