#52 — Multi-line Paste → Split Into Multiple Todos (Confirmation Prompt) #52
Labels
No labels
priority/could
priority/must
priority/should
priority/wont
status/blocked
status/claimed
status/done-migrated
type/bug
type/feature
type/infra
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/todo#52
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Story: Multi-line Paste → Split Into Multiple Todos (Confirmation Prompt)
As a list member,
I want to be asked whether a multi-line string I'm adding as a new todo should become one todo or several,
so that I can quickly add a batch of items (e.g. a pasted shopping list) without creating them one at a time.
Acceptance criteria:
and a confirmation dialog is shown before anything is created
the title
i.e. today's behavior
Dialog/DialogContentpattern (ReactUi/src/components/ui/dialog.tsx),consistent with other confirmation modals in the app (see
OwnerActionsMenu.tsx)Known technical constraint — needs an Architect decision before implementation:
Today's "new todo" field (
TodoInput.tsx) is a plain single-line<input type="text">. A native<input>cannot hold newline characters at all — pasting multi-line text into it collapses to a single line before this
feature could ever detect multiple lines. The Architect must decide how the multi-line string is actually
captured, e.g.:
<textarea>(Enter still submits single-line entries; needs a decisionon how multi-line entries are then produced — paste only, or also Shift+Enter for manual multi-line typing)
<input>but intercept theonPasteevent and readclipboardDatadirectly, before the browsernormalizes it, then branch into this flow without changing the field's normal single-line typing behavior
Either approach must preserve todo creation via Enter for the common single-line case without adding friction.
Out of scope for this story:
Blockers: None — independent of other in-flight work.
Priority: Should — flagged directly by the human Product Owner as important for a specific user's workflow
(2026-07-19); queued straight into
ready/to steer it ahead of the open Could-tier backlog, per the humanoverride note in
ai/roles/00_team_overview.md("Autonomous loop" section).security (
52_multiline_todo_split_confirmation_security.md)Security Review —
#52Multi-line Paste → Split ConfirmationAuthor: Security Agent
Date: 2026-07-19
Review
Frontend-only change. No new backend endpoint, no new authorization surface: every created
todo — whether from the "confirm split" path or the "decline" fallback — goes through the
existing
CreateTodoCommand, already gated byAuthorizeTodoListAccessForCurrentUserQuery/AuthorizeTodoListIsNotArchivedQueryexactly asbefore this story. Splitting client-side into N calls instead of 1 doesn't change what data
crosses the wire per call or who's allowed to make it — same shape, same guard, just more of them.
Clipboard access (
event.clipboardData.getData('text')) is a standard, user-initiated pasteevent handler — no clipboard read happens outside of an actual paste the user performed, and
nothing is sent anywhere the browser's own paste gesture didn't already imply.
No new dependency added. No secrets, tokens, or credential handling involved.
Verdict: Approved.
design (
52_multiline_todo_split_confirmation_design.md)Design Note —
#52Multi-line Paste → Split ConfirmationAuthor: Software Architect
Date: 2026-07-19
Resolving the flagged technical constraint
The story flags that
TodoInput.tsx's field is a native<input type="text">, which cannot holdnewline characters at all, and asks for a decision between switching to a
<textarea>orintercepting
onPastedirectly.Decision: keep the
<input>, interceptonPaste.onPaste'sevent.clipboardData.getData('text')gives the raw pasted string, newlines intact,before the browser inserts/normalizes it into the input's value — so no field-type change is
needed to observe multi-line content.
<textarea>would reopen the exact ambiguity the story's own note raises (doesEnter still submit, or does it need Shift+Enter, and does that change apply to every
single-line-typing user going forward) for a feature that, per the story's title and primary use
case, is specifically about paste, not manual multi-line typing.
onPasteinterception addsthe behavior with zero change to how typing/Enter-to-submit already works — satisfies "must
preserve todo creation via Enter for the common single-line case without adding friction" with
no new code path for that case at all.
<input>physically cannot contain\nfrom typing (Enternever inserts a newline,it only fires the existing
onKeyDownsubmit handler), so "single-line input... never triggersthe popup" holds trivially for typed input — only paste can ever produce multi-line text here,
which is exactly the surface
onPasteinterception covers.Scope decision: only intercept when the field is currently empty
The story doesn't specify what happens when a user pastes multi-line text into a field that
already has some typed content (splicing at cursor position). Given the story's own framing
("quickly add a batch of items e.g. a pasted shopping list") describes pasting into a fresh input,
and implementing correct cursor-position splicing adds real complexity for a case the acceptance
criteria never describes, this only triggers when
value === ''at paste time. If the fieldalready has content, the paste falls through to native browser behavior (newlines stripped, same
as today) — unchanged from current behavior, not a regression.
Flow
onPaste: ifvalue === '', readclipboardData.getData('text'), split on\n, filter blank(
trim() !== '') lines. If the resulting count is>= 2:event.preventDefault()(block thedefault paste so nothing lands in the input), store the raw pasted text in new state
(
pendingPaste: string | null), open a confirmation dialog. Otherwise (0 or 1 non-blank lines):let the paste proceed natively — same as today.
MultilinePasteConfirmDialogcomponent (matching the existing pattern ofTransferOwnershipDialog.tsx/AccountDeletionDialog.tsx— small dialog bodies extracted totheir own file, rendered inside
<Dialog><DialogContent>by the parent), titleCreate N todos?(N = non-blank line count), bodyOne todo will be created per line.await callApi('CreateTodoCommand', {todoListId, title: line, description: '', dueDate: null})sequentially (notPromise.all) — the story requires "in order," and while the backend's per-list insert trigger serializes concurrent inserts via a row lock regardless, sequential awaits are what actually guarantee completion order matches line order (aPromise.allbatch has no ordering guarantee on which request's lock-acquisition/insert completes first).await callApi('CreateTodoCommand', {todoListId, title: pendingPaste, description: '', dueDate: null})— the raw, unmodified pasted text, matching "today's behavior" (a single todo with the full original text as its title, not trimmed/altered).pendingPasteand closes the dialog.Out of scope (per story)
Editing individual lines before creating, a configurable delimiter, and applying this to editing
an existing todo's title — none of the above touches
TodoItem.tsx's edit paths.Backend
No backend changes. Each split line becomes one
CreateTodoCommandcall — an existing command,already authorized/validated (list access + not-archived) per-call, same as any other todo
creation.
qa (
52_multiline_todo_split_confirmation_qa.md)QA Notes —
#52Multi-line Paste → Split ConfirmationAuthor: QA Agent
Date: 2026-07-19
What was verified
tsc -bclean,npm run buildclean, full Vitest suite: 39 files / 279 tests passing.multilinePaste.test.ts(5 tests, the puresplitNonBlankLineshelper —newline splitting, trimming, blank-line dropping, single-line, all-blank);
MultilinePasteConfirmDialog.test.tsx(5 tests — title/body text, confirm callsonConfirmwith the right lines and shows a loading state, a second click while pending doesn't
double-call, a rejected
onConfirmre-enables the buttons instead of getting stuck, Cancelis a plain dismiss with no
onConfirmcall);TodoInput.test.tsxgained 7 new tests coveringthe full paste-to-split flow end to end (dialog opens for 2+ non-blank lines, blank lines
don't count, single-line paste doesn't open it, paste into a non-empty field falls through
to native behavior, confirming creates one todo per line in order, cancelling creates one
todo with the original text unchanged, and dismissing via Escape behaves the same as
cancelling).
Postgres/Redis-backed dev stack (and Playwright) can't run locally (see
ai/roles/memory/06_qa_agent_memory.md/07_team_coach_memory.mdfor the established note onthis). Real device/mobile clipboard behavior specifically (the story's "works on mobile/touch"
AC) could not be exercised beyond the jsdom-simulated
pasteevent and the defensiveclipboardData?.guard added during code review — this is a genuine verification gap, not aclaim of full mobile coverage, and should be spot-checked on a real device if one becomes
available before this is considered fully proven end-to-end.
Acceptance criteria status
confirmation dialog before anything is created
Promise.all— see design note)per a code-review fix, this now holds for every dismissal path (Cancel button, Escape,
backdrop click), not just the explicit button
Dialog/DialogContentpattern — code review corrected an initialversion that had the new component owning its own
Dialogwrapper instead of followingthe established parent-owns-
Dialogconvention (TransferOwnershipDialog.tsx,AccountDeletionDialog.tsx)(
clipboardData?.), but not verified on a real mobile device/browser (see note above)