feat(client): wrap long todos in quick add instead of hiding them - #82
Merged
Conversation
JackCuthbert
force-pushed
the
feat/quick-add-wrapping
branch
7 times, most recently
from
August 19, 2026 04:58
adc3e2e to
05454a2
Compare
Typing "next week when 3pm" now sets both the day and the time. It used to set the day and leave "3pm" in the title, describing a due date the todo did not have.
A long line now wraps onto as many lines as it needs, so text can no longer be pushed out of sight by choosing a list or a date. Recognised words are marked with a little more room around them, the list suggestions follow the cursor, and a tall modal moves up the screen rather than pushing its own buttons out of view. Summaries are capped at 500 characters, which keeps typing quick however much text is pasted in.
`mise` had no version set for bun, so the shim failed for anything that spawned it through a fresh shell — including Playwright's dev server, which meant the e2e suite could not start locally.
JackCuthbert
force-pushed
the
feat/quick-add-wrapping
branch
from
August 19, 2026 05:11
05454a2 to
c80722a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Quick add was a single-line
<input>that scrolled sideways, so a long todo pushed its own beginning out of view with no scrollbar to say so and no way back but walking the caret through it. The pills made it acute: they rewrite the text as the single source of truth, so choosing a list could lengthen the line by more than it had to spare — words vanished without a keystroke.The field is now a contenteditable that wraps.
Three commits: the parser fix stands on its own, the wrapping work is the bulk of it, and
mise.tomlis unrelated tooling.Why a contenteditable and not a
<textarea>A textarea wraps perfectly well and would have been a fraction of the change. It also keeps the shadow layer, and the shadow layer is what makes the token marks unpaddable — the spec has carried a section explaining that constraint since August, measured at 34px of drift across three tokens.
I re-measured it against a wrapping textarea before choosing: 52px. The fault was never the
<input>tag, it is the second layer, so it survives every variation of the two-layer approach. Wrapping and a padded mark are one change or neither.So the marks are now real elements in the element you type into:
2px 5pxpadding,--radius-sm, andbox-decoration-break: cloneso a mark broken across a line wrap is drawn complete on both lines.Undo
Writing
innerHTMLempties the browser undo stack. Measured:⌘Zafter a mark re-render does nothing at all, while ordinary typing, typing inside a mark, and direct text-node writes all leave undo intact. So the marks are redrawn only when the set of tokens changes, never per keystroke.The first cut of that predicate compared token offsets, which looks equivalent and is not: typing anywhere before a token shifts every later one, so a keystroke mid-summary counted as a change and redrew. It now compares the marked text, and separately checks what the DOM actually holds — because the browser edits inside these spans, so typing at a mark's trailing edge extends it (
p2becamep2 askdjnfas asdf ask) while the token still readsp2.One honest regression: the single keystroke that completes or breaks a token loses its own undo entry. Narrow, deliberate, and written into the spec.
Placement
The field grows without bound; the modal rises to accommodate it, via a collapsible flex spacer rather than a measured offset. Verified on a 500px viewport: it holds at the launcher height (106px) while there is room, then moves up — 106 → 74 → 16 — and only scrolls once it is genuinely taller than the screen.
A
max-heightwith an inner scrollbar was tried first and removed: it made the popup a scroll container, which clipped the#autocomplete; escaping that with fixed positioning detached the menu entirely.The autocomplete now hangs off the caret, clamped against the viewport using its measured width — not against the modal using its
min-width, which was wrong in both directions at once (nudging left when there was room, overflowing the screen when there wasn't). Verified at 380px, 560px and 1100px.Performance
Every keystroke re-parses the whole line and can redraw every mark, so cost grew with length without bound — ~12ms per keystroke at 4,000 characters on this Mac, several times that on an ordinary machine, which is what holding
⌘Vfelt like. The parser itself was never the problem (3ms at 8,000 characters).Summaries are capped at 500 characters, which makes ~14ms the worst case rather than a floor that keeps rising. A long paste truncates rather than being refused, and deleting still works at the cap.
Bugs found and fixed along the way
Six, all now covered by tests:
autoFocusis a form-control attribute and does nothing on a contenteditable — the modal opened with focus on the button that opened it.onBeforeInputnever fires forinsertParagraph/insertLineBreak; the handler was dead code, now a native listener.document.activeElement, but the closing menu still holds focus for a frame — the caret dropped to offset 0.strict mode violation.shiftKey, so Shift+Enter from the summary submitted the todo, contradicting the footer that documents that key as belonging to notes. The old<input>hid it by clearing as the modal closed.Verification
The full e2e suite now runs locally — 72 passing. It could not before this branch: Playwright's
webServerspawnsbunthrough a fresh shell, andmisehad no version set for it, so the shim failed outright. That is what the third commit fixes.Running it immediately caught two things my browser testing had missed: an ambiguous locator in one of my own new tests, and a genuine regression where the new positioning layer registered as a third full-viewport layer in the overlay stack that the mobile test counts. The layer paints nothing and takes no clicks, so the test now distinguishes a scrim from a positioner by
pointer-events.Also green: 728 unit tests, typecheck across 7 workspaces, lint, knip,
fmt:check.Spec
docs/specs/quick-add.mdis updated rather than appended to: the two paragraphs justifying an unpadded mark are gone, since the reason for them is gone. New sections cover the wrapping rule, the length cap and placement, and what a contenteditable costs — each with the measurement behind it.