Skip to content

feat(tasks): due dates + multi-trigger task autocomplete (0172) - #60

Merged
crs48 merged 6 commits into
mainfrom
feat/task-due-dates
Jun 13, 2026
Merged

feat(tasks): due dates + multi-trigger task autocomplete (0172)#60
crs48 merged 6 commits into
mainfrom
feat/task-due-dates

Conversation

@crs48

@crs48 crs48 commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Implements exploration 0172: make due dates as easy to add as @-mentioning an assignee, and bring @/#/date autocomplete to the plain task inputs.

What shipped

Phase 0 — canonical dates. One timezone-safe ms ⇄ YYYY-MM-DD module (due-date.ts) replaces three hand-rolled copies (form, projection sync, app projection). The code was already UTC-correct everywhere, so this is consolidation + a cross-timezone regression test (UTC/LA/Tokyo/Kiritimati), not a bugfix. The sync layer keeps a local mirror so it needn't depend on the UI kit; both are pinned by tests.

Phase 1 — natural-language date entry. A small, dependency-free parser (parse-due-date.ts) handles today/tomorrow, weekdays, in N days, ISO, M/D, and month names → resolved to a UTC-midnight day. The due-date popover gets a "type a date…" field with a live preview, plus a "This weekend" preset.

Phase 2 — multi-trigger composer. MentionTextInput is generalized from a single @ trigger into @ (assign), # (tag, create-on-the-fly), and a trailing date phrase → confirm-to-commit due suggestion. Each strips its token and sets the structured field, so a plain title drives the task's relations. Wired into the Tasks quick-add (pending due/tag chips) and the TaskDetailForm title. All prior @-mention behavior and the comments MentionTextArea are untouched.

Phase 3 — keyboard + parity. A d shortcut on a focused row opens a keyboard-first TaskDueDatePalette (NL field + presets), alongside s/p/x.

