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 <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-11-09 15:49:31 -06:00
1 parent 0e3fdebea8
commit 02bfe71a6b
2 files changed
+12 -4

No files matched your search

+11 -3
View File
@@ -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");
+1 -1
View File
@@ -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 ? (
<div className="mb-3 flex flex-col gap-2 text-xs">
{durableCreate.pending && durableCreate.status !== "sending" ? (
<PendingSendNotice