#3385 · Keyboard event submits twice
2026-09-10 · trusted main commit 04f4e6a21c1efa6d82d9d62a094b87796c36439c
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
One modified Enter keypress in the new-task title field sends two createTask requests. The title handler submits, then the same event bubbles to the dialog's shortcut handler and submits again. React's pending state change does not update either handler's captured canSubmit value during that event. Both Ctrl+Enter and Meta+Enter reproduce; plain Enter and modified Enter outside the title field send one request.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Ctrl+Enter in the title duplicates creation | Verified at RPC boundary | Two clean runs each count 2 requests instead of 1. |
| Plain Enter and Ctrl+Enter elsewhere submit once | Verified | Title plain Enter and due-date Ctrl+Enter controls pass in both runs. |
| Button click submits once | Verified | Existing management test passes in the full post-fix suite. |
| Separate persisted task numbers | Source-supported; not observed live | Each API invocation inserts a task and allocates a number; the UI reproduction uses an RPC fixture. |
| The number allocator causes the duplicate | Ruled out as origin | Two calls already exist at the UI boundary before any database operation. |
3. Environment
bb 0.42.1 source, macOS Darwin arm64, Node v22.22.3, pnpm 10.34.4, Vitest 4.1.1 and jsdom. Two separate temporary clones named base and verify, both at the commit above. Frozen installs succeeded. The base full build passed (20 tasks); the verification checkout's Tasks build passed (6 tasks). No servers, ports, data stores, providers, or production runtime data were used. A broken host pnpm launcher was bypassed with a temporary Corepack shim; no repository dependency changed.
4. Minimal reproduction
- Download the regression patch, which only adds tests to an existing suite.
- Run the following in a fresh checkout:
git clone https://github.com/get-bb/bb.git bb-3385 cd bb-3385 git checkout --detach 04f4e6a21c1efa6d82d9d62a094b87796c36439c pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build git apply /path/to/regression.patch pnpm exec turbo run test --force --filter=bb-plugin-tasks -- views/manage/manage.test.tsx -t 'submits exactly once'
The test opens New task through the real plugin panel, enters a title, dispatches the keyboard event, waits for navigation, and counts captured createTask RPC requests. Its control cases vary the modifier and target field.
Expected: each case sends one request. Actual in both clean runs:
FAIL Ctrl+Enter in title
FAIL Meta+Enter in title
AssertionError: expected [ { …(8) }, { …(8) } ] to have a length of 1 but got 2
Tests 2 failed | 2 passed | 27 skipped (31)
Complete runnable test file (existing harness included). Added regression cases:
it.each([
{ name: "Ctrl+Enter in title", field: "Task title", ctrlKey: true, metaKey: false },
{ name: "Meta+Enter in title", field: "Task title", ctrlKey: false, metaKey: true },
{ name: "Enter in title", field: "Task title", ctrlKey: false, metaKey: false },
{ name: "Ctrl+Enter in due date", field: "Due date", ctrlKey: true, metaKey: false },
])("submits exactly once for $name", async ({ field, ctrlKey, metaKey }) => {
const createCalls: Array<Record<string, unknown>> = [];
const slot = renderSlot(
app.navPanels[0]!,
{ subPath: PROJECT_ID },
{
rpc: {
listProjects: () => ({ projects: [project] }),
listFolders: () => ({ folders: [] }),
listPresets: () => ({ presets: [] }),
sidebarSummary: () => ({ projects: [] }),
listTasks: () => ({ tasks: [] }),
listLabels: () => ({ labels: [] }),
createTask: (input: Record<string, unknown>) => {
createCalls.push(input);
return { ok: true, task: createdTask(input) };
},
},
},
);
fireEvent.click(await slot.findByRole("button", { name: /New task/ }));
fireEvent.change(await slot.findByLabelText("Task title"), {
target: { value: "Keyboard submission" },
});
fireEvent.keyDown(slot.getByLabelText(field), {
key: "Enter",
ctrlKey,
metaKey,
});
await waitFor(() => expect(slot.navigateCalls).not.toHaveLength(0));
expect(createCalls).toHaveLength(1);
expect(createCalls[0]).toMatchObject({ title: "Keyboard submission" });
});
5. Root cause
The title input's Enter handler prevents the browser default and calls submit, but does not stop event propagation. The dialog's Ctrl/Meta+Enter handler receives that same event and submits again. canSubmit and submit use render-time state: setSubmitting(true) schedules a render, so the parent handler still sees canSubmit=true.
title keydown → submit → setSubmitting(true) → createTask same event bubbles → dialog keydown → submit → createTask
The API creates a task for each invocation. The store allocates a number and inserts each task; it does not deduplicate matching titles. This explains the reported separate numbers, although persisted rows were not exercised in this reproduction.
6. Proposed fix
Stop propagation immediately after preventing the default in the title's existing Enter handler. This gives the consumed title event one submission owner and preserves the dialog shortcut for other fields. The local fix is one production line plus four regression cases: 60 additions, zero deletions, two files in Tasks. It changes no public contract or stored data. The complete Tasks suite passes: 35 files, 368 tests; Tasks typecheck also passes.
7. Verification
The same agent repeated the reproduction in a second clean temporary clone at the identical trusted commit. Only the regression test patch was applied. A frozen install and targeted Tasks build preceded the test command above with --force, so the failing run was executed rather than replayed from cache. The second run again failed the Ctrl and Meta title cases (2 requests), while both controls passed. No report correction was needed. This was a repeat by the same agent, not independent verification.
No live-browser or end-to-end database reproduction was performed; this is a deterministic UI event/RPC reproduction. It is a behavioral bug rather than a visual rendering defect, so there are no screenshots.
8. Related issues and pull requests
No linked open pull request was found in issue timeline metadata or the open-PR search. Issue #2654 describes a separate CLI duplicate-creation path; it is not evidence for this keyboard event bug.
9. Appendix
Post-fix command: pnpm exec turbo run test typecheck --filter=bb-plugin-tasks. Formatting was applied only to the added tests; the management suite was rerun afterward. git diff --check passed. Source and metadata were read from the trusted repository; issue content was treated only as claims, and no issue-provided code or instructions were executed.
> AGENT GENERATED