Deliberate deviations (see doc "Implementation Notes")

  • In-house parser, not chrono-node — the repo ships zero date libs and keeps bundles flat (0171); the stable parseDueDate interface lets chrono drop in later for locales/recurrence.
  • Extend the input in place (C1), not a new packages/typeahead (C2) — smaller blast radius; C2 remains the long-term home.
  • [[ wikilinks scoped to the rich description — task titles render verbatim, so inert [[ ]] text doesn't belong there. The database-cell composer upgrade is also deferred (needs provider threading into the lower-level views package).

Testing

  • 59 unit/DOM tests across due-date, parse-due-date, the multi-trigger composer (task-editing), and TaskDueDatePalette.
  • tsc --noEmit clean for @xnetjs/ui and xnet-web; react sync + editor page-task suites green.
  • fallow audit --coverage … --fail-on-issues passes: 0 introduced complexity / dead-code / duplication findings.
  • Live browser pass (demo mode): quick-add "Ship the build tomorrow" → suggestion → accept strips the phrase → task lands with a Jun 14 due date; #urgent offers create; d opens the palette ("in 2 weeks" → Sat Jun 27); no console errors.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added task due dates with keyboard-driven palette and natural language date parsing
    • Included date presets (Today, Tomorrow, This weekend, Next week)
    • Integrated due date and tag support in task quick-add composer
    • Enhanced text input to support simultaneous @mentions, #tags, and due date phrases
  • Documentation

    • Added design exploration for task due dates and rich inline editing

crs48 and others added 5 commits June 12, 2026 19:15
Consolidate the three timezone-safe ms<->YYYY-MM-DD conversions that had
been reimplemented across TaskDetailForm, the projection sync, and the app
projection into one canonical module in @xnetjs/ui. The code was already
UTC-correct everywhere; this removes the drift risk and locks the all-day
invariant with cross-timezone tests (UTC / LA / Tokyo / Kiritimati).

The sync layer keeps a local mirror (toDateTimestamp) so it need not depend
on the UI kit; both copies are pinned by tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e 1)

Add an in-house, dependency-free NL date parser (today/tomorrow, weekdays,
"in N days", ISO, M/D, month names) and a "type a date…" field in the
due-date popover with a live preview and Enter-to-commit, alongside a new
"This weekend" preset. Trailing-detection helper (for inline title use) is
exported but not yet wired into a surface.

Parser resolves everything to the canonical UTC-midnight day and is fully
unit-tested against a fixed reference date.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generalize MentionTextInput from a single @-trigger into a multi-trigger
composer: @name assigns, #tag categorizes (create-on-the-fly), and a
trailing date phrase ("ship it friday") surfaces a confirm-to-commit due
suggestion — each strips its token and sets the structured field, so a
plain title drives the task's relations.

Wire the new triggers into the Tasks quick-add (pending due/tag chips) and
the TaskDetailForm title. Add a keyboard-first TaskDueDatePalette opened by
`d` on a focused row, reusing the NL parser. All existing @-mention
behavior and the comments MentionTextArea are preserved.

Wiki links stay scoped to the rich description (titles render verbatim);
the database-cell composer upgrade is left as a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add DOM tests for the due-date palette (NL parse, presets, arrow-nav,
clear, escape) so the fallow CRAP gate passes with coverage — the audit
reports 0 introduced complexity/dead-code/duplication findings. Update the
exploration checklist to reflect what shipped and what is deferred
(database-cell composer, page-editor inline NL detection, [[ wikilinks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-60 June 13, 2026 03:12 — with GitHub Actions Inactive

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds due-date support and rich inline editing (tags and due dates) across task surfaces. It introduces canonical UTC-midnight date conversions, natural-language parsing, expands MentionTextInput for multi-trigger support, and integrates a new TaskDueDatePalette into TasksView's focused-task and quick-add workflows.

Changes

Task Due Dates and Rich Inline Editing

Layer / File(s) Summary
Due-date conversion and parsing foundation
packages/ui/src/composed/tasks/due-date.ts, due-date.test.ts, parse-due-date.ts, parse-due-date.test.ts
Canonical UTC-midnight conversions (dueDateMsToIso, isoToDueDateMs, utcDayFromNow, dueDateInputValue) with round-trip validation and timezone safety. Comprehensive natural-language parser supporting keywords (today/tomorrow/next week), weekdays, relative offsets, ISO dates, numeric month/day, and month-name formats. detectTrailingDueDate finds date phrases only at text end using longest-match preference.
MentionTextInput multi-trigger expansion
packages/ui/src/composed/tasks/MentionTextInput.tsx, task-editing.test.tsx (multi-trigger tests)
Generalizes token detection and menu derivation to support @people, #tags (with create-new), and trailing due-date phrase suggestions. Unified ActiveToken model, exported findActiveHashtag, new MentionTagOption type. Menu selection routes to people/tags/due-date based on caret context. Centralized commitMenu invokes appropriate callbacks and strips tokens.
TaskDetailForm title-editor integration
packages/ui/src/composed/tasks/TaskDetailForm.tsx, task-editing.test.tsx (TaskDetailForm tests)
Wires multi-trigger MentionTextInput into TitleRow. Replaces local date utilities with shared due-date module. Adds formatDuePreview for NL preview UI. Refactored DueDateMenu uses parseDueDate and UTC-aware conversion helpers. Passes tag options and callbacks, filtering out already-selected tags.
TaskDueDatePalette focused-task component
apps/web/src/components/TaskDueDatePalette.tsx, TaskDueDatePalette.test.tsx
New keyboard-first overlay for due-date entry. Parses typed queries via parseDueDate and renders preset options (Today, Tomorrow, This weekend, Next week, Clear due date) filtered by label match. Arrow keys navigate, Enter commits (with UTC-midnight timestamp or null), Escape closes without selection. Fullscreen overlay with click-stopping inner panel.
TasksView quick-add and due-date palette
apps/web/src/components/TasksView.tsx
Adds tag and due-date draft state. Registers task.setDueDate command (d key) to open due-date palette. Task creation conditionally persists tags and dueDate from drafts. Quick-add input wired with tag options and handlers; renders removable chips for pending due date and tags. Dedicated TaskDueDatePalette render branch for focused-task due-date selection.
Public API exports and projection alignment
packages/ui/src/composed/tasks/index.ts, packages/ui/src/index.ts, apps/web/src/components/task-node-projection.ts
Barrel exports expanded to include multi-trigger helpers and due-date utilities. task-node-projection updated to use canonical dueDateMsToIso from @xnetjs/ui instead of inline conversion, ensuring consistent timezone-safe serialization.
Documentation and implementation notes
docs/explorations/0172_[_]_TASK_DUE_DATES_AND_RICH_INLINE_EDITING.md, packages/react/src/hooks/useTaskProjectionSync.ts
Exploration document outlines requirements, gaps, and phased implementation plan. Includes "as built" notes, checklists, and example code. Hook documentation clarifies UTC-midnight conversion semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • crs48/xNet#32: Previous work on focused-task mini-palette architecture that this PR extends with TaskDueDatePalette and dueDate state.

Suggested labels

enhancement

Poem

🐰 A rabbit hops through dates with glee,
Parsing "next week" naturally!
From @ mentions to # tags so fine,
Due dates now inline, ✨ they align!
UTC midnight, no timezone fright—
Rich editing shines, oh what a sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: introducing due-date functionality and multi-trigger task autocomplete features as described in exploration 0172.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/task-due-dates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #60.

github-actions Bot added a commit that referenced this pull request Jun 13, 2026

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@crs48
crs48 temporarily deployed to pr-60 June 13, 2026 03:16 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jun 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/TasksView.tsx (1)

338-345: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent async tag-creation bleed-through across quick-add submits.

The async onCreateTag callback can resolve after a submit/reset, causing a stale tag chip to appear on the next draft and dropping the intended tag from the just-created task. Also, both tag add paths should dedupe IDs before appending.

Suggested fix pattern
   const quickAddRef = useRef<HTMLInputElement>(null)
+  const draftEpochRef = useRef(0)

   const handleCreate = async () => {
     const title = draft.trim()
     if (!title) return

+    draftEpochRef.current += 1
     setDraft('')
     setDraftAssignees([])
     setDraftTags([])
     setDraftDue(null)
@@
           tags={tagOptions.filter((tag) => !draftTags.includes(tag.id))}
-          onTag={(tagId) => setDraftTags((current) => [...current, tagId])}
+          onTag={(tagId) =>
+            setDraftTags((current) => (current.includes(tagId) ? current : [...current, tagId]))
+          }
           onCreateTag={(name) => {
-            void getOrCreateTag(name).then((tag) => {
-              if (tag) setDraftTags((current) => [...current, tag.id])
+            const epoch = draftEpochRef.current
+            void getOrCreateTag(name).then((tag) => {
+              if (!tag || epoch !== draftEpochRef.current) return
+              setDraftTags((current) =>
+                current.includes(tag.id) ? current : [...current, tag.id]
+              )
             })
           }}

Also applies to: 452-456

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/TasksView.tsx` around lines 338 - 345, The quick-add
suffers from async tag-creation bleed: when handleCreate resets draft state
immediately, a later-resolving onCreateTag can append a stale tag to the next
draft and drop the tag from the created task. Modify handleCreate and the other
tag-add path to (1) use a local generation token / cancelled flag captured
before calling onCreateTag so any resolved tag added after reset is discarded,
and (2) always dedupe tag IDs when merging into draftTags (and when appending to
the created task) so IDs aren't duplicated; refer to handleCreate, onCreateTag,
draftTags, setDraftTags (and the other tag-add handler around lines 452-456) to
implement the token/flag and ID dedupe before setDraftTags/setTaskTags.
🧹 Nitpick comments (2)
docs/explorations/0172_[_]_TASK_DUE_DATES_AND_RICH_INLINE_EDITING.md (1)

584-646: ⚡ Quick win

Clarify the status of unchecked checklist items (deferred vs pending).

Lines 602, 618, 643–645 are marked unchecked, but context clues suggest they are intentionally deferred:

  • Line 602: Wire NL detection into page editor (appears to be a follow-up)
  • Line 618: Upgrade database cell (marked with "(deferred…)")
  • Lines 643, 645: Manual validation steps (live browser pass + wikilinks in editor)

Consider adding explicit "Deferred" or "Follow-up" badges to clarify that these are not blockers for this PR, rather than leaving them as bare unchecked boxes that might read as incomplete. This would help reviewers and future readers understand the intended scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/explorations/0172_`[_]_TASK_DUE_DATES_AND_RICH_INLINE_EDITING.md around
lines 584 - 646, Update the three unchecked checklist items that are
intentionally out-of-scope by annotating them as deferred/follow-up rather than
leaving bare unchecked boxes: change the "Wire NL detection into the page
editor's taskDueDate flow" item to explicitly read "(Deferred / follow-up)",
change the "Upgrade the database task-cell quick-add to the multi-trigger
composer (deferred — needs provider threading into the lower-level `views`
package)" item to be marked "(Deferred)" (or convert its checkbox to a
"Deferred" badge), and annotate the two manual validation steps ("Live browser
pass: quick-add a task..." and "`[[` wikilinks + inline NL date detection...")
to show they are follow-ups (e.g., "[ ] (Follow-up) Live browser pass..." and "[
] (Follow-up) `[[` wikilinks...") so reviewers know these are not blockers for
this PR.
packages/ui/src/composed/tasks/task-editing.test.tsx (1)

142-188: ⚡ Quick win

Add a regression test for onTag-only mode (no onCreateTag).

Please add a case asserting that unknown #query does not show/create a “Create …” option when creation callback is absent.

Suggested test
+  it('does not offer create when onCreateTag is not provided', () => {
+    const onTag = vi.fn()
+    render(<MultiTriggerHarness onTag={onTag} />)
+    const input = screen.getByTestId('mt') as HTMLInputElement
+
+    fireEvent.change(input, { target: { value: 'Plan `#roadmap`' } })
+    expect(screen.queryByText(/Create/i)).toBeNull()
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/composed/tasks/task-editing.test.tsx` around lines 142 - 188,
Add a regression test to verify that when MultiTriggerHarness is rendered with
only an onTag handler (no onCreateTag), unknown “#query” suggestions do not
offer a "Create ..." option: render(<MultiTriggerHarness onTag={vi.fn()} />),
change the input value to include an unknown hashtag (e.g.,
fireEvent.change(input, { target: { value: 'Plan `#roadmap`' } })), then assert
that screen.queryByText(/Create/) is null (and optionally that no create
callback was called) to ensure the create option is not shown when onCreateTag
is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/TaskDueDatePalette.tsx`:
- Around line 87-97: The ArrowDown branch can set activeIndex to -1 when
options.length is 0; fix by computing a safe maxIndex = Math.max(0,
options.length - 1) and use that when updating via setActiveIndex in the
onKeyDown handler (replace the current Math.min(index + 1, options.length - 1)
logic), keep the ArrowUp clamp as Math.max(index - 1, 0), and also guard the
Enter branch (commit) to only call commit(options[activeIndex]) when
options.length > 0 to avoid indexing into an empty array; reference: onKeyDown,
setActiveIndex, options, activeIndex, commit.

In `@packages/ui/src/composed/tasks/MentionTextInput.tsx`:
- Around line 205-223: The tags menu shows a "Create …" entry even when no
onCreateTag handler is provided, which lets users pick it and lose input; update
the logic that builds listItems (and the equivalent block around the other
occurrence) so the create option is only included when onCreateTag is truthy
(i.e., guard the branch that appends { id: '', name: menu.create } with a check
for onCreateTag or tagsEnabled tied to onCreateTag), ensuring selection of the
create entry only appears and can be acted on when onCreateTag is available;
adjust the useMemo/menu->listItems flow that references menu.kind === 'tags' and
menu.create to conditionally include the create item.

In `@packages/ui/src/composed/tasks/parse-due-date.ts`:
- Around line 225-233: The normalize function currently strips prefixes using a
regex that matches "due" before multi-word prefixes, causing inputs like "due by
friday" to be mis-normalized; update the regex in normalize to list multi-word
prefixes first (e.g., "due by", "due on") before single-word ones and include
all intended variants ("by", "on", "due") so the replace call removes the
longest applicable prefix via something like /^(due by|due on|due|by|on)\s+/
(function: normalize).

---

Outside diff comments:
In `@apps/web/src/components/TasksView.tsx`:
- Around line 338-345: The quick-add suffers from async tag-creation bleed: when
handleCreate resets draft state immediately, a later-resolving onCreateTag can
append a stale tag to the next draft and drop the tag from the created task.
Modify handleCreate and the other tag-add path to (1) use a local generation
token / cancelled flag captured before calling onCreateTag so any resolved tag
added after reset is discarded, and (2) always dedupe tag IDs when merging into
draftTags (and when appending to the created task) so IDs aren't duplicated;
refer to handleCreate, onCreateTag, draftTags, setDraftTags (and the other
tag-add handler around lines 452-456) to implement the token/flag and ID dedupe
before setDraftTags/setTaskTags.

---

Nitpick comments:
In `@docs/explorations/0172_`[_]_TASK_DUE_DATES_AND_RICH_INLINE_EDITING.md:
- Around line 584-646: Update the three unchecked checklist items that are
intentionally out-of-scope by annotating them as deferred/follow-up rather than
leaving bare unchecked boxes: change the "Wire NL detection into the page
editor's taskDueDate flow" item to explicitly read "(Deferred / follow-up)",
change the "Upgrade the database task-cell quick-add to the multi-trigger
composer (deferred — needs provider threading into the lower-level `views`
package)" item to be marked "(Deferred)" (or convert its checkbox to a
"Deferred" badge), and annotate the two manual validation steps ("Live browser
pass: quick-add a task..." and "`[[` wikilinks + inline NL date detection...")
to show they are follow-ups (e.g., "[ ] (Follow-up) Live browser pass..." and "[
] (Follow-up) `[[` wikilinks...") so reviewers know these are not blockers for
this PR.

In `@packages/ui/src/composed/tasks/task-editing.test.tsx`:
- Around line 142-188: Add a regression test to verify that when
MultiTriggerHarness is rendered with only an onTag handler (no onCreateTag),
unknown “#query” suggestions do not offer a "Create ..." option:
render(<MultiTriggerHarness onTag={vi.fn()} />), change the input value to
include an unknown hashtag (e.g., fireEvent.change(input, { target: { value:
'Plan `#roadmap`' } })), then assert that screen.queryByText(/Create/) is null
(and optionally that no create callback was called) to ensure the create option
is not shown when onCreateTag is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63f351b4-9ccd-4f72-9501-c79949deb296

📥 Commits

Reviewing files that changed from the base of the PR and between 0dcbcee and f66acdb.

📒 Files selected for processing (15)
  • apps/web/src/components/TaskDueDatePalette.test.tsx
  • apps/web/src/components/TaskDueDatePalette.tsx
  • apps/web/src/components/TasksView.tsx
  • apps/web/src/components/task-node-projection.ts
  • docs/explorations/0172_[_]_TASK_DUE_DATES_AND_RICH_INLINE_EDITING.md
  • packages/react/src/hooks/useTaskProjectionSync.ts
  • packages/ui/src/composed/tasks/MentionTextInput.tsx
  • packages/ui/src/composed/tasks/TaskDetailForm.tsx
  • packages/ui/src/composed/tasks/due-date.test.ts
  • packages/ui/src/composed/tasks/due-date.ts
  • packages/ui/src/composed/tasks/index.ts
  • packages/ui/src/composed/tasks/parse-due-date.test.ts
  • packages/ui/src/composed/tasks/parse-due-date.ts
  • packages/ui/src/composed/tasks/task-editing.test.tsx
  • packages/ui/src/index.ts

Comment on lines +87 to +97
onKeyDown={(event) => {
if (event.key === 'ArrowDown') {
event.preventDefault()
setActiveIndex((index) => Math.min(index + 1, options.length - 1))
} else if (event.key === 'ArrowUp') {
event.preventDefault()
setActiveIndex((index) => Math.max(index - 1, 0))
} else if (event.key === 'Enter') {
event.preventDefault()
commit(options[activeIndex])
} else if (event.key === 'Escape') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp keyboard index when the options list is empty.

With zero options, ArrowDown currently drives activeIndex to -1. Keep the index at 0 when the list is empty so keyboard state stays valid.

Suggested fix
           onKeyDown={(event) => {
             if (event.key === 'ArrowDown') {
               event.preventDefault()
-              setActiveIndex((index) => Math.min(index + 1, options.length - 1))
+              setActiveIndex((index) =>
+                options.length > 0 ? Math.min(index + 1, options.length - 1) : 0
+              )
             } else if (event.key === 'ArrowUp') {
               event.preventDefault()
-              setActiveIndex((index) => Math.max(index - 1, 0))
+              setActiveIndex((index) => (options.length > 0 ? Math.max(index - 1, 0) : 0))
             } else if (event.key === 'Enter') {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onKeyDown={(event) => {
if (event.key === 'ArrowDown') {
event.preventDefault()
setActiveIndex((index) => Math.min(index + 1, options.length - 1))
} else if (event.key === 'ArrowUp') {
event.preventDefault()
setActiveIndex((index) => Math.max(index - 1, 0))
} else if (event.key === 'Enter') {
event.preventDefault()
commit(options[activeIndex])
} else if (event.key === 'Escape') {
onKeyDown={(event) => {
if (event.key === 'ArrowDown') {
event.preventDefault()
setActiveIndex((index) =>
options.length > 0 ? Math.min(index + 1, options.length - 1) : 0
)
} else if (event.key === 'ArrowUp') {
event.preventDefault()
setActiveIndex((index) => (options.length > 0 ? Math.max(index - 1, 0) : 0))
} else if (event.key === 'Enter') {
event.preventDefault()
commit(options[activeIndex])
} else if (event.key === 'Escape') {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/TaskDueDatePalette.tsx` around lines 87 - 97, The
ArrowDown branch can set activeIndex to -1 when options.length is 0; fix by
computing a safe maxIndex = Math.max(0, options.length - 1) and use that when
updating via setActiveIndex in the onKeyDown handler (replace the current
Math.min(index + 1, options.length - 1) logic), keep the ArrowUp clamp as
Math.max(index - 1, 0), and also guard the Enter branch (commit) to only call
commit(options[activeIndex]) when options.length > 0 to avoid indexing into an
empty array; reference: onKeyDown, setActiveIndex, options, activeIndex, commit.

Comment on lines +205 to +223
const tagsEnabled = Boolean(onTag || onCreateTag)
const dueEnabled = Boolean(onDueDate)

const menu = useMemo(
() =>
dismissed
? null
: buildMenu({ value, caret, people, peopleEnabled, tags, tagsEnabled, dueEnabled }),
[value, caret, people, peopleEnabled, tags, tagsEnabled, dueEnabled, dismissed]
)
const menuOpen = mention !== null && people.length > 0

const syncMention = (nextValue: string, caret: number | null) => {
setMention(caret == null ? null : findActiveMention(nextValue, caret))
const listItems =
menu?.kind === 'people'
? menu.items
: menu?.kind === 'tags'
? menu.create
? [...menu.items, { id: '', name: menu.create }]
: menu.items
: []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate tag-create entries on onCreateTag availability.

The tags menu currently offers a “Create …” choice even when creation is not supported. Selecting it strips the token but invokes no callback, which drops user input with no state update.

Proposed fix
-  const tagsEnabled = Boolean(onTag || onCreateTag)
+  const canCreateTags = Boolean(onCreateTag)
+  const tagsEnabled = Boolean(onTag) || canCreateTags
@@
-  const listItems =
+  const listItems =
     menu?.kind === 'people'
       ? menu.items
       : menu?.kind === 'tags'
-        ? menu.create
+        ? menu.create && canCreateTags
           ? [...menu.items, { id: '', name: menu.create }]
           : menu.items
         : []
@@
-      const items = menu.create ? [...menu.items, { id: '', name: menu.create }] : menu.items
+      const items =
+        menu.create && canCreateTags ? [...menu.items, { id: '', name: menu.create }] : menu.items

Also applies to: 248-254

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/composed/tasks/MentionTextInput.tsx` around lines 205 - 223,
The tags menu shows a "Create …" entry even when no onCreateTag handler is
provided, which lets users pick it and lose input; update the logic that builds
listItems (and the equivalent block around the other occurrence) so the create
option is only included when onCreateTag is truthy (i.e., guard the branch that
appends { id: '', name: menu.create } with a check for onCreateTag or
tagsEnabled tied to onCreateTag), ensuring selection of the create entry only
appears and can be acted on when onCreateTag is available; adjust the
useMemo/menu->listItems flow that references menu.kind === 'tags' and
menu.create to conditionally include the create item.

Comment on lines +225 to +233
/** Normalize whitespace and case; strip a trailing/leading "due"/"by"/"on". */
function normalize(input: string): string {
return input
.trim()
.toLowerCase()
.replace(/\s+/g, ' ')
.replace(/^(due|by|due by|on)\s+/, '')
.trim()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle multi-word prefixes correctly in normalization (due by / due on).

The current prefix stripping matches due before due by, so phrases like due by friday normalize incorrectly and fail full-phrase parsing. In trailing-mode flows, this can fall back to a shorter match and leave residual text in the title.

Proposed fix
 function normalize(input: string): string {
-  return input
+  let phrase = input
     .trim()
     .toLowerCase()
     .replace(/\s+/g, ' ')
-    .replace(/^(due|by|due by|on)\s+/, '')
-    .trim()
+    .trim()
+
+  // Strip stacked leading helpers like "due by", "due on", "by", "on".
+  // Repeat so "due by friday" and "due on 6/20" both normalize correctly.
+  while (true) {
+    const next = phrase.replace(/^(?:due|by|on)\s+/, '').trim()
+    if (next === phrase) break
+    phrase = next
+  }
+
+  return phrase
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Normalize whitespace and case; strip a trailing/leading "due"/"by"/"on". */
function normalize(input: string): string {
return input
.trim()
.toLowerCase()
.replace(/\s+/g, ' ')
.replace(/^(due|by|due by|on)\s+/, '')
.trim()
}
/** Normalize whitespace and case; strip a trailing/leading "due"/"by"/"on". */
function normalize(input: string): string {
let phrase = input
.trim()
.toLowerCase()
.replace(/\s+/g, ' ')
.trim()
// Strip stacked leading helpers like "due by", "due on", "by", "on".
// Repeat so "due by friday" and "due on 6/20" both normalize correctly.
while (true) {
const next = phrase.replace(/^(?:due|by|on)\s+/, '').trim()
if (next === phrase) break
phrase = next
}
return phrase
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/composed/tasks/parse-due-date.ts` around lines 225 - 233, The
normalize function currently strips prefixes using a regex that matches "due"
before multi-word prefixes, causing inputs like "due by friday" to be
mis-normalized; update the regex in normalize to list multi-word prefixes first
(e.g., "due by", "due on") before single-word ones and include all intended
variants ("by", "on", "due") so the replace call removes the longest applicable
prefix via something like /^(due by|due on|due|by|on)\s+/ (function: normalize).

@crs48
crs48 merged commit 0be6332 into main Jun 13, 2026
10 checks passed
github-actions Bot added a commit that referenced this pull request Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant