diff --git a/.changeset/fn-7812-task-documents-select-to-comment.md b/.changeset/fn-7812-task-documents-select-to-comment.md new file mode 100644 index 0000000000..03b350ae17 --- /dev/null +++ b/.changeset/fn-7812-task-documents-select-to-comment.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Artifacts view — select text in a Task Document's content pane to comment and send it to a new task. +category: feature +dev: DocumentsView Task Documents right pane reuses the Project Files `useSelectionComment`/`SelectionCommentPopover` pattern (markdown + plain refs following the render toggle, composer-open lock, popover gated on the task-document selection + `onSendSelectionToTask`). Project Files behavior and the markdown/plain toggle are unchanged. Depends on FN-7811. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 6c3627356b..03ad744e3b 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -836,7 +836,7 @@ Features: - Empty states: with no search query it shows `No artifacts yet.` plus the hint that artifacts are created by agents, users, and system tools; with a search query it shows `No artifacts match "".` - Error state: a failed artifact list request uses the shared `Failed to load artifacts: ` panel with a **Retry** action that re-runs the artifact fetch - 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 or the selected Task Document's right pane, choose **Add comment**, and send the source path/key, selected snippet, and your comment to the **New Task** dialog Agent registrations also surface through the [Mailbox View](#mailbox-view): successful `fn_artifact_register` calls send a best-effort system inbox notification so users can discover new media even before opening the gallery. Artifact list live-refresh does not depend on that best-effort message; it listens to the registry registration event. @@ -871,7 +871,7 @@ Artifacts view supports toggling between raw text and formatted markdown when vi The toggle button is accessible with `aria-pressed` for screen readers. Toggle state is scoped per-document, so switching between documents resets the view to raw mode. -Project-file previews also support selection comments in both raw and rendered markdown modes. Select text, click **Add comment**, enter a short note, and Fusion opens **New Task** with a seeded description containing the file path, snippet, and comment. +Project-file previews and selected Task Documents also support selection comments in both raw and rendered markdown modes. Select text, click **Add comment**, enter a short note, and Fusion opens **New Task** with a seeded description containing the file path or task-document key, snippet, and comment. ## Todo View diff --git a/packages/dashboard/app/components/DocumentsView.tsx b/packages/dashboard/app/components/DocumentsView.tsx index f352f03bab..6964b57e2e 100644 --- a/packages/dashboard/app/components/DocumentsView.tsx +++ b/packages/dashboard/app/components/DocumentsView.tsx @@ -74,6 +74,8 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac const requestIdRef = useRef(0); const markdownPreviewRef = useRef(null); const plainPreviewRef = useRef(null); + const taskDocMarkdownPreviewRef = useRef(null); + const taskDocPlainPreviewRef = useRef(null); // Markdown render toggle for project file preview const [renderProjectMarkdown, setRenderProjectMarkdown] = useState(false); // Markdown render toggles per task document card (scoped by doc ID) @@ -81,6 +83,8 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac const [selectionCommentOpen, setSelectionCommentOpen] = useState(false); const markdownSelection = useSelectionComment(markdownPreviewRef, { locked: selectionCommentOpen }); const plainSelection = useSelectionComment(plainPreviewRef, { locked: selectionCommentOpen }); + const taskDocMarkdownSelection = useSelectionComment(taskDocMarkdownPreviewRef, { locked: selectionCommentOpen }); + const taskDocPlainSelection = useSelectionComment(taskDocPlainPreviewRef, { locked: selectionCommentOpen }); const activeProjectSelection = renderProjectMarkdown ? markdownSelection : plainSelection; const taskSearchQuery = activeTab === "tasks" ? searchQuery.trim() : ""; @@ -168,7 +172,10 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac /* FNXC:DocumentsView 2026-07-10-17:30: - Task Documents now mirrors the Project Files sidebar/right-pane contract: the task-document selection is deliberately separate from selectedFile so tab switching cannot leak project file content into the Task Documents pane. Select-to-comment remains Project-Files-only for FN-7811 and is tracked as follow-up scope; Task Documents preserves only its existing Plain/Markdown render toggle. + Task Documents now mirrors the Project Files sidebar/right-pane contract: the task-document selection is deliberately separate from selectedFile so tab switching cannot leak project file content into the Task Documents pane. Task Documents keeps its own Plain/Markdown render toggle so Project Files state never controls task-document rendering. + + FNXC:DocumentsView 2026-07-10-23:41: + FN-7812 extends the existing select-to-comment affordance to the Task Documents right pane without a new comment model. The active task-document selection ref follows the same Plain/Markdown toggle, and the composed source path uses taskId/key so operators can identify the originating task document. Keep Task Documents gated by selectedTaskDocument and Project Files gated by selectedFile so tab switches cannot cross-render popovers; the shared composer-open lock is safe because only one tab pane is mounted at a time. */ const selectedTaskDocument = useMemo(() => { if (!selectedTaskDocumentId) { @@ -320,7 +327,9 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac }, [activeTab, refreshArtifacts, refreshProjectFiles, refreshDocuments]); const activeCount = activeTab === "project" ? filteredProjectFiles.length : activeTab === "tasks" ? documents.length : artifacts.length; - const selectionPopover = selectedFile && onSendSelectionToTask && activeProjectSelection ? ( + const selectedTaskDocumentRendersMarkdown = selectedTaskDocument ? (taskDocMarkdownStates.get(selectedTaskDocument.id) ?? false) : false; + const activeTaskDocumentSelection = selectedTaskDocumentRendersMarkdown ? taskDocMarkdownSelection : taskDocPlainSelection; + const selectionPopover = activeTab === "project" && selectedFile && onSendSelectionToTask && activeProjectSelection ? ( ) : null; + const taskDocumentSelectionPopover = activeTab === "tasks" && selectedTaskDocument && onSendSelectionToTask && activeTaskDocumentSelection ? ( + + ) : null; const searchPlaceholder = activeTab === "project" ? t("documents.searchProjectFiles", "Search project markdown files…") @@ -691,22 +709,23 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac - {(taskDocMarkdownStates.get(selectedTaskDocument.id) ?? false) ? ( -
+ {selectedTaskDocumentRendersMarkdown ? ( +
{selectedTaskDocument.content}
) : ( -
{selectedTaskDocument.content}
+
{selectedTaskDocument.content}
)} + {taskDocumentSelectionPopover}
)} diff --git a/packages/dashboard/app/components/__tests__/DocumentsView.test.tsx b/packages/dashboard/app/components/__tests__/DocumentsView.test.tsx index 6ecd6cbedd..48083be4e5 100644 --- a/packages/dashboard/app/components/__tests__/DocumentsView.test.tsx +++ b/packages/dashboard/app/components/__tests__/DocumentsView.test.tsx @@ -298,6 +298,7 @@ describe("DocumentsView", () => { afterEach(() => { window.innerWidth = originalInnerWidth; + document.getSelection()?.removeAllRanges(); }); /* @@ -834,6 +835,98 @@ describe("DocumentsView", () => { expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Review this rendered content.")); }); + /* + FNXC:DocumentsView 2026-07-10-23:46: + FN-7812 adds select-to-comment parity to Task Documents, so tests must cover the same surface checklist as Project Files: plain and markdown render modes, desktop and mobile detail layouts, empty-pane gating, tab isolation, and task-document file context in the composed New Task description. + */ + it("sends selected plain task document text to a new task description", async () => { + mockSelectionRect(); + const onSendSelectionToTask = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + fireEvent.click(screen.getByRole("button", { name: "Open KB-001 plan" })); + const plainPreview = screen.getByText("Alpha document content"); + selectNodeText(plainPreview); + + fireEvent.click(await screen.findByRole("button", { name: /add a comment/i })); + fireEvent.change(screen.getByLabelText(/comment for the new task/i), { target: { value: "Follow up from the task doc." } }); + fireEvent.click(screen.getByRole("button", { name: /send to new task/i })); + + expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("File: KB-001/plan")); + expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Alpha document content")); + expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Follow up from the task doc.")); + }); + + it("sends selected markdown task document text to a new task description", async () => { + mockSelectionRect(); + const onSendSelectionToTask = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + fireEvent.click(screen.getByRole("button", { name: "Open KB-001 plan" })); + fireEvent.click(screen.getByRole("button", { name: /switch to markdown/i })); + const markdownPreviewText = await screen.findByText("Alpha document content"); + selectNodeText(markdownPreviewText); + + fireEvent.click(await screen.findByRole("button", { name: /add a comment/i })); + fireEvent.change(screen.getByLabelText(/comment for the new task/i), { target: { value: "Review rendered task doc." } }); + fireEvent.click(screen.getByRole("button", { name: /send to new task/i })); + + expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("File: KB-001/plan")); + expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Alpha document content")); + expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Review rendered task doc.")); + }); + + it("does not show the task document comment trigger in the empty right pane", () => { + const onSendSelectionToTask = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + + expect(screen.getByText("Select a task document to view its content.")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /add a comment/i })).not.toBeInTheDocument(); + }); + + it("keeps task document and project file selection comment popovers isolated", async () => { + mockSelectionRect(); + const onSendSelectionToTask = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + fireEvent.click(screen.getByRole("button", { name: "Open KB-001 plan" })); + selectNodeText(screen.getByText("Alpha document content")); + expect(await screen.findByRole("button", { name: /add a comment/i })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); + expect(screen.queryByRole("button", { name: /add a comment/i })).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); + const projectPreview = await screen.findByText(/Hello docs/); + selectNodeText(projectPreview); + fireEvent.click(await screen.findByRole("button", { name: /add a comment/i })); + fireEvent.change(screen.getByLabelText(/comment for the new task/i), { target: { value: "Project file still works." } }); + fireEvent.click(screen.getByRole("button", { name: /send to new task/i })); + + expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("File: README.md")); + expect(onSendSelectionToTask).toHaveBeenCalledWith(expect.stringContaining("Project file still works.")); + expect(onSendSelectionToTask).not.toHaveBeenCalledWith(expect.stringContaining("File: KB-001/plan")); + }); + + it("shows the task document comment trigger in the mobile detail pane", async () => { + window.innerWidth = 600; + mockSelectionRect(); + const onSendSelectionToTask = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + fireEvent.click(screen.getByRole("button", { name: "Open KB-001 plan" })); + const mobilePreview = screen.getByText("Alpha document content"); + selectNodeText(mobilePreview); + + expect(await screen.findByRole("button", { name: /add a comment/i })).toBeInTheDocument(); + }); + it("search filters task documents and clears filtered-out selection", async () => { mockUseProjectMarkdownFiles.mockReturnValue({ files: [],