From 02bfe71a6bbd7c5676cb7e72778b361c5bc72765 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:49:31 -0600 Subject: [PATCH] fix(ui): keep new-task composer stable during submission (#20365) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - People use the New task dialog to send work to an agent. > - The dialog uses the shared task composer. > - A send request added an empty header with a bottom margin. > - That margin increased the composer height by 12 pixels. > - This pull request shows the header only when it has content. > - The composer keeps its size and position while the request runs. ## Linked Issues or Issue Description **What happened?** The New task composer grew by 12 pixels when a person sent a task with a description and no separate title. On desktop, the centered dialog moved up by 6 pixels and the editor moved down by 6 pixels. On mobile, the editor moved down by 12 pixels. **Expected behavior** The composer, editor, and dialog must keep their size and position while the task request runs. **Steps to reproduce** 1. Open New task. 2. Enter a task description without a separate title. 3. Send the task while the create request is still pending. 4. Compare the composer and editor bounds before and after the send. **Paperclip version or commit** Reproduced on `0e3fdebea8` in the production NewIssueDialog component through its existing Storybook story. **Deployment mode** Local source checkout. The browser check uses fixture requests and starts no agent runs. No duplicate fix was found in the open PR and issue search. Related open PR #19725 changes keyboard and pinch-zoom sizing; it does not change the empty header during a task send. ## What Changed - Keep the header absent during a normal send when it has no title, subtask context, or notice. - Keep the existing title, subtask context, connection retry notice, and error message. - Extend the submit test to check stable composer sections for description-only tasks, titled tasks, and subtasks. ## Verification - `pnpm exec vitest run ui/src/components/NewIssueDialog.test.tsx ui/src/components/task-chat/TaskChatComposer.test.tsx`: 253 tests passed. - The new description-only regression failed on the original code because an empty header appeared. It passed after the fix. The title and subtask cases passed in both versions. - Chromium browser check at viewport widths 1280 and 390: the original composer grew by 12 pixels. After the fix, all measured bounds for the composer, editor, and dialog stayed equal before and during the send. - `pnpm check:token-gates`: passed. - `git diff --check`: passed. - No documentation change is needed. The task creation contract stays the same. - The worktree preflight installed dependencies but could not build Runner because Cargo is absent. UI verification completed. [CI passed](https://github.com/paperclipai/paperclip/actions/runs/42702272345) for `53aa01c169559da1b31fd224c60ebeabd4ccf9c5`. It covers typecheck, build, general tests, and all eight end-to-end test shards. ## Risks - Low risk. The change only controls an empty header during a normal send. Existing tests cover connection retry and error notices. - No API, database, dependency, or design token changes. ## Model Used - OpenAI GPT-6 (Codex), with reasoning, tool use, and code execution. The session does not expose a more specific backend 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 --- ui/src/components/NewIssueDialog.test.tsx | 14 +++++++++++--- ui/src/components/NewIssueDialog.tsx | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ui/src/components/NewIssueDialog.test.tsx b/ui/src/components/NewIssueDialog.test.tsx index c49d9e9cbf9..6ab66bf031e 100644 --- a/ui/src/components/NewIssueDialog.test.tsx +++ b/ui/src/components/NewIssueDialog.test.tsx @@ -1503,22 +1503,30 @@ describe("NewIssueDialog", () => { act(() => root.unmount()); }); - it("shows the create-task loading state only in the submit button", async () => { + it.each([ + { name: "description only", defaults: { description: "Pending task" } }, + { name: "explicit title", defaults: { title: "Pending task" } }, + { name: "sub-task", defaults: { description: "Pending task", parentId: "parent-1", parentTitle: "Parent task" } }, + ])("keeps the composer layout stable while submitting a task with $name", async ({ defaults }) => { mockIssuesApi.create.mockReturnValue(new Promise(() => undefined)); - dialogState.newIssueDefaults = { title: "Pending task" }; + dialogState.newIssueDefaults = defaults; const { root } = renderDialog(container); await flush(); const submitButton = Array.from(container.querySelectorAll("button")) - .find((button) => button.getAttribute("aria-label") === "Create task"); + .find((button) => button.getAttribute("data-testid") === "task-chat-composer-send"); expect(submitButton).not.toBeUndefined(); + const composer = container.querySelector(".paperclip-task-chat-composer")!; + const sections = Array.from(composer.children); await act(async () => { submitButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); await flush(); + expect(mockIssuesApi.create).toHaveBeenCalledTimes(1); + expect(Array.from(composer.children)).toEqual(sections); expect(submitButton?.querySelector(".animate-spin")).not.toBeNull(); expect(submitButton?.getAttribute("aria-busy")).toBe("true"); expect(container.textContent).not.toContain("Creating issue"); diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 94796956e4d..82886dc1b37 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -1467,7 +1467,7 @@ function AccountNewIssueDialog({ ignoreDefaults }: { ignoreDefaults: boolean }) runSettings: composerSettings, onRunSettingsChange: setComposerSettings, header: - hasTitle || isSubIssueMode || durableCreate.pending || resendError ? ( + hasTitle || isSubIssueMode || (durableCreate.pending && durableCreate.status !== "sending") || resendError ? (
{durableCreate.pending && durableCreate.status !== "sending" ? (