diff --git a/doc/PRODUCT.md b/doc/PRODUCT.md index 9d5190b1cee..cba3bec4ae2 100644 --- a/doc/PRODUCT.md +++ b/doc/PRODUCT.md @@ -93,6 +93,10 @@ Tasks have parentage. Every task exists in service of a parent task, all the way The current issue model includes stable issue identifiers, parent/sub-issues, blockers, a single assignee, comments, issue documents, attachments and work products, and review/approval handoffs. That structure keeps work inspectable by both the board and agents while still allowing agents to decompose work into smaller tasks. +When an agent creates a subtask or follow-up task, its source task's feed shows +a linked card at creation time with the current title, task number, and status. +The Tasks tab keeps the full relationship list. + Automatic monitor checks pause when a task is blocked. A person can use Check now to request one check of a retained monitor through normal execution admission. Dependencies, recovery, pause, budgets and ownership still apply. Provider-usage diff --git a/maps/feature-map/task-coordination.md b/maps/feature-map/task-coordination.md index 950219c3292..0d6a7e7c1b2 100644 --- a/maps/feature-map/task-coordination.md +++ b/maps/feature-map/task-coordination.md @@ -25,6 +25,11 @@ Agent access: [companion](../agent-access-map/task-coordination.md). Open the task’s parent/child and dependency links in task detail. +The task feed shows a linked card when an agent creates a direct subtask or a +follow-up task from that task. The card stays at its creation time beside +documents and artifacts. It shows the current task title, number, and status. +The Tasks tab retains the complete relationship list. + ### `review-and-controls` Inspect the task’s execution policy and available review, pause, resume, or stop controls. @@ -41,6 +46,11 @@ creates a hold; it does not change task statuses to a paused status. Automated: [dependency wakeups](../../server/src/__tests__/issue-dependency-wakeups-routes.test.ts) checks server behavior; visual relationship navigation remains a manual check. +The [created task card stories](../../ui/storybook/stories/task-coordination/task-created-inline.stories.tsx) +render the production feed with fixture task reads. The +[task feed tests](../../ui/src/components/TaskChatThread.task-creation-order.test.tsx) +check creation order, current status, links, and duplicate suppression. + Manual: Block one task on another, finish the prerequisite, and observe the dependent task’s eligible continuation. Follow parent/child links back to the source request. Verify no duplicate run or false parent completion occurs. ### `review-and-controls` diff --git a/ui/src/components/TaskChatThread.task-creation-order.test.tsx b/ui/src/components/TaskChatThread.task-creation-order.test.tsx index ef5498acd11..c81a4ddfda9 100644 --- a/ui/src/components/TaskChatThread.task-creation-order.test.tsx +++ b/ui/src/components/TaskChatThread.task-creation-order.test.tsx @@ -3,7 +3,7 @@ import { act, forwardRef, useImperativeHandle, type ForwardedRef } from "react"; import { createRoot, type Root } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { ConversationTaskHandoff, IssueDocument, IssueThreadInteraction } from "@paperclipai/shared"; +import type { ConversationTaskHandoff, Issue, IssueDocument, IssueThreadInteraction } from "@paperclipai/shared"; import { TaskChatThread } from "./TaskChatThread"; import { CoordinatorPresentation } from "./coordinator/CoordinatorPresentation"; import { ThemeProvider } from "@/context/ThemeContext"; @@ -77,6 +77,10 @@ const handoff = (id = "task", sourceRunId: string | null = "source-run", second id: `chat_handoff:${id}`, issueId: id, identifier: `PAPER-${id}`, title: `Created ${id}`, status: "todo", createdAt: at(second), sourceRunId, suggestionInteractionId: null, }); +const createdTask = (overrides: Partial = {}): Issue => ({ + id: "child", companyId: "company", parentId: "chat", identifier: "PAPER-102", title: "Verify the repair", + status: "todo", createdByAgentId: "agent", createdAt: new Date(at(5)), ...overrides, +} as Issue); const suggestion = (status: "pending" | "accepted"): IssueThreadInteraction => ({ ...pendingSuggestedTasksInteraction, id: "suggestion", companyId: "company", issueId: "chat", sourceRunId: "source-run", status, createdAt: new Date(at(3)), updatedAt: new Date(at(3)), @@ -111,6 +115,46 @@ const anchor = (id: string) => host.querySelector(`[data-thread-anchor="${id}"]` const before = (earlier: Element, later: Element) => expect(earlier.compareDocumentPosition(later) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); describe("Coordinator task creation ordering", () => { + it("shows agent-created subtasks in ordinary task feeds at creation time and refreshes their status", async () => { + state.coordinator = false; + await paint({ conversationMode: false, companyId: "company", createdTasks: [createdTask(), createdTask()] }); + const card = host.querySelector('[data-testid="task-chat-created-task"]')!; + expect(card.textContent).toContain("Subtask created"); + expect(card.textContent).toContain("Verify the repair"); + expect(card.textContent).toContain("Todo"); + expect(card.getAttribute("href")).toBe("/issues/PAPER-102"); + before(card, anchor("final")); + before(anchor("final"), anchor("next")); + expect(host.querySelectorAll('[data-testid="task-chat-created-task"]')).toHaveLength(1); + await paint({ conversationMode: false, companyId: "company", createdTasks: [createdTask({ status: "done" })] }); + expect(host.querySelector('[data-testid="task-chat-created-task"]')).toBe(card); + expect(card.textContent).toContain("Done"); + }); + + it("filters human and foreign-company tasks and links standalone follow-ups by ID", async () => { + state.coordinator = false; + await paint({ conversationMode: false, companyId: "company", createdTasks: [ + createdTask({ id: "human", createdByAgentId: null }), + createdTask({ id: "foreign", companyId: "other" }), + createdTask({ id: "chat" }), + createdTask({ parentId: null, identifier: null }), + ] }); + const cards = host.querySelectorAll('[data-testid="task-chat-created-task"]'); + expect(cards).toHaveLength(1); + expect(cards[0].textContent).toContain("Task created"); + expect(cards[0].getAttribute("href")).toBe("/issues/child"); + }); + + it("does not duplicate tasks already shown by chat handoffs or accepted suggestions", async () => { + state.handoffs = [handoff("direct")]; + await paint({ companyId: "company", interactions: [suggestion("accepted")], createdTasks: [ + createdTask({ id: "direct" }), createdTask({ id: "suggested-task" }), createdTask(), + ] }); + expect(host.querySelectorAll('[data-testid="task-chat-created-task"]')).toHaveLength(1); + expect(anchor("chat_handoff:direct")).toBeTruthy(); + expect(anchor("interaction:suggestion").querySelector(".coordinator-created-task")).toBeTruthy(); + }); + it("keeps the saved plan before its resulting document even when both belong to one native run", async () => { state.plan = { id: "plan", issueId: "chat", companyId: "company", key: "plan", title: "Plan", body: "Draft the concepts.", format: "markdown", latestRevisionId: "plan-rev", latestRevisionNumber: 1, diff --git a/ui/src/components/TaskChatThread.tsx b/ui/src/components/TaskChatThread.tsx index e68b2fbcaa7..e89adcb5a54 100644 --- a/ui/src/components/TaskChatThread.tsx +++ b/ui/src/components/TaskChatThread.tsx @@ -10,9 +10,10 @@ import { import { hasWorkspaceRestoreFailure } from "@paperclipai/shared"; import { workspaceRestoreMarkerDetail } from "@/lib/workspace-restore-marker"; import { credentialAccessNotice } from "@/lib/credential-access-notice"; -import type { ActivityEvent, IssueQueuedCommentQueue, TaskBrowser } from "@paperclipai/shared"; +import type { ActivityEvent, Issue, IssueQueuedCommentQueue, TaskBrowser } from "@paperclipai/shared"; import { useProjectCreatedItems } from "@/hooks/useProjectCreatedItems"; import { skillCreatedItems } from "@/components/task-chat/skill-created-items"; +import { taskCreatedItems } from "@/components/task-chat/task-created-items"; import { useCoordinatorPresentation } from "@/components/coordinator/CoordinatorPresentation"; import { CoordinatorChatHandoffStatus, CoordinatorChatTaskHandoff, standaloneChatHandoffs, useCoordinatorChatTaskHandoffs, useLiveChatHandoffIds } from "@/components/coordinator/CoordinatorChatTaskHandoffs"; import { CoordinatorChatReceiptStatus, useCoordinatorChatDecisionReceipts } from "@/components/coordinator/CoordinatorChatDecisionReceipts"; @@ -429,6 +430,7 @@ function resolvedWithoutUserFacingResponse(value: unknown): boolean { export type TaskChatThreadProps = ComponentProps & { conversationMode?: boolean; creationActivity?: ActivityEvent[]; + createdTasks?: Issue[]; compactHistory?: boolean; scrollingHistory?: ScrollingHistory; historyControl?: ReactNode; @@ -636,6 +638,14 @@ export function TaskChatThread(props: TaskChatThreadProps) { const liveHandoffIds = useLiveChatHandoffIds(`${currentUserId}:${issueId}`, taskHandoffs.handoffs, taskHandoffs.isSuccess); const createdProjectItems = useProjectCreatedItems(props.creationActivity ?? [], companyId); const createdSkillItems = useMemo(() => skillCreatedItems(props.creationActivity ?? []), [props.creationActivity]); + const createdTaskItems = useMemo(() => { + const representedTaskIds = new Set(taskHandoffs.handoffs.map((handoff) => handoff.issueId)); + for (const interaction of interactions ?? []) { + if (interaction.kind !== "suggest_tasks" || interaction.status !== "accepted" || isSuppressedThreadInteraction(interaction)) continue; + for (const task of interaction.result?.createdTasks ?? []) representedTaskIds.add(task.issueId); + } + return taskCreatedItems(props.createdTasks ?? [], issueId, companyId, representedTaskIds); + }, [props.createdTasks, issueId, companyId, taskHandoffs.handoffs, interactions]); const [pendingComposerAssignee, setPendingComposerAssignee] = useState< string | null >(null); @@ -1420,6 +1430,9 @@ export function TaskChatThread(props: TaskChatThreadProps) { for (const item of createdSkillItems) { entries.push({ id: item.id, item, ms: toMs(item.timestamp), order: 2 }); } + for (const item of createdTaskItems) { + entries.push({ id: item.id, item, ms: toMs(item.timestamp), order: 3 }); + } // Older sessions arrive with their surrounding messages when history is // paged in; otherwise they would collect above the loaded conversation. const historyStart = hasOlderComments @@ -1447,6 +1460,7 @@ export function TaskChatThread(props: TaskChatThreadProps) { }, [ createdProjectItems, createdSkillItems, + createdTaskItems, browsers, hasOlderComments, comments, diff --git a/ui/src/components/task-chat/TaskChatTaskCreatedCard.tsx b/ui/src/components/task-chat/TaskChatTaskCreatedCard.tsx new file mode 100644 index 00000000000..0cda27a57d3 --- /dev/null +++ b/ui/src/components/task-chat/TaskChatTaskCreatedCard.tsx @@ -0,0 +1,27 @@ +import { ArrowUpRight, ListTree } from "lucide-react"; +import { Link } from "@/lib/router"; +import { IssueStatusBadge } from "@/components/StatusBadge"; +import type { TaskChatTaskCreatedItem } from "./task-chat-model"; + +export function TaskChatTaskCreatedCard({ item }: { item: TaskChatTaskCreatedItem }) { + const { task } = item; + return ( + + + + {item.isSubtask ? "Subtask created" : "Task created"} + {task.title} + + {task.identifier && {task.identifier}} + + + + + + ); +} diff --git a/ui/src/components/task-chat/TaskChatThreadView.tsx b/ui/src/components/task-chat/TaskChatThreadView.tsx index 7c95a95f071..024c5ca1967 100644 --- a/ui/src/components/task-chat/TaskChatThreadView.tsx +++ b/ui/src/components/task-chat/TaskChatThreadView.tsx @@ -1,6 +1,7 @@ import { TaskBrowserActivity } from "../task-side-panel/TaskBrowserActivity"; import { TaskChatProjectCreatedCard } from "./TaskChatProjectCreatedCard"; import { TaskChatSkillCreatedCard } from "./TaskChatSkillCreatedCard"; +import { TaskChatTaskCreatedCard } from "./TaskChatTaskCreatedCard"; import { useMemo, type ReactNode } from "react"; import type { IssueAttachment } from "@paperclipai/shared"; import { cn } from "@/lib/utils"; @@ -111,6 +112,7 @@ function renderItem( case "browser": return ; case "project_created": return ; case "skill_created": return ; + case "task_created": return ; case "message": { // Compute the actions once: the bubble renders them for a runless reply // (footer = actions + timestamp), while an attached turn hands them to diff --git a/ui/src/components/task-chat/task-chat-model.ts b/ui/src/components/task-chat/task-chat-model.ts index d53fbe06b0d..b2c877a26f3 100644 --- a/ui/src/components/task-chat/task-chat-model.ts +++ b/ui/src/components/task-chat/task-chat-model.ts @@ -13,6 +13,7 @@ * CSS motion tokens in ui/src/index.css. */ import type { + Issue, TaskBrowser, IssueAttachment, IssueCommentMetadata, @@ -556,6 +557,14 @@ export interface TaskChatTurnItem { }; } +export interface TaskChatTaskCreatedItem { + id: string; + kind: "task_created"; + task: Pick; + isSubtask: boolean; + timestamp: string; +} + export interface TaskChatProjectCreatedItem { id: string; kind: "project_created"; @@ -590,6 +599,7 @@ export type TaskChatItem = | { id: string; kind: "decision_receipt"; receipt: DecisionHistoryItem } | { id: string; kind: "task_handoff"; handoff: ConversationTaskHandoff; animateEntry?: boolean } | TaskChatBrowserItem + | TaskChatTaskCreatedItem | TaskChatProjectCreatedItem | TaskChatSkillCreatedItem | TaskChatMessageItem diff --git a/ui/src/components/task-chat/task-created-items.ts b/ui/src/components/task-chat/task-created-items.ts new file mode 100644 index 00000000000..b7ac4b47a76 --- /dev/null +++ b/ui/src/components/task-chat/task-created-items.ts @@ -0,0 +1,23 @@ +import type { Issue } from "@paperclipai/shared"; +import type { TaskChatTaskCreatedItem } from "./task-chat-model"; + +/** Tasks come from the existing access-checked child/source-task reads, never tool output. */ +export function taskCreatedItems( + tasks: readonly Issue[], + issueId: string | null, + companyId: string | null | undefined, + representedTaskIds: ReadonlySet, +): TaskChatTaskCreatedItem[] { + const seen = new Set(representedTaskIds); + return tasks.flatMap((task) => { + if (!task.createdByAgentId || task.id === issueId || seen.has(task.id) || task.companyId !== companyId) return []; + seen.add(task.id); + return [{ + id: `task-created:${task.id}`, + kind: "task_created" as const, + task: { id: task.id, identifier: task.identifier, title: task.title, status: task.status }, + isSubtask: task.parentId === issueId, + timestamp: new Date(task.createdAt).toISOString(), + }]; + }); +} diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx index b8206f5e886..9c9d8d40570 100644 --- a/ui/src/pages/IssueDetail.test.tsx +++ b/ui/src/pages/IssueDetail.test.tsx @@ -1567,6 +1567,44 @@ describe("IssueDetail", () => { await waitForAssertion(() => expect(mockIssueChatThreadRender).toHaveBeenCalled()); }); + it("supplies direct children and source-created tasks to the feed without promoting grandchildren", async () => { + const issue = createIssue(); + const child = createIssue({ id: "child", parentId: issue.id, createdByAgentId: "agent-1" }); + const grandchild = createIssue({ id: "grandchild", parentId: child.id, createdByAgentId: "agent-1" }); + const followup = createIssue({ id: "followup", createdByAgentId: "agent-1" }); + mockIssuesApi.get.mockResolvedValue(issue); + mockIssuesApi.listAll.mockImplementation(async (_companyId, filters) => filters?.createdFromIssueId ? [child, followup] : [child, grandchild]); + await act(async () => renderIssueView()); + await waitForAssertion(() => expect(mockIssueChatThreadRender.mock.calls.at(-1)?.[0]?.createdTasks).toEqual([child, child, followup])); + const updated = { ...child, status: "done" }; + await act(async () => { queryClient.setQueryData(queryKeys.issues.listByDescendantRoot(issue.companyId, issue.id), [updated, grandchild]); }); + await waitForAssertion(() => expect(mockIssueChatThreadRender.mock.calls.at(-1)?.[0]?.createdTasks[0]).toMatchObject({ id: "child", status: "done" })); + }); + + it("keeps the conversation readable when created tasks fail to load and offers a retry", async () => { + mockIssuesApi.get.mockResolvedValue(createIssue()); + mockIssuesApi.listAll.mockRejectedValue(new Error("Task read unavailable")); + await act(async () => renderIssueView()); + await waitForAssertion(() => expect(container.textContent).toContain("Could not load created tasks.")); + expect(mockIssueChatThreadRender.mock.calls.at(-1)?.[0]?.initialHistoryError).toBe(false); + mockIssuesApi.listAll.mockResolvedValue([]); + const retry = Array.from(container.querySelectorAll("button")).find((button) => button.textContent === "Retry")!; + await act(async () => retry.click()); + await waitForAssertion(() => expect(container.textContent).not.toContain("Could not load created tasks.")); + }); + + it("loads standalone follow-up cards in the task feed when Streamlined UI is off", async () => { + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableStreamlinedUi: false, enableClassicTaskInterface: false }); + const issue = createIssue(); + const followup = createIssue({ id: "standalone", parentId: null, createdByAgentId: "agent-1" }); + mockIssuesApi.get.mockResolvedValue(issue); + mockIssuesApi.listAll.mockImplementation(async (_companyId, filters) => filters?.createdFromIssueId ? [followup] : []); + await act(async () => renderIssueView()); + await waitForAssertion(() => expect(mockIssueChatThreadRender.mock.calls.at(-1)?.[0]?.createdTasks).toEqual([followup])); + expect(container.querySelector('[data-testid="task-chat-thread"]')).not.toBeNull(); + expect(mockIssuesApi.listAll).toHaveBeenCalledWith(issue.companyId, { createdFromIssueId: issue.id, includeRoutineExecutions: true }); + }); + it("uses Coordinator task presentation and panel scope without changing the Classic preference", async () => { coordinatorPreference.enabled = true; mockInstanceSettingsApi.getExperimental.mockResolvedValue({ diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index aa36865e183..42cc6ddf0c9 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -1281,6 +1281,7 @@ type IssueDetailChatTabProps = { locallyQueuedCommentRunIds: ReadonlyMap; interactions: IssueThreadInteraction[]; documents: IssueDocumentSummary[]; + createdTasks: Issue[]; workProducts: IssueWorkProduct[]; attachments: IssueAttachment[]; hasOlderComments: boolean; @@ -1438,6 +1439,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ locallyQueuedCommentRunIds, interactions, documents, + createdTasks, workProducts, attachments, hasOlderComments, @@ -2655,7 +2657,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ interruption={interruption} retryPreparing={retryPreparing} key={conversationMode ? draftKey : issueId} - {...(!classicTaskInterfaceEnabled ? { creationActivity: resolvedActivity } : {})} + {...(!classicTaskInterfaceEnabled ? { creationActivity: resolvedActivity, createdTasks } : {})} onOpenSkill={onOpenSkill} browsers={browsers} onOpenBrowser={onOpenBrowser} @@ -3680,7 +3682,9 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS createdFromIssueId: issue!.id, includeRoutineExecutions: true, }), - enabled: streamlinedTaskDetailEnabled && !!resolvedCompanyId && !!issue?.id && !tasksTab, + enabled: !!resolvedCompanyId && !!issue?.id && !issue.id.startsWith("chat:") && ( + (taskChatShellEnabled && !conversation) || (streamlinedTaskDetailEnabled && !tasksTab) + ), }); const { @@ -3959,6 +3963,10 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), ); }, [issue?.id, rawChildIssues]); + const threadCreatedTasks = useMemo(() => [ + ...childIssues.filter((task) => task.parentId === issue?.id), + ...(createdTasksQuery.data ?? EMPTY_ISSUES), + ], [childIssues, createdTasksQuery.data, issue?.id]); const resolvedTasksTab = useMemo(() => { if (tasksTab) return tasksTab; if (!streamlinedTaskDetailEnabled) return undefined; @@ -8466,7 +8474,17 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS onOpenSkill={handleOpenSkill} browsers={browserQuery.data} onOpenBrowser={(id) => { setOpenBrowserId(id); if (isMobile) setMobilePropsOpen(true); else openTaskSidePanel(); }} - threadHeader={<>{taskChatThreadHeader}} + threadHeader={<>{taskChatThreadHeader} + {(childIssuesError || createdTasksQuery.isError) && ( +
+ Could not load created tasks. + +
+ )} + } issueBrief={ // Suppress the seeded-description bubble for the onboarding first // task: its description is agent instructions, not something the @@ -8568,6 +8586,7 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS locallyQueuedCommentRunIds={locallyQueuedCommentRunIds} interactions={interactions} documents={issue.documentSummaries ?? []} + createdTasks={threadCreatedTasks} workProducts={workProducts ?? issue.workProducts ?? []} attachments={attachments ?? []} hasOlderComments={paginatedConversation && !historyExpanded diff --git a/ui/storybook/stories/task-coordination/task-created-inline.stories.tsx b/ui/storybook/stories/task-coordination/task-created-inline.stories.tsx new file mode 100644 index 00000000000..6f3d1f1ae5a --- /dev/null +++ b/ui/storybook/stories/task-coordination/task-created-inline.stories.tsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, within } from "storybook/test"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { Issue, IssueComment, IssueDocumentSummary } from "@paperclipai/shared"; +import { TaskChatThread } from "@/components/TaskChatThread"; +import { storybookAgents, storybookIssues } from "../../fixtures/paperclipData"; +import { queryKeys } from "@/lib/queryKeys"; + +const parent = storybookIssues[0]!; +const at = (minute: number) => new Date(`2026-10-30T09:${String(minute).padStart(2, "0")}:00Z`); +const task: Issue = { + ...parent, id: "inline-subtask", parentId: parent.id, identifier: "PAP-42", + title: "Fix incomplete search results when the task list has multiple pages", + status: "in_progress", createdByAgentId: "agent-1", createdAt: at(2), +}; +const comments: IssueComment[] = [ + { id: "request", authorType: "user", authorUserId: "viewer", body: "Review the new findings and create a task for each repair that needs work.", createdAt: at(0) }, + { id: "progress", authorType: "agent", authorAgentId: "agent-1", body: "I found one actionable issue. I’ll create a subtask for the repair and save the batch report here.", createdAt: at(1) }, + { id: "response", authorType: "agent", authorAgentId: "agent-1", body: "Created one repair task. The batch report has the source evidence and selection details.", createdAt: at(4) }, +].map((comment) => ({ + ...comment, authorType: comment.authorType as "user" | "agent", + authorAgentId: comment.authorAgentId ?? null, authorUserId: comment.authorUserId ?? null, + presentation: null, metadata: null, companyId: parent.companyId, issueId: parent.id, updatedAt: comment.createdAt, +})); +const report = { + id: "report", issueId: parent.id, companyId: parent.companyId, key: "batch-report", title: "Batch report", + format: "markdown", latestRevisionNumber: 1, latestRevisionId: "report-1", createdAt: at(3), updatedAt: at(3), +} as IssueDocumentSummary; + +function TaskFeed({ multiple = false }: { multiple?: boolean }) { + const [client] = useState(() => { + const cache = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } }); + cache.setQueryData([...queryKeys.issues.documents(parent.id), "plan"], null); + return cache; + }); + return +
+ {}} showComposer={false} + threadHeader={

Task feed

Daily findings and repair tasks

} + /> +
+
; +} + +const meta = { + id: "task-created-inline", + title: "Tasks and conversations/Delegation, dependencies, and signoff/Created task cards", + component: TaskFeed, + parameters: { + layout: "fullscreen", initialEntries: ["/PAP/issues/parent"], + paperclip: { + capabilities: ["task-coordination/delegation", "tasks/discussion"], + implementation: "production", boundary: "fixtures", + notes: "Production TaskChatThread assembles created task cards beside a saved document. Task reads and status are fixtures; no agent runs or task writes occur.", + }, + }, + globals: { theme: "light" }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const InlineSubtask: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const card = await canvas.findByTestId("task-chat-created-task"); + await expect(card).toHaveTextContent("Subtask created"); + await expect(card).toHaveTextContent("In progress"); + await expect(card).toHaveAttribute("href", "/PAP/issues/PAP-42"); + await expect(canvas.getAllByTestId("task-chat-created-task")).toHaveLength(1); + }, +}; +export const MultipleSubtasks: Story = { args: { multiple: true } }; +export const Dark: Story = { globals: { theme: "dark" } }; +export const Mobile: Story = { args: { multiple: true }, globals: { viewport: { value: "mobile", isRotated: false } } };