Tasks (markdown outliner, in development) (part 4)
Tasks doesn't just show deadlines and snoozes on the row — it tells you when they come up. A quiet background check (running whenever Tasks is on) raises one Inbox alert the moment: a task's deadline arrives (its due date/time passes), or a snoozed task comes back (its snooze runs out).
What it is
This is part 4 of the Tasks (markdown outliner, in development) page. It carries the next stretch of the material on that page, moved here because a single page is capped at 40,000 characters.
Where to find it
Reach this part through Tasks (markdown outliner, in development) — it lists every part and explains where the feature lives in the product. Everything below is reached from the same place.
How it behaves
Everything below is the behaviour, detail and edge cases that belong to this stretch of the Tasks (markdown outliner, in development) page.
Deadline & snooze reminders
Tasks doesn't just show deadlines and snoozes on the row — it tells you when they come up. A quiet background check (running whenever Tasks is on) raises one Inbox alert the moment:
- a task's deadline arrives (its due date/time passes), or
- a snoozed task comes back (its snooze runs out).
Each alert shows the item and where it lives in your project — the project name on its own line at the top, then any parent tasks stacked beneath it as an indented outline, so you see the full path down to the item — plus two buttons: Open in Tasks (opens that task in its own list, highlighted and scrolled to the middle — and on a phone it also switches you to the Tasks screen) and Archive task (files the task away and clears the alert). Snoozing the reminder re-snoozes the whole task until the time you pick (and clears the reminder), so it genuinely defers the task instead of only hiding the notice — which is why it no longer just comes right back. It fires once per event (re-scheduling a deadline re-arms it for the new date), it's free (no AI), and a big backlog of already-due / already-back items trickles in a few a minute rather than flooding you. The reminders land in your Inbox's Alerts area with the project name shown inside. Mechanism
- invariants:
inbox-reminderin.claude/memory/contracts/tasks-v2-contract.md.
Waiting on someone — the follow-up clock
Some tasks aren't blocked on you — they're waiting on someone else (a reply, a delivery, a sign-off). Tasks lets you mark that, then quietly reminds you to chase it so it doesn't slip:
- The ⏳ "waiting" toggle sits with the row's other action icons on the right and appears when you hover the row, so a resting row stays clean — just its number + text (it's shown plainly on the focused row, and on the focused row on a phone). Click it to open a clean centered popup (the same style as the Snooze popup): type who or what you're waiting on — and the people and things you've waited on before drop down as a clickable list from the name box (it appears when you click into or type in the box, and floats over the popup instead of pushing it bigger) to fill it in — optionally type when to be reminded to chase it in plain language ("in 2 days", "next friday", or a quick preset — the same smart date box the due-date field uses, with a live preview of the exact date), and confirm — or just press Enter to mark it waiting without a name. Marking it starts a follow-up timer at the reminder time you picked, or 2 days out by default if you left it blank, saved on the task so it holds even if you never reopen it. The name's first word is capitalized automatically — like your task titles, "sarah" is saved as "Sarah". Once a task is waiting, the same ⏳ popup shows the current reminder and lets you edit who, set a new reminder time (which re-schedules the chase), or Clear waiting — and a time it can't read is refused with a small hint rather than saved wrong.
- A waiting task tucks into a "Waiting" section — exactly like Snoozed. The moment you mark it,
the task leaves the main list and collects under a single collapsible ⏳ Waiting (N) row
pinned at the bottom (below the Snoozed section), so your list stays focused on what you can act
on now. Open the section to see your waiting tasks grouped under the person or thing you're
waiting on (see the next point), each rendered just like a regular numbered task row with a
calm waiting chip (or a gentle amber follow up once the follow-up date passes — a nudge,
not an alarm; amber "Needs You", never red) and a "+N sub-tasks" count. To drop one back into your
list, open the row's ⋯ menu and choose Clear waiting — the old always-on button is gone, so
the rows stay calm and read like any other task. This works in your per-project lists and the
All Projects view (one section per project); the Today plate keeps waiting tasks inline
(they show the full
waiting · <who>/follow up · <who>chip on the row). A task that's both snoozed and waiting shows under Snoozed only. - The section always groups by who — each person/thing a numbered top-level row. Every person or thing you're waiting on becomes its own numbered top-level row (1. Dana, 2. Alex) with a count and a collapse chevron, and the tasks you're waiting on nest beneath it, numbered one level down (A., B.) — so you see who you're chasing at a glance. The person you most need to chase floats to the top (any overdue follow-up leads). This happens even for a single person — one numbered group, not a flat list; anything with no name set collects in a muted "No one specified" group at the bottom. Because the person is named at the top, the tasks beneath drop the redundant "· <who>" and just read waiting / follow up. Applies in your per-project lists and the All Projects view alike.
- You can also name who you're waiting on (and change the follow-up date) in the task's details pane — the same fields the ⏳ popup writes; leave the date empty for a calm reminder with no nudge.
- If that date passes while you're not looking at Tasks, a one-time inbox reminder appears,
titled
<task> — follow up with <who>, with Open in Tasks and Archive. It fires once per horizon — moving the follow-up date re-arms it — and, because Tasks works on your phone, the nudge reaches mobile too. - Your daily check-in also learns who you're waiting on and who's overdue to chase, so it can gently remind you to follow up.
This mirrors the Tasks follow-up clock: a ~1-minute background scan (on whenever Tasks is on)
over waiting tasks whose follow_up_at horizon has passed — gated + fire-once via a
follow_up_notified_at stamp — persisting a chase alert (Open-in-Tasks / Archive buttons, no transient toast)
surfaced through the standard ALERT_CREATED push. Service:
src/main/services/tasks/tasks-v2-followup-service.ts.
Always-on outline numbers
Every visible row carries an outline number — the same size and weight as your task text, with a
trailing period — just to the right of its checkbox: top level 1. / 2. / 3., children
A. / B., grandchildren roman i. / ii., great-grandchildren a. / b., then the
marker style keeps cycling by depth (1. → A. → i. → a., then repeats).
Each row shows only its own marker (a child reads A, not 2A) — the indentation shows
which parent it belongs to, like a classic outline: a sub-item is indented so its own marker
lines up under the start of its parent's text (each nesting level steps in by the parent's
marker gutter, not a token amount). It is always on and follows whatever
sort/filter you're viewing (and a drilled-in view renumbers from 1), so the number always
matches what's on screen. You see the numbers in both the default "All Projects" view
(where each project is numbered on its own, starting from 1) and a single-project view.
The labels are computed purely from the visible rows (outline-numbering.ts) and rendered as
a number the same size and weight as your task text, with a trailing period (1. / A. / i.),
in a gutter kept out of the task's own markdown — so a row whose text is itself a markdown
list never clashes with its number. The checkbox sits to the left of the number, and the
number sits on the row's first line beside the text (it never floats above the text like a
superscript); on a task that wraps onto several lines it stays beside that first line.
Visual skin — Liquid Glass
Tasks renders in a Liquid Glass skin: frosted panels, soft refraction edges, and a restrained accent glow. It is theme-aware and accent-derived — the glass fills track the surface palette and the tint/glow track the user's accent (--accent-rgb), so it follows the active light/dark theme and chosen accent rather than a fixed dark/teal look. backdrop-filter blur runs on the big panels only (the list rail, the master-context card, the menus); the row "bubble" (fill + border + sheen, no per-row blur) rides only on the active row — the one you're focused on or have multi-selected — so the rest of the list renders flat and long lists stay smooth, and the blur drops on mobile and under reduced-motion.
The skin is isolated to Tasks — its own --tv2-glass-* CSS vars + .tv2-glass-* rules (src/renderer/src/styles/globals.css) and TV2_* class constants (src/renderer/src/lib/styles.ts). The shipped Tasks (v1) outliner, which shares the older TASK_* constants, is left untouched (the safe-change rule: never restyle v2 by editing a shared constant). The only desktop right pane beside the list is the M4 Break-it-down interview (interviewTaskId) — the list rail, header, and outliner all stay visible while it's open. The per-task agent chat has no Tasks-tab pane: it lives in the Sessions tab (see Per-task agent chat); on a narrow screen an explicitly-opened chat still takes over the full screen. Context attachments still live in the modal context pane. Full styling invariants are in the contract (.claude/memory/contracts/tasks-v2-contract.md → "Liquid Glass styling").
For agents
Data model — the tasks_v2 table
Created by migration src/main/db/migrations/20260601091339-tasks-v2-table.ts. Columns:
| Column | Type | Notes |
|---|---|---|
id |
TEXT PK | randomUUID() |
parent_id |
TEXT, self-FK | NULL = top-level row |
sort_order |
INTEGER | fractional, 1024-spaced (SORT_GAP); midpoint inserts; auto-renormalized when a gap collapses below RENORMALIZE_THRESHOLD = 2 |
text |
TEXT | the markdown source (≤ 10 000 chars, Zod-enforced) |
done |
INTEGER | 0/1 |
due_at |
TEXT | ISO timestamp, nullable — ALWAYS a …Z UTC instant. Writes accept a zoneless local wall clock / an offset / Z and normalize (see Setting a time on a task); the deadline sweep compares this column LEXICALLY, so a non-Z value would break it |
snoozed_until |
TEXT | ISO timestamp, nullable — same …Z normalization as due_at |
snooze_history_json |
TEXT | JSON array of { at, until } — the durable per-task snooze log behind the "came back" alarm-clock icon + ×N count + hover; NULL = never snoozed; server-maintained (not settable via create/update); capped at 25 (migration 20260704161842-…) |
due_notified_at |
TEXT | reminder bookkeeping, nullable |
waiting_since |
TEXT | ISO timestamp — set when the task is marked "waiting on someone"; NULL = not waiting (the derived waiting state) (migration 20260713224000-…) |
waiting_on_who |
TEXT | free text — who/what is blocking (e.g. "Sarah"), nullable |
follow_up_at |
TEXT | ISO "nudge horizon" — when it passes the row flips to "chase" AND the follow-up clock fires; nullable (empty = calm reminder only) |
follow_up_notified_at |
TEXT | fire-once stamp for the follow-up clock; cleared on a follow_up_at change (the updateTaskV2 re-arm) to nudge again; nullable |
metadata_json |
TEXT | arbitrary JSON object (≤ 50 000 chars serialized); ctx + tags flow to the mirror, while the bold / star / highlight emphasis keys stay DB-only |
group_id |
TEXT | always NULL in v2 — there is no task-group feature in this table |
importance / urgency |
INTEGER | 0–3 priority dials (None/Low/Medium/High), nullable — the M1 fields |
start_at |
TEXT | ISO scheduled-start date, nullable — the M1 field (migration 20260607220204-…); same …Z normalization as due_at |
estimated_minutes |
INTEGER | optional estimated duration in whole minutes (1–6000 = 1 min…100 h), nullable — "how long will this take" (migration 20260712155750-…) |
list_id |
TEXT, FK → tasks_v2_lists(id) |
the list this task belongs to; backfilled to Inbox for pre-existing rows |
created_at / updated_at |
TEXT | ISO timestamps, bound in JS |
is_deleted |
INTEGER | 0/1 soft-delete |
Four partial indexes (all WHERE is_deleted = 0): idx_tasks_v2_parent_order on (parent_id, sort_order), idx_tasks_v2_due on (due_at) where undone, idx_tasks_v2_snooze on (snoozed_until) where undone, and idx_tasks_v2_due_followups on (follow_up_at) covering only waiting + due + un-fired rows (the follow-up clock's per-tick scan).
The core module that reads or writes this table is src/main/db/queries-tasks-v2.ts — listTasksV2, getTaskV2ById, listAllTasksV2, createTaskV2, updateTaskV2, completeTaskV2Cascade, softDeleteTaskV2Cascade, reorderTaskV2, indentTaskV2LeaveChildren (the v2 Tab — indent a row alone, re-homing its direct children to the new parent in one atomic transaction), moveTaskV2ToListCascade (re-file a row + its subtree into another list), countOpenTasksV2ByList (per-list open-task counts for the rail badges), plus listDueFollowUpsV2 + markFollowUpNotifiedV2 (the waiting-on follow-up clock's per-tick scan + its fire-once stamp). The notification-clock reads (listDueTasksV2 / listSnoozeExpiredTasksV2 / listDueFollowUpsV2 / listDueTaskEventsV2 + their mark/clear stamps + getTaskV2AncestorChain) and the rowToTask row-mapper live in the sibling modules queries-tasks-v2-clocks.ts and queries-tasks-v2-row.ts, both re-exported from queries-tasks-v2.ts so every import path is unchanged. Both the IPC handlers and the CLI routes call into it, sharing one Zod validation surface.
Related
The overview, the other parts, and everything else worth reading next all sit on Tasks (markdown outliner, in development).
Last verified 2026-09-23