FN-6907: rename task documents to artifacts

Rename the task-modal Documents tab to Artifacts and include task-scoped media outputs alongside task documents.

- Add shared artifact media rendering for global and task-local artifact cards.
- Surface image, video, audio, document, and generic artifacts in the task Artifacts tab.
- Update tab labels, empty states, counts, translations, tests, docs, and release notes for the Artifacts wording.

Files changed:
 .changeset/fn-6907-task-artifacts-tab.md           |   5 +
 docs/dashboard-guide.md                            |   5 +-
 .../dashboard/app/components/ArtifactMedia.tsx     |  55 +++++++
 .../dashboard/app/components/DocumentsView.tsx     |  47 +-----
 .../dashboard/app/components/TaskDetailModal.tsx   |   3 +-
 .../dashboard/app/components/TaskDocumentsTab.css  |  33 +++++
 .../dashboard/app/components/TaskDocumentsTab.tsx  | 109 ++++++++++++--
 .../TaskDetailModal.attachments-and-tabs.test.tsx  |   4 +-
 .../TaskDetailModal.definition-actions.test.tsx    |  20 +--
 .../components/__tests__/TaskDocumentsTab.test.tsx | 160 ++++++++++++++++++++-
 packages/i18n/locales/en/app.json                  |  16 ++-
 packages/i18n/locales/es/app.json                  |  16 ++-
 packages/i18n/locales/fr/app.json                  |  16 ++-
 packages/i18n/locales/ko/app.json                  |  16 ++-
 packages/i18n/locales/zh-CN/app.json               |  16 ++-
 packages/i18n/locales/zh-TW/app.json               |  16 ++-
 16 files changed, 436 insertions(+), 101 deletions(-)

Fusion-Task-Id: FN-6907

Fusion-Task-Lineage: a1318ed5-4d47-42f8-8b6c-fe91506e11fb
This commit is contained in:
gsxdsm
2026-06-21 23:07:55 -07:00
parent f4d2fa2ece
commit 7fd14eb88c
16 changed files with 437 additions and 102 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Rename the task detail Documents tab to Artifacts and add a task-scoped media artifact gallery alongside existing task documents.

View File

@@ -487,7 +487,7 @@ Features:
- Open project markdown files with inline preview - Open project markdown files with inline preview
- Browse the **Artifacts** tab for media registered by agents, users, or the system across tasks - Browse the **Artifacts** tab for media registered by agents, users, or the system across tasks
- Preview artifact images inline, play video and audio with native controls, read document previews, and open generic artifacts through their media URL - Preview artifact images inline, play video and audio with native controls, read document previews, and open generic artifacts through their media URL
- Jump directly from a document group or artifact card to the owning task detail modal when a task is linked - Jump directly from a document group or artifact card to the owning task detail modal when a task is linked; inside task detail, the **Artifacts** tab shows that task's documents and registered media artifacts together
- Toggle between raw text and rendered markdown using the **Markdown/Plain** button - Toggle between raw text and rendered markdown using the **Markdown/Plain** button
- Highlight text in raw or rendered project-file previews, choose **Add comment**, and send the file path, selected snippet, and your comment to the **New Task** dialog - Highlight text in raw or rendered project-file previews, choose **Add comment**, and send the file path, selected snippet, and your comment to the **New Task** dialog
@@ -812,7 +812,7 @@ For related global/project configuration behavior, see [Settings reference](./se
## Task Detail Modal ## Task Detail Modal
Inspect task definition, logs, review feedback, comments, documents, workflow outcomes, model overrides, and task routing from a single modal. Inspect task definition, logs, review feedback, comments, artifacts, workflow outcomes, model overrides, and task routing from a single modal.
- Editable tasks with descriptions show **Summarize as title** beside the read-mode title; it asks AI to generate a concise title from the description and saves it without opening the edit form. - Editable tasks with descriptions show **Summarize as title** beside the read-mode title; it asks AI to generate a concise title from the description and saves it without opening the edit form.
- The **Chat** tab includes an expand/collapse control that lets the transcript and composer fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed. - The **Chat** tab includes an expand/collapse control that lets the transcript and composer fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed.
@@ -831,6 +831,7 @@ Inspect task definition, logs, review feedback, comments, documents, workflow ou
- The **Create Pull Request** modal now offers in-app remediation for every blocking preflight check. If `branchOnRemote` is false, use **Push branch to remote** and Fusion will publish `fusion/<task-id-lower>` to `origin` and refresh preflight. If `conflictsWithBase` is true, use **Resolve conflicts with AI** and Fusion will use an AI coding agent to resolve merge markers on the task branch, commit and push real merge changes, or report success without an empty commit when the selected base is already merged; preflight then refreshes so normal PR creation can continue once all checks pass. - The **Create Pull Request** modal now offers in-app remediation for every blocking preflight check. If `branchOnRemote` is false, use **Push branch to remote** and Fusion will publish `fusion/<task-id-lower>` to `origin` and refresh preflight. If `conflictsWithBase` is true, use **Resolve conflicts with AI** and Fusion will use an AI coding agent to resolve merge markers on the task branch, commit and push real merge changes, or report success without an empty commit when the selected base is already merged; preflight then refreshes so normal PR creation can continue once all checks pass.
- The modal shell renders immediately: preflight checks and PR options load independently of AI-generated title/body metadata, so slow AI suggestions no longer block base-branch selection, diagnostics, or manual PR authoring. - The modal shell renders immediately: preflight checks and PR options load independently of AI-generated title/body metadata, so slow AI suggestions no longer block base-branch selection, diagnostics, or manual PR authoring.
- AI title/body generation is bounded to 60 seconds and is canceled if the dialog request disconnects; on timeout/cancel, Fusion falls back to deterministic task-based PR title/body content instead of leaving the spinner stuck forever. - AI title/body generation is bounded to 60 seconds and is canceled if the dialog request disconnects; on timeout/cancel, Fusion falls back to deterministic task-based PR title/body content instead of leaving the spinner stuck forever.
- The **Artifacts** tab combines task documents written by agents or users with task-scoped registered media artifacts. Images preview inline, video and audio use native controls, document artifacts show text previews, and generic artifacts open through their media URL.
- The **Review** tab is separate from **Comments**: Review shows actionable PR/reviewer feedback and same-task revision controls, while Comments remains the general collaboration thread. - The **Review** tab is separate from **Comments**: Review shows actionable PR/reviewer feedback and same-task revision controls, while Comments remains the general collaboration thread.
- **Request revision** in Review resumes work on the same task ID (no refinement task): `in-progress` tasks get steering injection, while `in-review` tasks are moved back to `in-progress` for the same branch/worktree revision pass. - **Request revision** in Review resumes work on the same task ID (no refinement task): `in-progress` tasks get steering injection, while `in-review` tasks are moved back to `in-progress` for the same branch/worktree revision pass.
- Review supports a manual **Refresh** action in-place: PR mode pulls latest GitHub review state/decision, while direct mode rehydrates reviewer-agent feedback from persisted task data (no GitHub call). - Review supports a manual **Refresh** action in-place: PR mode pulls latest GitHub review state/decision, while direct mode rehydrates reviewer-agent feedback from persisted task data (no GitHub call).

View File

@@ -0,0 +1,55 @@
import { FileText, Package } from "lucide-react";
import type { TFunction } from "i18next";
import type { ArtifactType, ArtifactWithTask } from "@fusion/core";
export function getArtifactTypeLabel(t: TFunction<"app">, type: ArtifactType): string {
switch (type) {
case "image":
return t("documents.artifactTypeImage", "Image");
case "video":
return t("documents.artifactTypeVideo", "Video");
case "audio":
return t("documents.artifactTypeAudio", "Audio");
case "document":
return t("documents.artifactTypeDocument", "Document");
case "other":
return t("documents.artifactTypeOther", "Other");
}
}
interface ArtifactMediaProps {
artifact: Pick<ArtifactWithTask, "type">;
mediaUrl: string;
title: string;
preview?: string;
t: TFunction<"app">;
}
/**
* FNXC:ArtifactRegistry 2026-06-21-21:31:
* The global Documents gallery and the per-task Artifacts tab must share one media renderer so image, video, audio, document, and generic artifact previews cannot drift across dashboard surfaces.
*/
export function ArtifactMedia({ artifact, mediaUrl, title, preview, t }: ArtifactMediaProps) {
switch (artifact.type) {
case "image":
return <img className="documents-artifact-media" src={mediaUrl} alt={title} loading="lazy" />;
case "video":
return <video className="documents-artifact-media" controls src={mediaUrl} aria-label={t("documents.artifactVideoLabel", "Video artifact: {{title}}", { title })} />;
case "audio":
return <audio className="documents-artifact-audio" controls src={mediaUrl} aria-label={t("documents.artifactAudioLabel", "Audio artifact: {{title}}", { title })} />;
case "document":
return (
<div className="documents-artifact-document" data-testid="artifact-document-preview">
<FileText size={16} />
<p>{preview || t("documents.noArtifactPreview", "No preview available.")}</p>
</div>
);
case "other":
return (
<a className="documents-artifact-generic" href={mediaUrl} target="_blank" rel="noreferrer" data-testid="artifact-other-link">
<Package size={16} />
{t("documents.openArtifactMedia", "Open artifact media")}
</a>
);
}
}

View File

@@ -1,10 +1,10 @@
import "./DocumentsView.css"; import "./DocumentsView.css";
import { useState, useMemo, useCallback, useEffect, useRef, type ChangeEvent } from "react"; import { useState, useMemo, useCallback, useEffect, useRef, type ChangeEvent } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ArrowLeft, FileText, ChevronDown, ChevronUp, ChevronRight, RefreshCw, Search, X, Eye, EyeOff, Package } from "lucide-react"; import { ArrowLeft, FileText, ChevronDown, ChevronUp, ChevronRight, RefreshCw, Search, X, Eye, EyeOff } from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import type { ArtifactType, ArtifactWithTask, TaskDocumentWithTask, TaskDetail } from "@fusion/core"; import type { ArtifactWithTask, TaskDocumentWithTask, TaskDetail } from "@fusion/core";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { artifactMediaUrl, fetchTaskDetail, fetchWorkspaceFileContent, type MarkdownFileEntry } from "../api"; import { artifactMediaUrl, fetchTaskDetail, fetchWorkspaceFileContent, type MarkdownFileEntry } from "../api";
import { useArtifacts } from "../hooks/useArtifacts"; import { useArtifacts } from "../hooks/useArtifacts";
@@ -13,6 +13,7 @@ import { useProjectMarkdownFiles } from "../hooks/useProjectMarkdownFiles";
import { useSelectionComment } from "../hooks/useSelectionComment"; import { useSelectionComment } from "../hooks/useSelectionComment";
import { SelectionCommentPopover } from "./SelectionCommentPopover"; import { SelectionCommentPopover } from "./SelectionCommentPopover";
import { LoadingSpinner } from "./LoadingSpinner"; import { LoadingSpinner } from "./LoadingSpinner";
import { ArtifactMedia, getArtifactTypeLabel } from "./ArtifactMedia";
const MOBILE_BREAKPOINT = 768; const MOBILE_BREAKPOINT = 768;
@@ -183,21 +184,6 @@ function TaskGroup({ taskId, taskTitle, documents, onOpenTask, renderMarkdownSta
); );
} }
function getArtifactTypeLabel(t: ReturnType<typeof useTranslation<"app">>["t"], type: ArtifactType): string {
switch (type) {
case "image":
return t("documents.artifactTypeImage", "Image");
case "video":
return t("documents.artifactTypeVideo", "Video");
case "audio":
return t("documents.artifactTypeAudio", "Audio");
case "document":
return t("documents.artifactTypeDocument", "Document");
case "other":
return t("documents.artifactTypeOther", "Other");
}
}
function ArtifactCard({ artifact, projectId, onOpenTask }: ArtifactCardProps) { function ArtifactCard({ artifact, projectId, onOpenTask }: ArtifactCardProps) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const mediaUrl = artifactMediaUrl(artifact.id, projectId); const mediaUrl = artifactMediaUrl(artifact.id, projectId);
@@ -205,35 +191,10 @@ function ArtifactCard({ artifact, projectId, onOpenTask }: ArtifactCardProps) {
const preview = artifact.content ? getContentPreview(artifact.content, 320) : artifact.description; const preview = artifact.content ? getContentPreview(artifact.content, 320) : artifact.description;
const title = artifact.title || t("documents.untitledArtifact", "Untitled artifact"); const title = artifact.title || t("documents.untitledArtifact", "Untitled artifact");
const media = (() => {
switch (artifact.type) {
case "image":
return <img className="documents-artifact-media" src={mediaUrl} alt={title} loading="lazy" />;
case "video":
return <video className="documents-artifact-media" controls src={mediaUrl} aria-label={t("documents.artifactVideoLabel", "Video artifact: {{title}}", { title })} />;
case "audio":
return <audio className="documents-artifact-audio" controls src={mediaUrl} aria-label={t("documents.artifactAudioLabel", "Audio artifact: {{title}}", { title })} />;
case "document":
return (
<div className="documents-artifact-document" data-testid="artifact-document-preview">
<FileText size={16} />
<p>{preview || t("documents.noArtifactPreview", "No preview available.")}</p>
</div>
);
case "other":
return (
<a className="documents-artifact-generic" href={mediaUrl} target="_blank" rel="noreferrer" data-testid="artifact-other-link">
<Package size={16} />
{t("documents.openArtifactMedia", "Open artifact media")}
</a>
);
}
})();
return ( return (
<article className="document-card documents-artifact-card" aria-label={t("documents.artifactCardLabel", "Artifact {{title}}", { title })}> <article className="document-card documents-artifact-card" aria-label={t("documents.artifactCardLabel", "Artifact {{title}}", { title })}>
<div className="documents-artifact-preview"> <div className="documents-artifact-preview">
{media} <ArtifactMedia artifact={artifact} mediaUrl={mediaUrl} title={title} preview={preview} t={t} />
</div> </div>
<div className="documents-artifact-body"> <div className="documents-artifact-body">
<div className="documents-artifact-header"> <div className="documents-artifact-header">

View File

@@ -3141,7 +3141,8 @@ export function TaskDetailContent({
className={`detail-tab${activeTab === "documents" ? " detail-tab-active" : ""}`} className={`detail-tab${activeTab === "documents" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("documents")} onClick={() => setActiveTab("documents")}
> >
{t("taskDetail.tabs.documents", "Documents")} {/* FNXC:ArtifactRegistry 2026-06-21-21:56: Keep the internal "documents" tab id stable for persisted task-modal state while presenting the expanded user-facing tab as Artifacts. */}
{t("taskDetail.tabs.documents", "Artifacts")}
</button> </button>
<button <button
className={`detail-tab${activeTab === "model" ? " detail-tab-active" : ""}`} className={`detail-tab${activeTab === "model" ? " detail-tab-active" : ""}`}

View File

@@ -1,4 +1,32 @@
/* === Task Documents === */ /* === Task Documents === */
.task-artifacts-section,
.task-documents-section {
display: flex;
flex-direction: column;
gap: var(--space-md);
margin-top: var(--space-lg);
}
.task-artifacts-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
}
.task-artifacts-section-header h5 {
margin: 0;
}
.task-artifacts-section-count {
color: var(--text-muted);
font-size: 0.75rem;
}
.task-artifacts-gallery {
width: 100%;
}
.task-documents-list { .task-documents-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -225,6 +253,11 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.task-artifacts-section-header {
align-items: flex-start;
flex-direction: column;
}
.task-document-card-header { .task-document-card-header {
flex-direction: column; flex-direction: column;
gap: var(--space-sm); gap: var(--space-sm);

View File

@@ -1,10 +1,11 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FileText, ChevronDown, ChevronUp, Plus, Trash2, History } from "lucide-react"; import { FileText, ChevronDown, ChevronUp, Plus, Trash2, History } from "lucide-react";
import "./DocumentsView.css";
import "./TaskDocumentsTab.css"; import "./TaskDocumentsTab.css";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import type { Task, TaskDocument, TaskDocumentRevision } from "@fusion/core"; import type { ArtifactWithTask, Task, TaskDocument, TaskDocumentRevision } from "@fusion/core";
import { getErrorMessage } from "@fusion/core"; import { getErrorMessage } from "@fusion/core";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { import {
@@ -12,8 +13,11 @@ import {
fetchTaskDocumentRevisions, fetchTaskDocumentRevisions,
putTaskDocument, putTaskDocument,
deleteTaskDocument, deleteTaskDocument,
artifactMediaUrl,
} from "../api"; } from "../api";
import { useArtifacts } from "../hooks/useArtifacts";
import { LoadingSpinner } from "./LoadingSpinner"; import { LoadingSpinner } from "./LoadingSpinner";
import { ArtifactMedia, getArtifactTypeLabel } from "./ArtifactMedia";
// Document key validation: alphanumeric, hyphens, underscores, 1-64 chars // Document key validation: alphanumeric, hyphens, underscores, 1-64 chars
const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/; const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
@@ -37,6 +41,51 @@ function getContentPreview(content: string, maxLength: number = MAX_CONTENT_PREV
return content.substring(0, maxLength) + "…"; return content.substring(0, maxLength) + "…";
} }
function formatFileSize(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(bytes >= 10 * 1024 ? 0 : 1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
interface TaskArtifactCardProps {
artifact: ArtifactWithTask;
projectId?: string;
}
function TaskArtifactCard({ artifact, projectId }: TaskArtifactCardProps) {
const { t } = useTranslation("app");
const mediaUrl = artifactMediaUrl(artifact.id, projectId);
const typeLabel = getArtifactTypeLabel(t, artifact.type);
const preview = artifact.content ? getContentPreview(artifact.content, 320) : artifact.description;
const title = artifact.title || t("documents.untitledArtifact", "Untitled artifact");
return (
<article className="document-card documents-artifact-card" aria-label={t("documents.artifactCardLabel", "Artifact {{title}}", { title })}>
<div className="documents-artifact-preview">
<ArtifactMedia artifact={artifact} mediaUrl={mediaUrl} title={title} preview={preview} t={t} />
</div>
<div className="documents-artifact-body">
<div className="documents-artifact-header">
<span className="documents-artifact-type-badge">{typeLabel}</span>
<span className="documents-artifact-author">{artifact.authorId}</span>
</div>
<h5 className="documents-artifact-title">{title}</h5>
{artifact.description && <p className="documents-artifact-description">{artifact.description}</p>}
<div className="documents-artifact-meta">
<span>{formatTimestamp(artifact.createdAt)}</span>
{artifact.sizeBytes !== undefined && <span>{formatFileSize(artifact.sizeBytes)}</span>}
</div>
</div>
</article>
);
}
export function TaskDocumentsTab({ export function TaskDocumentsTab({
taskId, taskId,
addToast, addToast,
@@ -61,6 +110,7 @@ export function TaskDocumentsTab({
const [deletingKey, setDeletingKey] = useState<string | null>(null); const [deletingKey, setDeletingKey] = useState<string | null>(null);
const [confirmDelete, setConfirmDelete] = useState<string | null>(null); const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
const [renderMarkdown, setRenderMarkdown] = useState(false); const [renderMarkdown, setRenderMarkdown] = useState(false);
const { artifacts, loading: artifactsLoading, error: artifactsError } = useArtifacts({ projectId, taskId });
const loadDocuments = useCallback(async () => { const loadDocuments = useCallback(async () => {
try { try {
@@ -77,6 +127,12 @@ export function TaskDocumentsTab({
void loadDocuments(); void loadDocuments();
}, [loadDocuments]); }, [loadDocuments]);
useEffect(() => {
if (artifactsError) {
addToast(artifactsError || t("taskDocuments.failedToLoadArtifacts", "Failed to load artifacts"), "error");
}
}, [addToast, artifactsError, t]);
async function handleExpandDocument(doc: TaskDocument) { async function handleExpandDocument(doc: TaskDocument) {
if (expandedDocKey === doc.key) { if (expandedDocKey === doc.key) {
setExpandedDocKey(null); setExpandedDocKey(null);
@@ -210,18 +266,52 @@ export function TaskDocumentsTab({
setEditContent(""); setEditContent("");
} }
if (loading) { if (loading || artifactsLoading) {
return ( return (
<div className="detail-section"> <div className="detail-section">
<h4>{t("taskDocuments.heading", "Documents")}</h4> <h4>{t("taskDocuments.heading", "Artifacts")}</h4>
<div className="detail-log-empty"><LoadingSpinner label={t("taskDocuments.loading", "Loading documents…")} /></div> <div className="detail-log-empty"><LoadingSpinner label={t("taskDocuments.loading", "Loading documents and artifacts…")} /></div>
</div> </div>
); );
} }
const isEmpty = documents.length === 0 && artifacts.length === 0 && !showCreateForm;
return ( return (
<div className="detail-section"> <div className="detail-section">
<h4>{t("taskDocuments.heading", "Documents")}</h4> <h4>{t("taskDocuments.heading", "Artifacts")}</h4>
{isEmpty && (
<div className="detail-log-empty">
{t("taskDocuments.noDocuments", "No documents or artifacts yet.")}
</div>
)}
{/*
* FNXC:ArtifactRegistry 2026-06-21-21:44:
* The per-task Artifacts tab must surface both traditional task documents and agent-created media artifacts so users can inspect all task-scoped outputs without leaving the task modal.
*/}
{artifacts.length > 0 && (
<section className="task-artifacts-section" aria-labelledby="task-artifacts-heading">
<div className="task-artifacts-section-header">
<h5 id="task-artifacts-heading">{t("taskDocuments.artifactsSubheading", "Media artifacts")}</h5>
<span className="task-artifacts-section-count">{t("taskDocuments.artifactCount", "{{count}} artifact{{plural}}", { count: artifacts.length, plural: artifacts.length === 1 ? "" : "s" })}</span>
</div>
<div className="documents-artifact-gallery documents-artifact-gallery--mobile task-artifacts-gallery">
{artifacts.map((artifact) => (
<TaskArtifactCard key={artifact.id} artifact={artifact} projectId={projectId} />
))}
</div>
</section>
)}
<section className="task-documents-section" aria-labelledby="task-documents-heading">
<div className="task-artifacts-section-header">
<h5 id="task-documents-heading">{t("taskDocuments.documentsSubheading", "Task documents")}</h5>
{documents.length > 0 && (
<span className="task-artifacts-section-count">{t("taskDocuments.documentCount", "{{count}} document{{plural}}", { count: documents.length, plural: documents.length === 1 ? "" : "s" })}</span>
)}
</div>
{/* Create Form */} {/* Create Form */}
{showCreateForm && ( {showCreateForm && (
@@ -276,9 +366,11 @@ export function TaskDocumentsTab({
{/* Document List */} {/* Document List */}
{documents.length === 0 && !showCreateForm ? ( {documents.length === 0 && !showCreateForm ? (
<div className="detail-log-empty"> !isEmpty && (
{t("taskDocuments.noDocuments", "No documents yet.")} <div className="detail-log-empty">
</div> {t("taskDocuments.noTaskDocuments", "No task documents yet.")}
</div>
)
) : ( ) : (
<div className="task-documents-list"> <div className="task-documents-list">
{documents.map((doc) => ( {documents.map((doc) => (
@@ -456,6 +548,7 @@ export function TaskDocumentsTab({
<Plus size={14} /> {t("taskDocuments.newDocumentButton", "New Document")} <Plus size={14} /> {t("taskDocuments.newDocumentButton", "New Document")}
</button> </button>
)} )}
</section>
</div> </div>
); );
} }

View File

@@ -759,8 +759,8 @@ describe("TaskDetailModal", () => {
// For an in-progress task (no workflow steps, no merge commit), the // For an in-progress task (no workflow steps, no merge commit), the
// top-level tabs are: Chat, Definition, Logs, Changes, Review, Comments, // top-level tabs are: Chat, Definition, Logs, Changes, Review, Comments,
// Documents, Model, Workflow, Stats, Routing. // Artifacts, Model, Workflow, Stats, Routing.
const tabTexts = ["Chat", "Definition", "Logs", "Changes", "Review", "Comments", "Documents", "Model", "Workflow", "Stats", "Routing"]; const tabTexts = ["Chat", "Definition", "Logs", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"];
const tabs = screen.getAllByRole("button").filter((b) => const tabs = screen.getAllByRole("button").filter((b) =>
tabTexts.includes(b.textContent || "") tabTexts.includes(b.textContent || "")
); );

View File

@@ -190,7 +190,7 @@ describe("TaskDetailModal", () => {
); );
// In-progress tasks show exactly 11 tabs: // In-progress tasks show exactly 11 tabs:
// Chat, Definition, Logs, Changes, Review, Comments, Documents, Model, Workflow, Stats, Routing // Chat, Definition, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
const tabs = container.querySelectorAll(".detail-tab"); const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(11); expect(tabs.length).toBe(11);
expect(tabs[0].textContent).toBe("Chat"); expect(tabs[0].textContent).toBe("Chat");
@@ -199,7 +199,7 @@ describe("TaskDetailModal", () => {
expect(tabs[3].textContent).toBe("Changes"); expect(tabs[3].textContent).toBe("Changes");
expect(tabs[4].textContent).toBe("Review"); expect(tabs[4].textContent).toBe("Review");
expect(tabs[5].textContent).toBe("Comments"); expect(tabs[5].textContent).toBe("Comments");
expect(tabs[6].textContent).toBe("Documents"); expect(tabs[6].textContent).toBe("Artifacts");
expect(tabs[7].textContent).toBe("Model"); expect(tabs[7].textContent).toBe("Model");
expect(tabs[8].textContent).toBe("Workflow"); expect(tabs[8].textContent).toBe("Workflow");
expect(tabs[9].textContent).toBe("Stats"); expect(tabs[9].textContent).toBe("Stats");
@@ -231,7 +231,7 @@ describe("TaskDetailModal", () => {
expect(tabs[3].textContent).toBe("Changes"); expect(tabs[3].textContent).toBe("Changes");
expect(tabs[4].textContent).toBe("Review"); expect(tabs[4].textContent).toBe("Review");
expect(tabs[5].textContent).toBe("Comments"); expect(tabs[5].textContent).toBe("Comments");
expect(tabs[6].textContent).toBe("Documents"); expect(tabs[6].textContent).toBe("Artifacts");
expect(tabs[7].textContent).toBe("Model"); expect(tabs[7].textContent).toBe("Model");
expect(tabs[8].textContent).toBe("Workflow"); expect(tabs[8].textContent).toBe("Workflow");
expect(tabs[9].textContent).toBe("Stats"); expect(tabs[9].textContent).toBe("Stats");
@@ -255,7 +255,7 @@ describe("TaskDetailModal", () => {
/>, />,
); );
// Done task with commit SHA: Chat, Definition, Logs, Changes, Review, Comments, Documents, Model, Workflow, Stats, Routing (11 tabs, no Commits) // Done task with commit SHA: Chat, Definition, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing (11 tabs, no Commits)
const tabs = container.querySelectorAll(".detail-tab"); const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(11); expect(tabs.length).toBe(11);
expect(tabs[0].textContent).toBe("Chat"); expect(tabs[0].textContent).toBe("Chat");
@@ -264,7 +264,7 @@ describe("TaskDetailModal", () => {
expect(tabs[3].textContent).toBe("Changes"); expect(tabs[3].textContent).toBe("Changes");
expect(tabs[4].textContent).toBe("Review"); expect(tabs[4].textContent).toBe("Review");
expect(tabs[5].textContent).toBe("Comments"); expect(tabs[5].textContent).toBe("Comments");
expect(tabs[6].textContent).toBe("Documents"); expect(tabs[6].textContent).toBe("Artifacts");
expect(tabs[7].textContent).toBe("Model"); expect(tabs[7].textContent).toBe("Model");
expect(tabs[8].textContent).toBe("Workflow"); expect(tabs[8].textContent).toBe("Workflow");
expect(tabs[9].textContent).toBe("Stats"); expect(tabs[9].textContent).toBe("Stats");
@@ -300,7 +300,7 @@ describe("TaskDetailModal", () => {
expect(tabs[3].textContent).toBe("Changes"); expect(tabs[3].textContent).toBe("Changes");
expect(tabs[4].textContent).toBe("Review"); expect(tabs[4].textContent).toBe("Review");
expect(tabs[5].textContent).toBe("Comments"); expect(tabs[5].textContent).toBe("Comments");
expect(tabs[6].textContent).toBe("Documents"); expect(tabs[6].textContent).toBe("Artifacts");
expect(tabs[7].textContent).toBe("Model"); expect(tabs[7].textContent).toBe("Model");
expect(tabs[8].textContent).toBe("Workflow"); expect(tabs[8].textContent).toBe("Workflow");
expect(tabs[9].textContent).toBe("Stats"); expect(tabs[9].textContent).toBe("Stats");
@@ -324,9 +324,9 @@ describe("TaskDetailModal", () => {
); );
const triageTabs = triageContainer.querySelectorAll(".detail-tab"); const triageTabs = triageContainer.querySelectorAll(".detail-tab");
expect(triageTabs.length).toBe(10); // Chat, Definition, Logs, Review, Comments, Documents, Model, Workflow, Stats, Routing expect(triageTabs.length).toBe(10); // Chat, Definition, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([ expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
"Chat", "Definition", "Logs", "Review", "Comments", "Documents", "Model", "Workflow", "Stats", "Routing", "Chat", "Definition", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]); ]);
const { container: todoContainer } = render( const { container: todoContainer } = render(
@@ -343,9 +343,9 @@ describe("TaskDetailModal", () => {
); );
const todoTabs = todoContainer.querySelectorAll(".detail-tab"); const todoTabs = todoContainer.querySelectorAll(".detail-tab");
expect(todoTabs.length).toBe(10); // Chat, Definition, Logs, Review, Comments, Documents, Model, Workflow, Stats, Routing expect(todoTabs.length).toBe(10); // Chat, Definition, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([ expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
"Chat", "Definition", "Logs", "Review", "Comments", "Documents", "Model", "Workflow", "Stats", "Routing", "Chat", "Definition", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]); ]);
}); });

View File

@@ -1,8 +1,9 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import type { TaskDocument } from "@fusion/core"; import type { ArtifactWithTask, TaskDocument } from "@fusion/core";
import { TaskDocumentsTab } from "../TaskDocumentsTab"; import { TaskDocumentsTab } from "../TaskDocumentsTab";
import { fetchTaskDocuments, fetchTaskDocumentRevisions } from "../../api"; import { artifactMediaUrl, fetchTaskDocuments, fetchTaskDocumentRevisions } from "../../api";
import { useArtifacts } from "../../hooks/useArtifacts";
vi.mock("../../api", () => ({ vi.mock("../../api", () => ({
fetchTaskDocuments: vi.fn(), fetchTaskDocuments: vi.fn(),
@@ -10,10 +11,63 @@ vi.mock("../../api", () => ({
fetchTaskDocumentRevisions: vi.fn(), fetchTaskDocumentRevisions: vi.fn(),
putTaskDocument: vi.fn(), putTaskDocument: vi.fn(),
deleteTaskDocument: vi.fn(), deleteTaskDocument: vi.fn(),
artifactMediaUrl: vi.fn((id: string) => `/api/artifacts/${id}/media`),
}));
vi.mock("../../hooks/useArtifacts", () => ({
useArtifacts: vi.fn(),
})); }));
const mockFetchTaskDocuments = vi.mocked(fetchTaskDocuments); const mockFetchTaskDocuments = vi.mocked(fetchTaskDocuments);
const mockFetchTaskDocumentRevisions = vi.mocked(fetchTaskDocumentRevisions); const mockFetchTaskDocumentRevisions = vi.mocked(fetchTaskDocumentRevisions);
const mockArtifactMediaUrl = vi.mocked(artifactMediaUrl);
const mockUseArtifacts = vi.mocked(useArtifacts);
const mockArtifacts: ArtifactWithTask[] = [
{
id: "artifact-image",
type: "image",
title: "Image artifact",
description: "Screenshot from the agent",
authorId: "agent-image",
taskId: "KB-001",
createdAt: "2026-04-19T10:00:00.000Z",
sizeBytes: 2048,
},
{
id: "artifact-video",
type: "video",
title: "Video artifact",
authorId: "agent-video",
taskId: "KB-001",
createdAt: "2026-04-19T10:01:00.000Z",
},
{
id: "artifact-audio",
type: "audio",
title: "Audio artifact",
authorId: "agent-audio",
taskId: "KB-001",
createdAt: "2026-04-19T10:02:00.000Z",
},
{
id: "artifact-document",
type: "document",
title: "Document artifact",
content: "Inline document preview",
authorId: "agent-doc",
taskId: "KB-001",
createdAt: "2026-04-19T10:03:00.000Z",
},
{
id: "artifact-other",
type: "other",
title: "Other artifact",
authorId: "agent-other",
taskId: "KB-001",
createdAt: "2026-04-19T10:04:00.000Z",
},
];
const mockDocuments: TaskDocument[] = [ const mockDocuments: TaskDocument[] = [
{ {
@@ -45,25 +99,121 @@ describe("TaskDocumentsTab", () => {
vi.clearAllMocks(); vi.clearAllMocks();
mockFetchTaskDocuments.mockResolvedValue(mockDocuments); mockFetchTaskDocuments.mockResolvedValue(mockDocuments);
mockFetchTaskDocumentRevisions.mockResolvedValue([]); mockFetchTaskDocumentRevisions.mockResolvedValue([]);
mockArtifactMediaUrl.mockImplementation((id: string) => `/api/artifacts/${id}/media`);
mockUseArtifacts.mockReturnValue({
artifacts: [],
loading: false,
error: null,
refresh: vi.fn().mockResolvedValue(undefined),
});
}); });
it("renders document list", async () => { it("renders the renamed Artifacts heading with document list", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "Artifacts" })).toBeInTheDocument();
expect(screen.getByText("plan")).toBeInTheDocument();
});
expect(screen.getByText("notes")).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Task documents" })).toBeInTheDocument();
});
it("shows loading until documents and artifacts resolve", () => {
mockUseArtifacts.mockReturnValue({
artifacts: [],
loading: true,
error: null,
refresh: vi.fn().mockResolvedValue(undefined),
});
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
expect(screen.getByText("Loading documents and artifacts…")).toBeInTheDocument();
});
it("shows combined empty state when no documents or artifacts", async () => {
mockFetchTaskDocuments.mockResolvedValue([]);
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => {
expect(screen.getByText("No documents or artifacts yet.")).toBeInTheDocument();
});
expect(screen.queryByText("No task documents yet.")).not.toBeInTheDocument();
});
it("renders documents-only state", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("plan")).toBeInTheDocument(); expect(screen.getByText("plan")).toBeInTheDocument();
}); });
expect(screen.getByText("notes")).toBeInTheDocument(); expect(screen.queryByRole("heading", { name: "Media artifacts" })).not.toBeInTheDocument();
expect(screen.getByText("2 documents")).toBeInTheDocument();
}); });
it("shows empty state when no documents", async () => { it("renders artifacts-only state", async () => {
mockFetchTaskDocuments.mockResolvedValue([]); mockFetchTaskDocuments.mockResolvedValue([]);
mockUseArtifacts.mockReturnValue({
artifacts: mockArtifacts,
loading: false,
error: null,
refresh: vi.fn().mockResolvedValue(undefined),
});
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} projectId="project-1" />);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "Media artifacts" })).toBeInTheDocument();
});
expect(screen.getByText("5 artifacts")).toBeInTheDocument();
expect(screen.getByText("No task documents yet.")).toBeInTheDocument();
expect(screen.queryByText("No documents or artifacts yet.")).not.toBeInTheDocument();
});
it("renders both documents and all five media artifact paths", async () => {
mockUseArtifacts.mockReturnValue({
artifacts: mockArtifacts,
loading: false,
error: null,
refresh: vi.fn().mockResolvedValue(undefined),
});
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} projectId="project-1" />);
await waitFor(() => {
expect(screen.getByText("plan")).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Media artifacts" })).toBeInTheDocument();
});
expect(screen.getByRole("img", { name: "Image artifact" })).toHaveAttribute("src", "/api/artifacts/artifact-image/media");
expect(screen.getByLabelText("Video artifact: Video artifact").tagName).toBe("VIDEO");
expect(screen.getByLabelText("Audio artifact: Audio artifact").tagName).toBe("AUDIO");
expect(screen.getByTestId("artifact-document-preview")).toHaveTextContent("Inline document preview");
expect(screen.getByTestId("artifact-other-link")).toHaveAttribute("href", "/api/artifacts/artifact-other/media");
expect(screen.getByText("agent-image")).toBeInTheDocument();
expect(screen.getByText("2.0 KB")).toBeInTheDocument();
expect(document.querySelector(".documents-artifact-gallery--mobile")).not.toBeNull();
expect(mockUseArtifacts).toHaveBeenCalledWith({ projectId: "project-1", taskId: "KB-001" });
expect(mockArtifactMediaUrl).toHaveBeenCalledWith("artifact-image", "project-1");
});
it("surfaces artifact fetch errors", async () => {
mockUseArtifacts.mockReturnValue({
artifacts: [],
loading: false,
error: "Artifact fetch failed",
refresh: vi.fn().mockResolvedValue(undefined),
});
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("No documents yet.")).toBeInTheDocument(); expect(addToast).toHaveBeenCalledWith("Artifact fetch failed", "error");
}); });
}); });

View File

@@ -7461,7 +7461,7 @@
"chat": "Chat", "chat": "Chat",
"comments": "Comments", "comments": "Comments",
"definition": "Definition", "definition": "Definition",
"documents": "Documents", "documents": "Artifacts",
"logs": "Logs", "logs": "Logs",
"model": "Model", "model": "Model",
"pullRequest": "Pull Request", "pullRequest": "Pull Request",
@@ -7520,21 +7520,21 @@
"failedToLoad": "Failed to load documents", "failedToLoad": "Failed to load documents",
"failedToLoadRevisions": "Failed to load revisions", "failedToLoadRevisions": "Failed to load revisions",
"failedToSave": "Failed to save document", "failedToSave": "Failed to save document",
"heading": "Documents", "heading": "Artifacts",
"history": "History", "history": "History",
"invalidKeyFormat": "Invalid key format. Use 1-64 alphanumeric characters, hyphens, or underscores.", "invalidKeyFormat": "Invalid key format. Use 1-64 alphanumeric characters, hyphens, or underscores.",
"keyHint": "Alphanumeric, hyphens, underscores (1-64 chars)", "keyHint": "Alphanumeric, hyphens, underscores (1-64 chars)",
"keyLabel": "Key", "keyLabel": "Key",
"keyPlaceholder": "e.g., plan, notes, research", "keyPlaceholder": "e.g., plan, notes, research",
"keyRequired": "Document key is required", "keyRequired": "Document key is required",
"loading": "Loading documents…", "loading": "Loading documents and artifacts…",
"loadingRevisions": "Loading…", "loadingRevisions": "Loading…",
"modeMarkdown": "Markdown", "modeMarkdown": "Markdown",
"modePlain": "Plain", "modePlain": "Plain",
"newDocumentButton": "New Document", "newDocumentButton": "New Document",
"newDocumentTitle": "New Document", "newDocumentTitle": "New Document",
"no": "No", "no": "No",
"noDocuments": "No documents yet.", "noDocuments": "No documents or artifacts yet.",
"noPreviousRevisions": "No previous revisions.", "noPreviousRevisions": "No previous revisions.",
"revisionHistory": "Revision History", "revisionHistory": "Revision History",
"save": "Save", "save": "Save",
@@ -7542,7 +7542,13 @@
"saving": "Saving…", "saving": "Saving…",
"switchToMarkdown": "Switch to markdown", "switchToMarkdown": "Switch to markdown",
"switchToPlainText": "Switch to plain text", "switchToPlainText": "Switch to plain text",
"yes": "Yes" "yes": "Yes",
"artifactCount": "{{count}} artifact{{plural}}",
"artifactsSubheading": "Media artifacts",
"documentCount": "{{count}} document{{plural}}",
"documentsSubheading": "Task documents",
"failedToLoadArtifacts": "Failed to load artifacts",
"noTaskDocuments": "No task documents yet."
}, },
"taskFields": { "taskFields": {
"moreFields": "Additional fields", "moreFields": "Additional fields",

View File

@@ -7461,7 +7461,7 @@
"chat": "Chat", "chat": "Chat",
"comments": "Comentarios", "comments": "Comentarios",
"definition": "Definición", "definition": "Definición",
"documents": "Documentos", "documents": "Artifacts",
"logs": "Registros", "logs": "Registros",
"model": "Modelo", "model": "Modelo",
"pullRequest": "Pull Request", "pullRequest": "Pull Request",
@@ -7520,21 +7520,21 @@
"failedToLoad": "Error al cargar los documentos", "failedToLoad": "Error al cargar los documentos",
"failedToLoadRevisions": "Error al cargar las revisiones", "failedToLoadRevisions": "Error al cargar las revisiones",
"failedToSave": "Error al guardar el documento", "failedToSave": "Error al guardar el documento",
"heading": "Documentos", "heading": "Artifacts",
"history": "Historial", "history": "Historial",
"invalidKeyFormat": "Formato de clave inválido. Use 1-64 caracteres alfanuméricos, guiones o guiones bajos.", "invalidKeyFormat": "Formato de clave inválido. Use 1-64 caracteres alfanuméricos, guiones o guiones bajos.",
"keyHint": "Alfanumérico, guiones, guiones bajos (1-64 caracteres)", "keyHint": "Alfanumérico, guiones, guiones bajos (1-64 caracteres)",
"keyLabel": "Clave", "keyLabel": "Clave",
"keyPlaceholder": "p. ej., plan, notas, investigación", "keyPlaceholder": "p. ej., plan, notas, investigación",
"keyRequired": "Se requiere la clave del documento", "keyRequired": "Se requiere la clave del documento",
"loading": "Cargando documentos…", "loading": "Loading documents and artifacts…",
"loadingRevisions": "Cargando…", "loadingRevisions": "Cargando…",
"modeMarkdown": "Markdown", "modeMarkdown": "Markdown",
"modePlain": "Texto sin formato", "modePlain": "Texto sin formato",
"newDocumentButton": "Nuevo documento", "newDocumentButton": "Nuevo documento",
"newDocumentTitle": "Nuevo documento", "newDocumentTitle": "Nuevo documento",
"no": "No", "no": "No",
"noDocuments": "Sin documentos aún.", "noDocuments": "No documents or artifacts yet.",
"noPreviousRevisions": "Sin revisiones anteriores.", "noPreviousRevisions": "Sin revisiones anteriores.",
"revisionHistory": "Historial de revisiones", "revisionHistory": "Historial de revisiones",
"save": "Guardar", "save": "Guardar",
@@ -7542,7 +7542,13 @@
"saving": "Guardando…", "saving": "Guardando…",
"switchToMarkdown": "Cambiar a markdown", "switchToMarkdown": "Cambiar a markdown",
"switchToPlainText": "Cambiar a texto sin formato", "switchToPlainText": "Cambiar a texto sin formato",
"yes": "Sí" "yes": "Sí",
"artifactCount": "{{count}} artifact{{plural}}",
"artifactsSubheading": "Media artifacts",
"documentCount": "{{count}} document{{plural}}",
"documentsSubheading": "Task documents",
"failedToLoadArtifacts": "Failed to load artifacts",
"noTaskDocuments": "No task documents yet."
}, },
"taskFields": { "taskFields": {
"moreFields": "Campos adicionales", "moreFields": "Campos adicionales",

View File

@@ -7461,7 +7461,7 @@
"chat": "Chat", "chat": "Chat",
"comments": "Commentaires", "comments": "Commentaires",
"definition": "Définition", "definition": "Définition",
"documents": "Documents", "documents": "Artifacts",
"logs": "Journaux", "logs": "Journaux",
"model": "Modèle", "model": "Modèle",
"pullRequest": "Pull Request", "pullRequest": "Pull Request",
@@ -7520,21 +7520,21 @@
"failedToLoad": "Échec du chargement des documents", "failedToLoad": "Échec du chargement des documents",
"failedToLoadRevisions": "Échec du chargement des révisions", "failedToLoadRevisions": "Échec du chargement des révisions",
"failedToSave": "Échec de l'enregistrement du document", "failedToSave": "Échec de l'enregistrement du document",
"heading": "Documents", "heading": "Artifacts",
"history": "Historique", "history": "Historique",
"invalidKeyFormat": "Format de clé invalide. Utilisez 1-64 caractères alphanumériques, tirets ou traits de soulignement.", "invalidKeyFormat": "Format de clé invalide. Utilisez 1-64 caractères alphanumériques, tirets ou traits de soulignement.",
"keyHint": "Alphanumériques, tirets, traits de soulignement (1-64 caractères)", "keyHint": "Alphanumériques, tirets, traits de soulignement (1-64 caractères)",
"keyLabel": "Clé", "keyLabel": "Clé",
"keyPlaceholder": "ex. plan, notes, recherche", "keyPlaceholder": "ex. plan, notes, recherche",
"keyRequired": "La clé du document est requise", "keyRequired": "La clé du document est requise",
"loading": "Chargement des documents…", "loading": "Loading documents and artifacts…",
"loadingRevisions": "Chargement…", "loadingRevisions": "Chargement…",
"modeMarkdown": "Markdown", "modeMarkdown": "Markdown",
"modePlain": "Texte brut", "modePlain": "Texte brut",
"newDocumentButton": "Nouveau document", "newDocumentButton": "Nouveau document",
"newDocumentTitle": "Nouveau document", "newDocumentTitle": "Nouveau document",
"no": "Non", "no": "Non",
"noDocuments": "Aucun document pour le moment.", "noDocuments": "No documents or artifacts yet.",
"noPreviousRevisions": "Aucune révision précédente.", "noPreviousRevisions": "Aucune révision précédente.",
"revisionHistory": "Historique des révisions", "revisionHistory": "Historique des révisions",
"save": "Enregistrer", "save": "Enregistrer",
@@ -7542,7 +7542,13 @@
"saving": "Enregistrement…", "saving": "Enregistrement…",
"switchToMarkdown": "Basculer vers markdown", "switchToMarkdown": "Basculer vers markdown",
"switchToPlainText": "Basculer vers du texte brut", "switchToPlainText": "Basculer vers du texte brut",
"yes": "Oui" "yes": "Oui",
"artifactCount": "{{count}} artifact{{plural}}",
"artifactsSubheading": "Media artifacts",
"documentCount": "{{count}} document{{plural}}",
"documentsSubheading": "Task documents",
"failedToLoadArtifacts": "Failed to load artifacts",
"noTaskDocuments": "No task documents yet."
}, },
"taskFields": { "taskFields": {
"moreFields": "Champs supplémentaires", "moreFields": "Champs supplémentaires",

View File

@@ -7461,7 +7461,7 @@
"chat": "Chat", "chat": "Chat",
"comments": "댓글", "comments": "댓글",
"definition": "정의", "definition": "정의",
"documents": "문서", "documents": "Artifacts",
"logs": "로그", "logs": "로그",
"model": "모델", "model": "모델",
"pullRequest": "풀 리퀘스트", "pullRequest": "풀 리퀘스트",
@@ -7520,21 +7520,21 @@
"failedToLoad": "문서 불러오기에 실패했습니다", "failedToLoad": "문서 불러오기에 실패했습니다",
"failedToLoadRevisions": "개정 이력 불러오기에 실패했습니다", "failedToLoadRevisions": "개정 이력 불러오기에 실패했습니다",
"failedToSave": "문서 저장에 실패했습니다", "failedToSave": "문서 저장에 실패했습니다",
"heading": "문서", "heading": "Artifacts",
"history": "이력", "history": "이력",
"invalidKeyFormat": "키 형식이 올바르지 않습니다. 1~64자의 영숫자, 하이픈 또는 밑줄을 사용하세요.", "invalidKeyFormat": "키 형식이 올바르지 않습니다. 1~64자의 영숫자, 하이픈 또는 밑줄을 사용하세요.",
"keyHint": "영숫자, 하이픈, 밑줄 (1~64자)", "keyHint": "영숫자, 하이픈, 밑줄 (1~64자)",
"keyLabel": "키", "keyLabel": "키",
"keyPlaceholder": "예: plan, notes, research", "keyPlaceholder": "예: plan, notes, research",
"keyRequired": "문서 키를 입력해 주세요", "keyRequired": "문서 키를 입력해 주세요",
"loading": "문서 불러오는 중…", "loading": "Loading documents and artifacts…",
"loadingRevisions": "불러오는 중…", "loadingRevisions": "불러오는 중…",
"modeMarkdown": "Markdown", "modeMarkdown": "Markdown",
"modePlain": "일반 텍스트", "modePlain": "일반 텍스트",
"newDocumentButton": "새 문서", "newDocumentButton": "새 문서",
"newDocumentTitle": "새 문서", "newDocumentTitle": "새 문서",
"no": "아니요", "no": "아니요",
"noDocuments": "문서가 없습니다.", "noDocuments": "No documents or artifacts yet.",
"noPreviousRevisions": "이전 개정 이력이 없습니다.", "noPreviousRevisions": "이전 개정 이력이 없습니다.",
"revisionHistory": "개정 이력", "revisionHistory": "개정 이력",
"save": "저장", "save": "저장",
@@ -7542,7 +7542,13 @@
"saving": "저장 중…", "saving": "저장 중…",
"switchToMarkdown": "Markdown으로 전환", "switchToMarkdown": "Markdown으로 전환",
"switchToPlainText": "일반 텍스트로 전환", "switchToPlainText": "일반 텍스트로 전환",
"yes": "예" "yes": "예",
"artifactCount": "{{count}} artifact{{plural}}",
"artifactsSubheading": "Media artifacts",
"documentCount": "{{count}} document{{plural}}",
"documentsSubheading": "Task documents",
"failedToLoadArtifacts": "Failed to load artifacts",
"noTaskDocuments": "No task documents yet."
}, },
"taskFields": { "taskFields": {
"moreFields": "추가 필드", "moreFields": "추가 필드",

View File

@@ -7461,7 +7461,7 @@
"chat": "Chat", "chat": "Chat",
"comments": "评论", "comments": "评论",
"definition": "定义", "definition": "定义",
"documents": "文档", "documents": "Artifacts",
"logs": "日志", "logs": "日志",
"model": "模型", "model": "模型",
"pullRequest": "拉取请求", "pullRequest": "拉取请求",
@@ -7520,21 +7520,21 @@
"failedToLoad": "加载文档失败", "failedToLoad": "加载文档失败",
"failedToLoadRevisions": "加载修订失败", "failedToLoadRevisions": "加载修订失败",
"failedToSave": "保存文档失败", "failedToSave": "保存文档失败",
"heading": "文档", "heading": "Artifacts",
"history": "历史", "history": "历史",
"invalidKeyFormat": "无效的键格式。使用 1-64 个字母数字字符、连字符或下划线。", "invalidKeyFormat": "无效的键格式。使用 1-64 个字母数字字符、连字符或下划线。",
"keyHint": "字母数字、连字符、下划线(1-64 个字符)", "keyHint": "字母数字、连字符、下划线(1-64 个字符)",
"keyLabel": "键", "keyLabel": "键",
"keyPlaceholder": "例如:plan、notes、research", "keyPlaceholder": "例如:plan、notes、research",
"keyRequired": "文档键是必需的", "keyRequired": "文档键是必需的",
"loading": "正在加载文档…", "loading": "Loading documents and artifacts…",
"loadingRevisions": "正在加载…", "loadingRevisions": "正在加载…",
"modeMarkdown": "Markdown", "modeMarkdown": "Markdown",
"modePlain": "纯文本", "modePlain": "纯文本",
"newDocumentButton": "新文档", "newDocumentButton": "新文档",
"newDocumentTitle": "新文档", "newDocumentTitle": "新文档",
"no": "否", "no": "否",
"noDocuments": "还没有文档。", "noDocuments": "No documents or artifacts yet.",
"noPreviousRevisions": "没有以前的修订。", "noPreviousRevisions": "没有以前的修订。",
"revisionHistory": "修订历史", "revisionHistory": "修订历史",
"save": "保存", "save": "保存",
@@ -7542,7 +7542,13 @@
"saving": "保存中…", "saving": "保存中…",
"switchToMarkdown": "切换到 Markdown", "switchToMarkdown": "切换到 Markdown",
"switchToPlainText": "切换到纯文本", "switchToPlainText": "切换到纯文本",
"yes": "是" "yes": "是",
"artifactCount": "{{count}} artifact{{plural}}",
"artifactsSubheading": "Media artifacts",
"documentCount": "{{count}} document{{plural}}",
"documentsSubheading": "Task documents",
"failedToLoadArtifacts": "Failed to load artifacts",
"noTaskDocuments": "No task documents yet."
}, },
"taskFields": { "taskFields": {
"moreFields": "其他字段", "moreFields": "其他字段",

View File

@@ -7461,7 +7461,7 @@
"chat": "Chat", "chat": "Chat",
"comments": "評論", "comments": "評論",
"definition": "定義", "definition": "定義",
"documents": "文件", "documents": "Artifacts",
"logs": "日誌", "logs": "日誌",
"model": "模型", "model": "模型",
"pullRequest": "拉取請求", "pullRequest": "拉取請求",
@@ -7520,21 +7520,21 @@
"failedToLoad": "載入文件失敗", "failedToLoad": "載入文件失敗",
"failedToLoadRevisions": "載入修訂失敗", "failedToLoadRevisions": "載入修訂失敗",
"failedToSave": "保存文件失敗", "failedToSave": "保存文件失敗",
"heading": "文件", "heading": "Artifacts",
"history": "歷史", "history": "歷史",
"invalidKeyFormat": "無效的鍵格式。使用 1-64 個字母、數字、連字號或底線。", "invalidKeyFormat": "無效的鍵格式。使用 1-64 個字母、數字、連字號或底線。",
"keyHint": "字母、數字、連字號、底線(1-64 字元)", "keyHint": "字母、數字、連字號、底線(1-64 字元)",
"keyLabel": "鍵", "keyLabel": "鍵",
"keyPlaceholder": "例如:plan、notes、research", "keyPlaceholder": "例如:plan、notes、research",
"keyRequired": "文件鍵為必填項", "keyRequired": "文件鍵為必填項",
"loading": "正在載入文件…", "loading": "Loading documents and artifacts…",
"loadingRevisions": "正在載入…", "loadingRevisions": "正在載入…",
"modeMarkdown": "Markdown", "modeMarkdown": "Markdown",
"modePlain": "純文字", "modePlain": "純文字",
"newDocumentButton": "新文件", "newDocumentButton": "新文件",
"newDocumentTitle": "新文件", "newDocumentTitle": "新文件",
"no": "否", "no": "否",
"noDocuments": "還沒有文件。", "noDocuments": "No documents or artifacts yet.",
"noPreviousRevisions": "沒有先前的修訂。", "noPreviousRevisions": "沒有先前的修訂。",
"revisionHistory": "修訂歷史", "revisionHistory": "修訂歷史",
"save": "儲存", "save": "儲存",
@@ -7542,7 +7542,13 @@
"saving": "儲存中…", "saving": "儲存中…",
"switchToMarkdown": "切換為 Markdown", "switchToMarkdown": "切換為 Markdown",
"switchToPlainText": "切換為純文字", "switchToPlainText": "切換為純文字",
"yes": "是" "yes": "是",
"artifactCount": "{{count}} artifact{{plural}}",
"artifactsSubheading": "Media artifacts",
"documentCount": "{{count}} document{{plural}}",
"documentsSubheading": "Task documents",
"failedToLoadArtifacts": "Failed to load artifacts",
"noTaskDocuments": "No task documents yet."
}, },
"taskFields": { "taskFields": {
"moreFields": "其他欄位", "moreFields": "其他欄位",