fix(ui): open the new-task form immediately (#17041)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People use New task to send work to an agent.
> - The board currently downloads the task dialog code after the first
click.
> - This shows a loading window before people can enter their request.
> - The form already loads agent and project choices after it opens.
> - This pull request loads the task dialog code with the board.
> - People can start writing immediately and keep their text as choices
arrive.

## Linked Issues or Issue Description

Related change: #16849. Commit
`fa50cab5685b9cdbb26ad9c610cc5b6c373d3ff4` added the deferred dialog and
its loading window on October 19, 2026.

**What happened?**

The first New task click shows “Loading new task…” while the browser
downloads the dialog code.

**Expected behavior**

The form opens on click. Agent and project choices load in place. Text
entered during those requests stays in the form.

**Steps to reproduce**

1. Open the board in a fresh browser session.
2. Select New task before its code is cached.
3. Observe the loading window before the form appears.

**Paperclip version or commit**

Source baseline: `8b4ab65b1c3d863d282752e83307943720a5c85c`.

**Deployment mode**

The browser code applies to local and hosted boards. Verification used a
source checkout.

## What Changed

- Import NewIssueDialog with the board. Keep the existing mount and
draft lifecycle.
- Add a first-open test that fails if the form waits for an asynchronous
render.
- Cover text entry during delayed agent and project requests and
defaults that arrive later.
- Document the behavior and its verification recipe.

## Verification

- Regression proof: the first-open test failed before the import change
and passed after it.
- All 125 tests in BoardDialogs, DeferredOverlay, NewIssueDialog and
Layout passed.
- The production UI build and UI TypeScript check passed. The typecheck
passed with `NODE_OPTIONS=--max-old-space-size=2400` after the competing
test process was stopped.
- Token gates, map validation and Git diff checks passed.
- The first UI typecheck could not resolve the unbuilt local plugin SDK.
The SDK was then built.
- Full typecheck and build attempts hit the sandbox's 4 GB memory limit
when run with the full test suite. Two later UI typecheck attempts also
hit that limit during other validation. The cgroup recorded four OOM
kills in total. The full repository test attempt and a broader UI test
attempt were stopped. No complete suite result is claimed. Serial
repository typecheck reached the missing Rust `cargo` executable. Full
CI passed on head `9dde4bc`; local limits remain recorded here.
- First CI run at `e5a99b8`: typecheck, build, all unit-test shards and
seven browser shards passed. Browser shard 2 failed at
`chat-adapters-ui-providers.spec.ts:185`, waiting for Slack setup
Success after reload. The saved page showed the working connection and
Continue button. The cause is not confirmed; this failure is retained
for follow-up. [Failed
job](https://github.com/paperclipai/paperclip/actions/runs/39522214527/job/118968542923).
- Removed the added import comment after Greptile review. Head `9dde4bc`
changes only that comment. The review thread is resolved. Fresh CI
passed on head `9dde4bc`. Greptile reviewed that exact head at 5/5 with
no open findings. All eight browser shards passed, including the earlier
Slack failure. The earlier failure remains unexplained; the only
intervening code change removed a source comment. A passing retry
establishes recovery, not its cause.
- Component tests use mocked directory requests. No deployed latency or
browser persistence claim is made.

Latest CI: [run
39523376437](https://github.com/paperclipai/paperclip/actions/runs/39523376437).
Reviewed head: `9dde4bc0caa3556e315a2fc197268b7c9879efa4`.

## Risks

- Task dialog code is part of board startup again. This adds its
dependencies to the initial code graph. No startup latency benchmark is
claimed.
- Other creation dialogs retain their existing lazy imports and loading
behavior.

## Model Used

OpenAI Codex, GPT-6, with reasoning, repository tools and code
execution. The runtime does not expose a more precise model ID or
context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-24 08:15:35 -07:00
1 parent 87bf89756e
commit f66d590899
5 files changed
+45 -2

No files matched your search

+4
View File
@@ -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
+3 -1
View File
@@ -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.
+10
View File
@@ -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(<BoardDialogs />); }); }
it("renders the task form on the first open without an asynchronous loading frame", () => {
act(() => flushSync(() => root.render(<BoardDialogs />)));
state.newIssueOpen = true;
act(() => {
flushSync(() => root.render(<BoardDialogs />));
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("");
+1 -1
View File
@@ -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 })));
+27
View File
@@ -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<HTMLTextAreaElement>('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" },