From 56b20a76af839a48f0d5e9b7699a6d90dd49c065 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 10 Jul 2026 23:25:02 -0700 Subject: [PATCH] FN-7812: extend select-to-comment popover to Task Documents right pane Adds select-to-comment parity to the Task Documents right pane in the Artifacts view, reusing the existing Project Files selection-comment pattern (useSelectionComment/SelectionCommentPopover) so operators can highlight task-document content and send it to a new task. - Add markdown/plain preview refs and useSelectionComment hooks scoped to the selected Task Document, following the existing Plain/Markdown render toggle - Gate the Task Documents selection popover on activeTab === "tasks" and the selected task document (separate from the Project Files popover, which stays gated on activeTab === "project") so tab switches never cross-render popovers - Compose the New Task description source as `taskId/key` for task-document selections, mirroring the file-path convention used for Project Files - Add regression tests covering plain/markdown task-document selection, empty-pane gating, tab isolation between Task Documents and Project Files popovers, and the mobile detail pane - Update dashboard-guide.md to document select-to-comment support for Task Documents alongside Project Files - Add a minor changeset for @runfusion/fusion documenting the feature (depends on FN-7811) Files changed: .changeset/fn-7812-task-documents-select-to-comment.md | 7 ++ docs/dashboard-guide.md | 4 +- packages/dashboard/app/components/DocumentsView.tsx | 37 ++++++--- packages/dashboard/app/components/__tests__/DocumentsView.test.tsx | 93 ++++++++++++++++++++++ 4 files changed, 130 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-7812 Fusion-Task-Lineage: ace10d70-35fd-42fe-8a4a-e2c2c7c7c27b Co-authored-by: Fusion (runfusion.ai) --- ...n-7812-task-documents-select-to-comment.md | 7 ++ docs/dashboard-guide.md | 4 +- .../app/components/DocumentsView.tsx | 37 ++++++-- .../__tests__/DocumentsView.test.tsx | 93 +++++++++++++++++++ 4 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 .changeset/fn-7812-task-documents-select-to-comment.md 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: [],