FN-7864: add inline artifact preview/link to artifact-registered mail messages
Artifact-registration mailbox notifications now render a shared inline preview and open-artifact link instead of plain text metadata. - Add MailboxArtifactAttachment component rendering an inline image/document preview plus an "open artifact" link from message.metadata (artifactId/artifactType/mimeType) via artifactMediaUrl - Wire MailboxModal and MailboxView to render the new attachment for artifact-registered messages, with supporting CSS - Emit metadata.mimeType from notifyArtifactRegistered in agent-tools.ts so mailbox surfaces can pick the right preview affordance without an extra artifact fetch - Add/extend tests for the new component and for MailboxView/agent-artifact-tools coverage - Update dashboard guide docs and add a changeset for the feature Files changed: .changeset/fn-7864-artifact-mail-link.md | 7 ++ docs/dashboard-guide.md | 2 +- .../app/components/MailboxArtifactAttachment.tsx | 103 +++++++++++++++++++++ packages/dashboard/app/components/MailboxModal.css | 74 +++++++++++++++ packages/dashboard/app/components/MailboxModal.tsx | 15 +++ packages/dashboard/app/components/MailboxView.tsx | 15 +++ .../__tests__/MailboxArtifactAttachment.test.tsx | 65 +++++++++++++ .../app/components/__tests__/MailboxView.test.tsx | 93 +++++++++++++++++++ .../src/__tests__/agent-artifact-tools.test.ts | 32 ++++++- packages/engine/src/agent-tools.ts | 5 + 10 files changed, 409 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7864 Fusion-Task-Lineage: a6502e18-5f7f-4c67-80fb-a709e4a52c50 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7864-artifact-mail-link.md
Normal file
7
.changeset/fn-7864-artifact-mail-link.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Artifact-registration mail notifications now show an inline preview and open link.
|
||||
category: feature
|
||||
dev: MailboxView/MailboxModal render a shared MailboxArtifactAttachment from message.metadata (artifactId/artifactType/mimeType) via artifactMediaUrl; notifyArtifactRegistered now also emits metadata.mimeType.
|
||||
@@ -613,7 +613,7 @@ Mailbox view shows inbox/outbox communication threads and unread state.
|
||||
- Inbox renders one row per message (no sender-based collapsing)
|
||||
- clicking a message in the Mail tab opens the task detail pane with full message content and conversation context
|
||||
- reply rows in the mailbox modal can expand inline to show the replied-to message context for easier thread reading
|
||||
- when an agent or dashboard chat session registers an artifact with `fn_artifact_register`, Fusion sends a best-effort `system` → user inbox message announcing the new artifact (for example, `New image artifact registered: <title>`) with metadata for `artifactId`, `artifactType`, `title`, `authorId`, and optional `taskId`; notification delivery is informational and never blocks or rolls back the artifact registration
|
||||
- when an agent or dashboard chat session registers an artifact with `fn_artifact_register`, Fusion sends a best-effort `system` → user inbox message announcing the new artifact (for example, `New image artifact registered: <title>`) with metadata for `artifactId`, `artifactType`, `title`, optional `mimeType`, `authorId`, and optional `taskId`; notification delivery is informational and never blocks or rolls back the artifact registration. Artifact notifications are actionable in message detail views: image artifacts show an inline preview plus **Open artifact**, while video/audio/document/other artifacts show an **Open artifact** link to the managed media URL.
|
||||
- mailbox now includes an **Approvals** tab with pending and history filters (`approved` / `denied` / `completed`), approval detail context, and inline approve/deny actions for pending requests
|
||||
- for approvals gated by an agent's permission policy (permanent agents and task-worker heartbeats), the Approvals detail pane renders the gated action's real payload — tool name, shell command line or structured arguments, and working directory when present — instead of only a generic "Agent gated action for `<tool>`" summary; a stateless heartbeat retrying the same gated command reuses the existing pending approval instead of creating a duplicate (FN-7609)
|
||||
- in the **Agents** tab, the agent selector now includes **All agents**, which shows one combined agent-to-agent stream (with sender + recipient labels); selecting a specific agent still shows Inbox/Outbox subtabs
|
||||
|
||||
103
packages/dashboard/app/components/MailboxArtifactAttachment.tsx
Normal file
103
packages/dashboard/app/components/MailboxArtifactAttachment.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { memo, useMemo, useState, type ReactNode } from "react";
|
||||
import type { ArtifactType } from "@fusion/core";
|
||||
import { artifactMediaUrl } from "../api";
|
||||
|
||||
export interface MailboxArtifactAttachmentProps {
|
||||
artifactId?: unknown;
|
||||
artifactType?: unknown;
|
||||
title?: unknown;
|
||||
mimeType?: unknown;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function readArtifactType(value: unknown): ArtifactType | "unknown" {
|
||||
return value === "image" || value === "video" || value === "audio" || value === "document" || value === "other"
|
||||
? value
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:ArtifactRegistry 2026-07-12-00:00:
|
||||
* Artifact-registration mail messages must expose the artifact announced by message.metadata. Render image artifacts inline, keep every type reachable through artifactMediaUrl(projectId-aware), and render nothing when metadata has no artifactId so ordinary messages keep their exact layout.
|
||||
*/
|
||||
export const MailboxArtifactAttachment = memo(function MailboxArtifactAttachment({
|
||||
artifactId,
|
||||
artifactType,
|
||||
title,
|
||||
mimeType,
|
||||
projectId,
|
||||
}: MailboxArtifactAttachmentProps) {
|
||||
const id = readString(artifactId);
|
||||
const type = readArtifactType(artifactType);
|
||||
const label = readString(title) ?? "artifact";
|
||||
const mediaMimeType = readString(mimeType);
|
||||
const [imageFailed, setImageFailed] = useState(false);
|
||||
const mediaUrl = useMemo(() => id ? artifactMediaUrl(id, projectId) : "", [id, projectId]);
|
||||
|
||||
if (!id) return null;
|
||||
|
||||
const openLink = (
|
||||
<a
|
||||
className="mailbox-artifact-attachment__link btn"
|
||||
href={mediaUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`Open artifact: ${label}`}
|
||||
>
|
||||
Open artifact
|
||||
</a>
|
||||
);
|
||||
|
||||
let preview: ReactNode = null;
|
||||
if (type === "image" && !imageFailed) {
|
||||
preview = (
|
||||
<img
|
||||
className="mailbox-artifact-attachment__media mailbox-artifact-attachment__image"
|
||||
src={mediaUrl}
|
||||
alt={label}
|
||||
loading="lazy"
|
||||
onError={() => setImageFailed(true)}
|
||||
/>
|
||||
);
|
||||
} else if (type === "video") {
|
||||
preview = (
|
||||
<video
|
||||
className="mailbox-artifact-attachment__media"
|
||||
src={mediaUrl}
|
||||
controls
|
||||
aria-label={`Video artifact: ${label}`}
|
||||
/>
|
||||
);
|
||||
} else if (type === "audio") {
|
||||
preview = (
|
||||
<audio
|
||||
className="mailbox-artifact-attachment__audio"
|
||||
src={mediaUrl}
|
||||
controls
|
||||
aria-label={`Audio artifact: ${label}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mailbox-artifact-attachment"
|
||||
data-testid="mailbox-artifact-attachment"
|
||||
data-artifact-type={type}
|
||||
data-artifact-mime-type={mediaMimeType}
|
||||
>
|
||||
<div className="mailbox-artifact-attachment__header">
|
||||
<span className="mailbox-artifact-attachment__title">{label}</span>
|
||||
<span className="mailbox-artifact-attachment__type">{type === "unknown" ? "artifact" : type}</span>
|
||||
</div>
|
||||
{preview}
|
||||
<div className="mailbox-artifact-attachment__actions">
|
||||
{openLink}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -321,6 +321,80 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ArtifactRegistry 2026-07-12-00:00:
|
||||
Artifact-registration mail messages now render metadata-driven media affordances directly below the markdown body. The block must share mailbox spacing/radius tokens and collapse entirely for messages without artifactId metadata.
|
||||
*/
|
||||
.mailbox-artifact-attachment {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
margin-top: var(--space-sm);
|
||||
padding: var(--space-md);
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__header,
|
||||
.mailbox-artifact-attachment__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__title {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__type {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__media,
|
||||
.mailbox-artifact-attachment__image,
|
||||
.mailbox-artifact-attachment__audio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__media,
|
||||
.mailbox-artifact-attachment__image {
|
||||
max-height: min(45vh, 28rem);
|
||||
border-radius: var(--radius-sm);
|
||||
object-fit: contain;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__audio {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mailbox-artifact-attachment {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__header,
|
||||
.mailbox-artifact-attachment__actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mailbox-artifact-attachment__link {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Markdown rendering inside message bodies. ReactMarkdown produces standard
|
||||
block elements (p, ul, h*, code, table, ...). Reset margins so message
|
||||
prose still feels compact, and let pre/table scroll horizontally. */
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "../api";
|
||||
import { MessageComposer } from "./MessageComposer";
|
||||
import { MailboxMessageContent } from "./MailboxMessageContent";
|
||||
import { MailboxArtifactAttachment } from "./MailboxArtifactAttachment";
|
||||
import type { Agent } from "../api";
|
||||
import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
|
||||
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
||||
@@ -888,6 +889,13 @@ export function MailboxModal({
|
||||
content={msg.content}
|
||||
className="mailbox-conversation-msg-body"
|
||||
/>
|
||||
<MailboxArtifactAttachment
|
||||
artifactId={msg.metadata?.artifactId}
|
||||
artifactType={msg.metadata?.artifactType}
|
||||
title={msg.metadata?.title}
|
||||
mimeType={msg.metadata?.mimeType}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -910,6 +918,13 @@ export function MailboxModal({
|
||||
className="mailbox-message-body"
|
||||
testId="mailbox-message-body"
|
||||
/>
|
||||
<MailboxArtifactAttachment
|
||||
artifactId={selectedMessage.metadata?.artifactId}
|
||||
artifactType={selectedMessage.metadata?.artifactType}
|
||||
title={selectedMessage.metadata?.title}
|
||||
mimeType={selectedMessage.metadata?.mimeType}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type ApprovalRequestDetail,
|
||||
} from "../api";
|
||||
import { MailboxMessageContent } from "./MailboxMessageContent";
|
||||
import { MailboxArtifactAttachment } from "./MailboxArtifactAttachment";
|
||||
import { MessageComposer } from "./MessageComposer";
|
||||
import { ViewHeader } from "./ViewHeader";
|
||||
import { WorktrunkInstallApprovalDetails } from "./WorktrunkInstallApprovalDetails";
|
||||
@@ -873,6 +874,13 @@ export function MailboxView({
|
||||
content={msg.content}
|
||||
className="mailbox-conversation-msg-body"
|
||||
/>
|
||||
<MailboxArtifactAttachment
|
||||
artifactId={msg.metadata?.artifactId}
|
||||
artifactType={msg.metadata?.artifactType}
|
||||
title={msg.metadata?.title}
|
||||
mimeType={msg.metadata?.mimeType}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -890,6 +898,13 @@ export function MailboxView({
|
||||
className="mailbox-message-body"
|
||||
testId="mailbox-message-body"
|
||||
/>
|
||||
<MailboxArtifactAttachment
|
||||
artifactId={selectedMessage.metadata?.artifactId}
|
||||
artifactType={selectedMessage.metadata?.artifactType}
|
||||
title={selectedMessage.metadata?.title}
|
||||
mimeType={selectedMessage.metadata?.mimeType}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { MailboxArtifactAttachment } from "../MailboxArtifactAttachment";
|
||||
import { artifactMediaUrl } from "../../api";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
artifactMediaUrl: vi.fn((id: string, projectId?: string) => `/api/artifacts/${id}/media${projectId ? `?projectId=${projectId}` : ""}`),
|
||||
}));
|
||||
|
||||
const mockArtifactMediaUrl = vi.mocked(artifactMediaUrl);
|
||||
|
||||
describe("MailboxArtifactAttachment", () => {
|
||||
it("renders image artifacts inline with the project-scoped media URL", () => {
|
||||
render(
|
||||
<MailboxArtifactAttachment
|
||||
artifactId="art-image"
|
||||
artifactType="image"
|
||||
title="Screenshot"
|
||||
mimeType="image/png"
|
||||
projectId="proj-1"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(mockArtifactMediaUrl).toHaveBeenCalledWith("art-image", "proj-1");
|
||||
const image = screen.getByRole("img", { name: "Screenshot" });
|
||||
expect(image).toHaveAttribute("src", "/api/artifacts/art-image/media?projectId=proj-1");
|
||||
expect(screen.getByRole("link", { name: "Open artifact: Screenshot" })).toHaveAttribute("href", "/api/artifacts/art-image/media?projectId=proj-1");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["document", "Spec"],
|
||||
["other", "Archive"],
|
||||
])("renders an open link for %s artifacts", (artifactType, title) => {
|
||||
render(<MailboxArtifactAttachment artifactId={`art-${artifactType}`} artifactType={artifactType} title={title} />);
|
||||
|
||||
expect(screen.queryByRole("img")).toBeNull();
|
||||
expect(screen.getByRole("link", { name: `Open artifact: ${title}` })).toHaveAttribute("href", `/api/artifacts/art-${artifactType}/media`);
|
||||
});
|
||||
|
||||
it("renders controls media and an open link for video and audio artifacts", () => {
|
||||
const { rerender, container } = render(<MailboxArtifactAttachment artifactId="art-video" artifactType="video" title="Clip" />);
|
||||
expect(container.querySelector("video[controls]")).toHaveAttribute("src", "/api/artifacts/art-video/media");
|
||||
expect(screen.getByRole("link", { name: "Open artifact: Clip" })).toHaveAttribute("href", "/api/artifacts/art-video/media");
|
||||
|
||||
rerender(<MailboxArtifactAttachment artifactId="art-audio" artifactType="audio" title="Recording" />);
|
||||
expect(container.querySelector("audio[controls]")).toHaveAttribute("src", "/api/artifacts/art-audio/media");
|
||||
expect(screen.getByRole("link", { name: "Open artifact: Recording" })).toHaveAttribute("href", "/api/artifacts/art-audio/media");
|
||||
});
|
||||
|
||||
it("renders nothing when artifactId metadata is missing", () => {
|
||||
const { container } = render(<MailboxArtifactAttachment artifactType="image" title="No id" />);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(screen.queryByTestId("mailbox-artifact-attachment")).toBeNull();
|
||||
});
|
||||
|
||||
it("degrades image load failures to the open artifact link", () => {
|
||||
render(<MailboxArtifactAttachment artifactId="art-broken" artifactType="image" title="Broken screenshot" />);
|
||||
|
||||
fireEvent.error(screen.getByRole("img", { name: "Broken screenshot" }));
|
||||
|
||||
expect(screen.queryByRole("img", { name: "Broken screenshot" })).toBeNull();
|
||||
expect(screen.getByRole("link", { name: "Open artifact: Broken screenshot" })).toHaveAttribute("href", "/api/artifacts/art-broken/media");
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ vi.mock("../../api", () => ({
|
||||
fetchApprovals: vi.fn(),
|
||||
fetchApprovalDetail: vi.fn(),
|
||||
decideApproval: vi.fn(),
|
||||
artifactMediaUrl: vi.fn((id: string, projectId?: string) => `/api/artifacts/${id}/media${projectId ? `?projectId=${projectId}` : ""}`),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useViewportMode", () => {
|
||||
@@ -810,6 +811,98 @@ describe("MailboxView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders an inline artifact attachment in the single-message detail path", async () => {
|
||||
const artifactMessage: Message = {
|
||||
...mockMessage,
|
||||
metadata: {
|
||||
artifactId: "art-mailbox-image",
|
||||
artifactType: "image",
|
||||
title: "Mailbox Screenshot",
|
||||
mimeType: "image/png",
|
||||
},
|
||||
};
|
||||
mockFetchInbox.mockResolvedValue(makeInboxResponse([artifactMessage], 1));
|
||||
mockFetchConversation.mockResolvedValue([artifactMessage]);
|
||||
mockMarkMessageRead.mockResolvedValue({ ...artifactMessage, read: true });
|
||||
|
||||
render(<MailboxView {...defaultProps} projectId="project-a" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent(artifactMessage.content);
|
||||
expect(screen.getByTestId("mailbox-artifact-attachment")).toBeInTheDocument();
|
||||
expect(screen.getByRole("img", { name: "Mailbox Screenshot" })).toHaveAttribute("src", "/api/artifacts/art-mailbox-image/media?projectId=project-a");
|
||||
expect(screen.getByRole("link", { name: "Open artifact: Mailbox Screenshot" })).toHaveAttribute("href", "/api/artifacts/art-mailbox-image/media?projectId=project-a");
|
||||
});
|
||||
});
|
||||
|
||||
it("renders no artifact attachment for messages without artifact metadata", async () => {
|
||||
mockFetchInbox.mockResolvedValue(makeInboxResponse([mockMessage], 1));
|
||||
mockFetchConversation.mockResolvedValue([mockMessage]);
|
||||
mockMarkMessageRead.mockResolvedValue({ ...mockMessage, read: true });
|
||||
|
||||
render(<MailboxView {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent(mockMessage.content);
|
||||
expect(screen.queryByTestId("mailbox-artifact-attachment")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders artifact attachments inside conversation thread messages", async () => {
|
||||
const rootMessage: Message = {
|
||||
...mockMessage,
|
||||
id: "msg-artifact-root",
|
||||
content: "Artifact root",
|
||||
};
|
||||
const artifactReply: Message = {
|
||||
...mockMessage,
|
||||
id: "msg-artifact-reply",
|
||||
content: "New image artifact registered: Thread Image",
|
||||
metadata: {
|
||||
replyTo: { messageId: "msg-artifact-root" },
|
||||
artifactId: "art-thread-image",
|
||||
artifactType: "image",
|
||||
title: "Thread Image",
|
||||
},
|
||||
read: true,
|
||||
};
|
||||
mockFetchInbox.mockResolvedValue(makeInboxResponse([rootMessage], 1));
|
||||
mockFetchConversation.mockResolvedValue([rootMessage, artifactReply]);
|
||||
mockMarkMessageRead.mockResolvedValue({ ...rootMessage, read: true });
|
||||
|
||||
render(<MailboxView {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-artifact-root")).toBeDefined();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId("mailbox-item-msg-artifact-root"));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-conversation")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mailbox-artifact-attachment")).toBeInTheDocument();
|
||||
expect(screen.getByRole("img", { name: "Thread Image" })).toHaveAttribute("src", "/api/artifacts/art-thread-image/media");
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps list pane visible alongside detail pane on desktop/tablet", async () => {
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
messages: [mockMessage],
|
||||
|
||||
@@ -254,7 +254,7 @@ describe("artifact register tool", () => {
|
||||
|
||||
it("sends exactly one system-to-user inbox notification with artifact metadata", async () => {
|
||||
const { store, registerArtifact } = createMockStore();
|
||||
const artifact = createMockArtifact({ id: "art-notify", type: "image", title: "Screenshot", uri: "artifacts/screenshot.png", content: undefined });
|
||||
const artifact = createMockArtifact({ id: "art-notify", type: "image", title: "Screenshot", mimeType: "image/png", uri: "artifacts/screenshot.png", content: undefined });
|
||||
registerArtifact.mockResolvedValue(artifact);
|
||||
const { messageStore, sendMessage } = createMockMessageStore();
|
||||
|
||||
@@ -276,12 +276,42 @@ describe("artifact register tool", () => {
|
||||
artifactId: "art-notify",
|
||||
artifactType: "image",
|
||||
title: "Screenshot",
|
||||
mimeType: "image/png",
|
||||
authorId: AUTHOR_ID,
|
||||
taskId: TASK_ID,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("still sends artifact notification metadata when mimeType is absent", async () => {
|
||||
const { store, registerArtifact } = createMockStore();
|
||||
registerArtifact.mockResolvedValue(createMockArtifact({
|
||||
id: "art-no-mime",
|
||||
title: "Metadata-only artifact",
|
||||
mimeType: undefined,
|
||||
content: undefined,
|
||||
uri: "artifact://metadata-only",
|
||||
}));
|
||||
const { messageStore, sendMessage } = createMockMessageStore();
|
||||
|
||||
const tool = createArtifactRegisterTool(store, AUTHOR_ID, messageStore);
|
||||
const result = await runTool(tool, "call-no-mime-notify", {
|
||||
type: "other",
|
||||
title: "Metadata-only artifact",
|
||||
uri: "artifact://metadata-only",
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
artifactId: "art-no-mime",
|
||||
title: "Metadata-only artifact",
|
||||
mimeType: undefined,
|
||||
}),
|
||||
}));
|
||||
expect(getText(result)).toContain("Registered artifact");
|
||||
});
|
||||
|
||||
it("still succeeds when notification sendMessage throws", async () => {
|
||||
const { store, registerArtifact } = createMockStore();
|
||||
registerArtifact.mockResolvedValue(createMockArtifact({ id: "art-best-effort" }));
|
||||
|
||||
@@ -1917,6 +1917,10 @@ function hasImageSignature(data: Buffer, mimeType: string): boolean {
|
||||
function notifyArtifactRegistered(messageStore: MessageStore | undefined, artifact: Artifact, authorId: string): void {
|
||||
if (!messageStore) return;
|
||||
|
||||
/*
|
||||
FNXC:ArtifactRegistry 2026-07-12-00:00:
|
||||
Artifact-registration mailbox notifications remain best-effort and keep their stable content string, but metadata now carries mimeType so dashboard mailbox surfaces can render document/other artifact affordances from metadata without an extra artifact fetch.
|
||||
*/
|
||||
try {
|
||||
messageStore.sendMessage({
|
||||
fromType: "system",
|
||||
@@ -1928,6 +1932,7 @@ function notifyArtifactRegistered(messageStore: MessageStore | undefined, artifa
|
||||
artifactId: artifact.id,
|
||||
artifactType: artifact.type,
|
||||
title: artifact.title,
|
||||
mimeType: artifact.mimeType,
|
||||
authorId,
|
||||
taskId: artifact.taskId,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user