mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
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:
1 parent
87bf89756e
commit
f66d590899
5 files changed
+45
-2
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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 })));
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user