diff --git a/DESIGN.md b/DESIGN.md index eff137181a8..a9baddf65b4 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -47,6 +47,10 @@ Completed Slack setup is a success view: show the full agent avatar, success mes and one centered Done action. Omit Save & exit after the connection has been confirmed. The shared footer rule applies to its unfinished setup steps. +The New task form opens immediately. Load its code with the board and fill +agent and project choices in place as their requests finish. Keep text entered +while those requests are pending. + ## Mobile navigation and text fields The fixed bottom navigation uses an opaque surface so scrolling content cannot diff --git a/maps/feature-map/tasks.md b/maps/feature-map/tasks.md index 639bd31c4f3..b9e747561eb 100644 --- a/maps/feature-map/tasks.md +++ b/maps/feature-map/tasks.md @@ -67,10 +67,12 @@ Use these checks alongside the entry-point recipes below and the operation recor ### `new-task` -Automated: [creation dialog](../../ui/src/components/NewIssueDialog.test.tsx) is component coverage with mocked requests. +Automated: [board dialogs](../../ui/src/components/BoardDialogs.test.tsx) checks that the task form appears on the first synchronous open and retains its draft across reopen. [Creation dialog](../../ui/src/components/NewIssueDialog.test.tsx) covers typing while agent/project requests are pending and retaining that text when defaults arrive. These are component checks with mocked requests. Manual: Create a uniquely named task with an agent and project. Open its resulting link and reload. Confirm text, attachment, assignment, and project persisted, and verify execution started only when the task and agent are eligible. +On the first New task open after a page load, confirm the form appears without a loading dialog. Delay agent and project responses, type a request, then release the responses. Confirm the choices populate without replacing the form or its text. + Creation is request-first: enter **Describe a task…**, choose the assignee and project, and submit **Create task**. A standalone title field is not always shown. Use a human/unassigned task to verify persistence without invoking a model. diff --git a/ui/src/components/BoardDialogs.test.tsx b/ui/src/components/BoardDialogs.test.tsx index 08575ea269b..564aedd42aa 100644 --- a/ui/src/components/BoardDialogs.test.tsx +++ b/ui/src/components/BoardDialogs.test.tsx @@ -1,5 +1,6 @@ // @vitest-environment jsdom import { act } from "react"; +import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { BoardDialogs } from "./BoardDialogs"; @@ -23,6 +24,15 @@ afterEach(async () => { container.remove(); }); async function render() { await act(async () => { root.render(); }); } +it("renders the task form on the first open without an asynchronous loading frame", () => { + act(() => flushSync(() => root.render())); + state.newIssueOpen = true; + act(() => { + flushSync(() => root.render()); + expect(container.querySelector('[aria-label="Task draft"]')).not.toBeNull(); + expect(document.body.textContent).not.toContain("Loading new task"); + }); +}); it("leaves unopened dialog trees unmounted", async () => { await render(); expect(container.innerHTML).toBe(""); diff --git a/ui/src/components/BoardDialogs.tsx b/ui/src/components/BoardDialogs.tsx index c4c17d798f7..eda574e646b 100644 --- a/ui/src/components/BoardDialogs.tsx +++ b/ui/src/components/BoardDialogs.tsx @@ -1,8 +1,8 @@ import { lazy } from "react"; import { useDialogActions, useDialogState } from "../context/DialogContext"; import { DeferredOverlay } from "./DeferredOverlay"; +import { NewIssueDialog } from "./NewIssueDialog"; -const NewIssueDialog = lazy(() => import("./NewIssueDialog").then((module) => ({ default: module.NewIssueDialog }))); const NewProjectDialog = lazy(() => import("./NewProjectDialog").then((module) => ({ default: module.NewProjectDialog }))); const NewGoalDialog = lazy(() => import("./NewGoalDialog").then((module) => ({ default: module.NewGoalDialog }))); const NewAgentDialog = lazy(() => import("./NewAgentDialog").then((module) => ({ default: module.NewAgentDialog }))); diff --git a/ui/src/components/NewIssueDialog.test.tsx b/ui/src/components/NewIssueDialog.test.tsx index 8f1ebb57cc3..e8e7cb53dcc 100644 --- a/ui/src/components/NewIssueDialog.test.tsx +++ b/ui/src/components/NewIssueDialog.test.tsx @@ -425,6 +425,33 @@ describe("NewIssueDialog", () => { { id: "ceo", name: "CEO", role: "ceo", status: "idle", adapterType: "claude_local", adapterConfig: {}, runtimeConfig: {}, permissions: {} }, ]; + it("accepts a draft while agents and projects load, then fills defaults in place", async () => { + let resolveAgents!: (agents: typeof defaultAgents) => void; + let resolveProjects!: (projects: { id: string; name: string; archivedAt: null }[]) => void; + mockAgentsApi.list.mockImplementationOnce(() => new Promise((resolve) => { resolveAgents = resolve; })); + mockProjectsApi.list.mockImplementationOnce(() => new Promise((resolve) => { resolveProjects = resolve; })); + trackRecentProject("project-1", "company-1"); + + const { root } = renderDialog(container); + try { + const editor = container.querySelector('textarea[aria-label="Describe a task…"]')!; + expect(editor).not.toBeNull(); + expect(editor.disabled).toBe(false); + await typeTextareaValue(editor, "Keep writing while the lists load"); + + resolveAgents(defaultAgents); + resolveProjects([{ id: "project-1", name: "Alpha", archivedAt: null }]); + await waitForAssertion(() => { + expect(container.querySelector('[data-slot="new-issue-compact-control"]')?.textContent).toContain("project-1"); + expect(container.textContent).toContain("CEO"); + }); + expect(container.querySelector('textarea[aria-label="Describe a task…"]')).toBe(editor); + expect(editor.value).toBe("Keep writing while the lists load"); + } finally { + act(() => root.unmount()); + } + }); + it.each([ { remembered: "project-1", expected: "project-1" }, { remembered: "", expected: "Project" },