Tasks (markdown outliner, in development) (part 6)
A dedicated Today surface holds the day's true priorities across every project. It is a non-destructive overlay: a task on Today keeps its home project and listid — Today just flags it (dailyplatedate = the local day). Nothing is moved or copied; at day's end a task simply drops off Today and is still sitting in its project.
What it is
This is part 6 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.
Today — your daily plate (a flat, editable list)
A dedicated Today surface holds the day's true priorities across every project. It is a non-destructive overlay: a task on Today keeps its home project and list_id — Today just flags it (daily_plate_date = the local day). Nothing is moved or copied; at day's end a task simply drops off Today and is still sitting in its project.
It behaves like any other project — fully editable. Today renders your plate as one flat, editable list using the SAME rows the "All Projects" view uses: edit a task inline, open its Task details (due / priority / context), launch an agent on it, use the ⋯ menu, check it off — all right here, without bouncing to the project. Editing a task on Today edits the real task in its home project (it's the same row shown in two places — nothing is copied). Done tasks collect in a Done section below. (The older read-only "Do · best first" / "Keep · must land today" bands were dropped in favor of this editable list; deadlines now show as the normal due chip on each row, and snoozed tasks stay hidden until they wake.)
Where it lives. A Today tab (calendar icon) sits directly below "All Projects" at the top of the projects rail — a project-style row rendered above your projects (and above New project / Archived) — AND a Today section is pinned at the top of the "All Projects" combined view, styled as a project section (calendar glyph + bold heading), staying fixed above your project groups as they scroll (a long day's plate scrolls inside its own capped height, so it never swallows the screen). That Today section is always shown — when nothing is on today's plate it reads a quiet "Nothing scheduled for today yet" in place of the list, followed by the same "Plan my day with AI" button described below, so an empty Today in the combined view can kick off planning in one click (it previously had no button). "Today" is always the first thing at the top of the combined view. So the day's plate is the first thing you see in the combined view, while the rail keeps a Today tab right under "All Projects". Today is mutually exclusive with All Projects and any single-project selection.
Plan my day with AI (the prominent button). Today carries a prominent "Plan my day with AI" button — in the header, and the SAME button also appears in the "All Projects" view's Today section when that's empty. When the Today tab itself is empty it shows a grid of action cards instead of a bare message — Plan my day, Catch me up (with a count of caught items), an adaptive Suggest card (a next-best-task pick for power users; a start-or-continue-your-AI-coaching card for newer ones) and Brain dump — so an empty day teaches by doing in one click. Once everything on today's plate is done, a two-card version (Suggest and Brain dump) shows above the finished list. It launches a real Claude session (a full agent, not the cheap prioritizer) steered by a Daily Focus Planner prompt and handed today's plate, your whole open board, and your strategic context — so it starts already knowing what's on your plate and reasons from it (leading with a point of view) instead of asking you to list everything from scratch. It talks through your day with you and — once you approve — captures what to focus on.
- It's a paid session (real tokens per turn) — it launches straight from the labelled button (no confirm pop-up), and it can never be triggered by a background agent (app-only).
- Your open board = every open task across all your projects that isn't already on today's plate, grouped by project and bounded — so the agent starts oriented over what's really open and never has to go fetch it.
- One "Context loaded for Planning AI" divider shows the loaded planning context (today's plate + your open board + your projects) at the top of the chat, collapsed, so you can see exactly what it started with. The app's login token still reaches the agent but is never shown in that divider.
- Strategic context = your AI Coaching "About you" profile (your goals, standing priorities, how you like to work). It's folded into the session privately — used only to steer the AI, addressed to you in the second person, never shown back in the chat (not even in that divider), in the session title, or written into your tasks. It appears only when AI Coaching is enabled; without it, the session still works from your plate + board alone.
How tasks get onto Today.
- Add a task straight to Today (you pick which project it belongs to).
- Create one inline, from a row you're already on — Add subtask (⋯ menu / right-click), Enter for a new task below, Shift+Enter for one above. Anything created this way lands on today's plate as a user pin, so it stays put through a re-arrange. Because Today is a flat list it appears as its own row rather than indented under its parent — it is still a real sub-task back in its home project.
- From the Move-to-project menu — on any existing task (its ⋯ menu, right-click, or the
Vkey) pick Add to Today to put it on the plate, or Remove from Today to take it off. It's the same menu you use to file a task under a project, so moving things on and off Today works "just like any other project"; being an overlay it keeps the task's home project, and a hand-added task is a user pin (plate_source = null) that a re-arrange never disturbs. - The "Plan my day with AI" session can add tasks to today's plate for you (only after you approve).
- Or accept the daily check-in interview: the AI reviews what is due and slipping and proposes a focus; on accept, Today is arranged — the focus tasks + any due-soon deadlines are placed on the plate. Arrange is deterministic and free (no AI, no spend): the interview does the thinking; arranging just materializes its result plus your deadlines. A one-tap Re-arrange redoes it anytime.
- Each placed task carries a provenance mark:
plate_source = 'ai'(arrange placed it) vs a user pin (null— you added it by hand). A re-arrange never touches a task you pinned yourself.
The daily reset.
- Past items drop off Today on their own (still in their projects).
- Roll unfinished → tomorrow (with undo) carries anything you did not finish forward to tomorrow's plate; done rows stay put and provenance is preserved.
For AI readers — the wiring.
- Data: two additive
tasks_v2columns —daily_plate_date(LOCAL YYYY-MM-DD, nullable) andplate_source('ai'|null/user). A partial index (daily_plate_date IS NOT NULL AND is_deleted = 0) keeps the "on today's plate" filter cheap. Migration20260707211837-tasks-v2-daily-plate-columns(PRAGMA-guarded, additive). - Editable surface:
TaskV2TodayViewrenders the plate with the shared editableTaskV2Row+TaskV2RowActions+useMasterViewKeyboardActions+useTaskV2CommandKeys(mirrors the master view; MasterView is NOT refactored). Flat (no bands), snoozed pre-filtered, done tasks in a separate section; every create on this surface threads BOTH the row'slistId(the plate has no active list) anddailyPlateDate: today— the plate renders onlydaily_plate_date === today, so a create that omits the stamp is filtered straight back out and reads as a dead button (contract I6a;plateSourcestays unset so the row is a user pin). That covers the header quick-add and the row-level creates (useTaskV2RowCreation'shandleAddSubtask/handleCreateBelowvia their optionaldailyPlateDateparam, plususeTodayRowActions' ownhandleCreateAbove), andcommitDraftmust carry the field too or the row vanishes on save. Outline numbers omitted; reorder is within-project siblings only (cross-project = the explicit Move-to-project). - Plan-my-day agent: the prominent button →
TASKS_V2_LAUNCH_PLATE_AGENT(tasks-v2:launch-plate-agent, IPC-only — no CLI route,deferred-no-route) →buildPlateAgentPrompt(db, plateDate, { settings })(src/main/services/tasks/plate-agent-prompt.ts) →createSessionWithPromptbound to the Tasks workspace project. Steering = the user's verbatim "Compass" Daily Focus Planner prompt (PLATE_AGENT_PROMPT, kept in its own moduleplate-agent-compass-prompt.tsbecause it embeds markdown fences) + an OmniscioPLATE_AGENT_BRIDGEthat overrides Compass's blank-slate opening (start from the loaded plate/board, open with a point of view, question only calendar/energy/capacity). Hidden/visible split: the steering + the strategic context (buildCoachingBrief, gated byai-coaching, fail-open, ≤2000 chars, second-person "Background about you" heading) + the day's plate (boundedMAX_PLATE_TASKS = 40) + the open board (renderOpenBoard— open tasks NOT on today's plate, grouped by project, boundedMAX_BOARD_TASKS_PER_LIST = 12/MAX_BOARD_TASKS_TOTAL = 120/ per-itemMAX_BOARD_TASK_CHARS = 120) + the project roster + the shared CLI route cheat-sheet all ride HIDDEN; the user sees onlyPLATE_AGENT_VISIBLE_FRAMING.promptalways containsdisplayText(visible-subset); the steering + strategic context NEVER reachdisplayText(steering-hidden). The loaded-context divider: the builder also returnscontextDisplay(plate + board + roster — the actionable context only, never the steering/coaching/footer), passed tocreateSessionWithPromptasseedContext: { label: 'Planning' (PLATE_AGENT_SEED_LABEL), content }so ONE "Context loaded for Planning AI" divider shows it; the Bearer token still folds into the prompt but is never shown in the divider. The IPC handler and its verbatim CLI-action dispatch mirror (tasks-v2-launch-plate-agent-handler.ts) both pass it. Launched directly from the button (no confirm pop-up) + an in-flight guard keyed by the plate date; storelaunchPlateAgent(plateDate?)→setDetailSessionId. - Deadlines (load-bearing): v2's
due_atis a FULL ISO datetime and v2 has NOai_due_at, so the due-soon math normalizes the instant to the user's LOCAL date first —src/shared/tasks-v2-deadline.ts(isDueSoonV2/effectiveDeadlineV2), NOT the generation-agnosticsrc/shared/tasks-deadline.tshelper (which assumes a date-only string and reads NaN off an ISO time portion). - Arrange:
arrangeTodayPlate(db, plateDate)(deterministic, DB-only) — target = the day's check-in focus (still-open) ∪ due-soon deadlines; claims each asplate_source='ai'unless already there or user-pinned; drops an ai-owned pick no longer selected; never touches a user pin. Idempotent. - Channels + routes:
TASKS_V2_ARRANGE(tasks-v2:arrange,POST /tasks-v2/arrange) +TASKS_V2_ROLL_PLATE(tasks-v2:roll-plate,POST /tasks-v2/roll-plate) +TASKS_V2_LAUNCH_PLATE_AGENT(tasks-v2:launch-plate-agent, IPC-only). Accepting the check-in (TASKS_V2_CHECKIN_ACCEPT/POST /tasks-v2/checkin/accept) also arranges the plate, in ONE transaction (atomic — never "accepted but not arranged"). - Contract:
.claude/memory/contracts/tasks-v2-contract.md→today-plate,today-editable,today-plate-agent.
Projects & master context
Naming note. The UI calls these "projects", but everything persisted or wired still uses the original "list" spelling — the
tasks_v2_liststable, thelist_idcolumn, theTASKS_V2_LISTS_*channels, the/tasks-v2-lists/*CLI routes, and thetasks-v2.mdmirror's## {list}headings are byte-stable. The relabel changed only user-visible text (the same convention Omniscio uses for KMS). So the table and channel names below keep saying "list" — that's correct.
Each project is a row in the tasks_v2_lists table (migration src/main/db/migrations/20260602002635-tasks-v2-lists.ts):
| Column | Type | Notes |
|---|---|---|
id |
TEXT PK | randomUUID() |
name |
TEXT | list name (1–120 chars, Zod-enforced) |
color |
TEXT | optional project color, nullable — default none ⇒ NO dot |
icon |
TEXT | optional emoji:<char> | lucide:<name>, nullable (migration 20260620173500-…); shows in place of the color dot, edited from the project ⋯ menu — the dot is the no-icon fallback, painted only when a color is set |
master_context |
TEXT | the shared markdown note, nullable (≤ 50 000 chars) |
default_project_id |
TEXT | reserved for a later chunk, nullable |
sort_order |
INTEGER | 1024-spaced, controls rail + mirror order |
created_at / updated_at |
TEXT | ISO timestamps |
is_deleted |
INTEGER | 0/1 soft-delete |
The migration also adds list_id to tasks_v2, seeds one Inbox list, and backfills every list-less task into it. The only writer of tasks_v2_lists is src/main/db/queries-tasks-v2-lists.ts (listTasksV2Lists, getTaskV2ListById, getDefaultInboxListId, createTaskV2List, updateTaskV2List, reorderTaskV2List, softDeleteTaskV2ListCascade). Deleting a list cascades the soft-delete to its tasks (reversible — links preserved).
List CRUD has its own gated IPC channels (in src/shared/ipc-channels/productivity.ts, handlers in src/main/ipc/tasks-v2-lists-handlers.ts):
| Constant | Channel string | Purpose |
|---|---|---|
TASKS_V2_LISTS_LIST |
tasks-v2-lists:list |
all lists, by sort_order, + per-list open-task counts |
TASKS_V2_LISTS_CREATE |
tasks-v2-lists:create |
append a list; returns it |
TASKS_V2_LISTS_UPDATE |
tasks-v2-lists:update |
patch name / color / icon / master context |
TASKS_V2_LISTS_DELETE |
tasks-v2-lists:delete |
cascade soft-delete the list + its tasks |
TASKS_V2_LISTS_REORDER |
tasks-v2-lists:reorder |
reposition a list (fractional sort_order); IPC-only |
TASKS_V2_LISTS_CHANGED |
tasks-v2-lists:changed |
push — cache-invalidate, strict empty payload |
Every list mutation (create / update / delete / reorder) schedules a mirror write — the ## {list} headings, their order, and the master-context blockquotes all live in tasks-v2.md. (This is the deliberate divergence from task reorder, which does not write the mirror.)
In the renderer, useTasksV2Store tracks lists, activeListId, and taskCountByList (the backend counts) and files new tasks into the active project (or, in the giant view, the picked one). The UI adds a project rail (TaskV2ListRail.tsx) and a pinned master-context card (TaskV2MasterContext.tsx). The rail is a persistent FILTER, not just a switcher: an always-present "All Projects" entry (the giant default view — valid even with a single project) plus one entry per project that filters the view down to it. The rail supports:
- Drag-to-reorder the projects (the whole tab is the drag surface; a plain tap still switches the filter).
- Clean rows — icon + name + a right-aligned count, no per-row buttons. The old per-row quick-add "+" was removed (add a task by switching to the project and using its always-present "Add a task" row, or from a master-view project header's "+" — see Per-project "+" add); with no trailing buttons the count sits flush-right in one column and the name takes the freed width. A project with no icon and no color also drops its blank leading slot (
ProjectIcon collapseWhenEmpty), so its name starts flush-left and reclaims that width too; a project with an emoji/icon or a color still shows it, and the fixed All Projects / New project / Archived rows keep theirs. - Creating a project opens it and drops you into a first task. The fixed "New project" row reveals an inline name input (Enter submits, Escape cancels, blank ignored — wired to the store's
createList). On submit, the rail uses the new project's id to switch the view INTO it (setActiveList) and arm a one-shot signal (autoStartFirstTaskListId); the per-project outliner consumes it once and auto-opens a blank task in edit mode, so you start typing the first task immediately — no "Press Enter" step. Desktop only (an unprompted edit box would pop the mobile soft keyboard, the same reason focus-on-entry is desktop-only — mobile still opens the project, just without the auto task box). The signal self-clears on use and if you navigate away first, so it can't fire later on an unrelated empty list. Locked by the contract'screate-opens-projectinvariant (TaskV2ListRail.test.tsx+TaskV2Outliner.test.tsx). (The giant All Projects view also has its own bottom "New project" box — that one deliberately stays on the giant view instead of switching in; see The giant default view.) You can also spin up a project from a task's Move-to-project menu — its "+ New project…" row creates the project and moves the task(s) straight into it, then opens it. - A per-project options menu (
TaskV2ListMenu.tsx) — rename, recolor (the sharedColorPicker), set an icon (theIconPicker— an emoji or a Lucide glyph; see Per-project color & icon), and delete (a dangerConfirmDialog; Delete is disabled when only one project remains; Ctrl/Cmd+Z undoes a deleted project). The rail row has no ⋯ button — open the menu by right-clicking (desktop), long-pressing (mobile), or Shift+F10 / Menu (keyboard) on the row. The master-view project header keeps its ⋯ button for this same rename/color/icon/delete menu (the rail and the header ⋯ both drive one shareduseListOptionsMenuhook, so a project is customizable wherever its name appears); right-clicking the header now opens the broader project actions menu (which offers a direct Delete project… row, and reaches rename/color/icon via its "Rename, color & icon…" row), not this menu directly. - A per-project open-task count badge that is accurate for every project (backend-computed), not just the active one.
- A resizable width that respects the sidebar. The rail starts matched to your sessions-sidebar width — the slot it stands in for, since Tasks
panelOwnsLayoutsuppresses that sidebar — instead of a fixed column. Drag the handle on its right edge to resize it in place (double-click resets); your width persists to a dedicatedtasksV2RailWidthsetting (a separate key fromsessionsSidebarWidth, so resizing the rail never makes the real sidebar "jump"). It reuses the shared, freeze-hardeneduseResizeSidebar+ResizeHandle— see the contract's invariant 13.
On mobile the rail is not a side column. A side rail beside the content leaves a phone almost no room (the content column collapses to a one-word-per-line sliver), so on useIsMobile() TasksV2View.tsx drops the side rail entirely — the content column goes full-width — and moves the same TaskV2ListRail into a DrawerShell slide-up sheet. A header switcher button (data-testid="tv2-mobile-list-switcher", showing the active list's name + a chevron) opens it; the sheet auto-closes when the active list / master view changes, so a tap-to-switch dismisses it. The header action buttons (Check-in, Send to inbox, Export JSON, Suggest breakdowns) collapse to icon-only on mobile (the text label stays in the a11y tree as sr-only) and the header flex-wraps so nothing spills off-screen. Because the sheet is a transformed/overflow container, TaskV2ListMenu.tsx portals to document.body so its fixed backdrop/box is positioned against the viewport (never clipped inside the sheet) — identical on desktop, where its coords already come from the trigger's getBoundingClientRect(). Desktop is unchanged. Locked by the TasksV2View — mobile layout describe in tests/unit/features/tasks-v2/TasksV2View.test.tsx (see the contract's invariant 13).
Tasks can be moved between projects from the outliner: a focused row shows a "Move to project" trigger (TaskV2MoveToListMenu.tsx) that re-files the row and its whole subtree into the chosen project. (Moving to another project is its own explicit action — it is NOT a reorder.) The markdown mirror groups tasks under a ## {list name} heading (in sort_order) with the master context as a > blockquote beneath it; tasks with no known project fall under Inbox (or an ## (Unfiled) section) so none are lost.
Per-project color & icon
Each project carries an optional color (default: none) and an optional icon — an emoji or a curated Lucide glyph — chosen from the project's ⋯ options menu (the IconPicker sits beside the existing ColorPicker). The icon is stored on tasks_v2_lists.icon as a self-describing string (emoji:<char> | lucide:<name>; null / empty / unknown ⇒ no icon) and rendered by the shared ProjectIcon wherever a project name appears (the rail row and every master-view header). ProjectIcon falls back to a color dot when there is no icon or the string is malformed/unknown — but the dot is painted only when the project has a color. Projects default to no color, so by default a project shows no dot: in the rail a no-icon, no-color project drops its leading slot entirely so its name reclaims the width (ProjectIcon collapseWhenEmpty), while master-view project headers keep the empty same-size slot so their names stay aligned. A colored project shows its dot, and a bad icon value never crashes a render. The dot is the shared ListColorDot (color set ⇒ a rounded-full dot; no color ⇒ nothing); a no-color Lucide icon tints neutral (inherits the row text color), never an accent. The pure parse/format lives in src/shared/tasks-v2-icon.ts; the curated Lucide allow-list in src/renderer/src/features/tasks-v2/project-icon-catalog.ts. Icon is update-only (creating a project never sets one). The options menu that hosts both pickers opens from the master-view project header's ⋯ button and from a rail row via right-click / long-press (one shared useListOptionsMenu hook), clamped on-screen via containToViewport.
The built-in views — Today and Caught — support the same context menu (right-click / Shift+F10) as regular projects: rename, recolor, set an icon. Because these are view-mode toggles (not DB-backed TaskV2List rows), their customizations persist to AppSettings (tasksV2TodayColor / tasksV2TodayIcon / tasksV2TodayName, and the Caught equivalents) instead of the tasks_v2_lists table. The menu reuses the same TaskV2ListMenu component with Delete disabled (built-in views cannot be removed) and Move up/down omitted (their positions are fixed). When no customization is applied, the default Lucide icons (CalendarDays for Today, Sparkles for Caught) and the i18n-derived labels render unchanged.
Per-project "+" add
Each master-view project header carries a "+" (add task) button. Clicking it drops a new empty top-level task into that project and opens it for editing immediately (the top-level twin of "Add subtask"); a collapsed project is expanded first so the new row is visible. (The rail row no longer carries a "+" — its rows are clean icon + name + a flush-right count; to add from the rail, switch to the project and use its always-present "Add a task" row.) With the old top "Add a task" box removed, this per-project "+" and the outliner's "Add a task" row are the primary ways to add a task.
Inline "Add a task" row at the bottom
Beneath the tasks there's an always-present blank entry row you can click and start typing into right away (Notion / Apple-Reminders style) — no "+" button, no separate box. It's there in both editable views: at the bottom of a single-project list, and at the bottom of each project's section in the giant default view (a collapsed project shows none). Type a line and press Enter and it becomes a real task in that project, and your cursor stays right in the add row — cleared and ready for the next one — so capturing many tasks is just type, Enter, type, Enter…. It does not re-open the task you just typed for editing (you already named it in the row) — that's the rapid-entry behavior, locked by the add-row-rapid-entry invariant in the contract. The "+" button still drops into editing because it starts a blank task; the empty-list first-task field does too only on a bare Enter (also blank) — but a first task you actually type in full now settles to the highlighted row border, the same rapid-entry as this add row (no longer re-opened in the green editing state). On a phone, a small tappable "Add" button appears at the end of the row once you've typed something — because a phone's on-screen keyboard can't be relied on to commit (autocorrect / predictive text can swallow its Enter), the button is the deterministic way to save; tap it and the task is created exactly as Enter does — same rapid-entry, the cursor stays in the add row for the next one. Desktop keeps Enter only — no button. Visually, the inline row shows a muted "+" marker sitting in the outline-number column — lined up with the task numbers (1. / 2.) above it, not off under the checkboxes — and hovering the row gives it a subtle button-like highlight (the "+" darkens with it), so it reads as a place to add without being a separate click-to-add button (that's the per-project header "+" above). The checkbox spacer follows your Show checkboxes setting, so the "+" stays aligned whether checkboxes are on or off.
It only turns into a task once you've typed something and pressed Enter — so it never leaves empty junk tasks behind (Tasks deliberately deletes blank tasks, so a permanent real empty task would fight that; this row is just a typing slot until you commit it). It's plain text only — typing "call vendor due friday" creates a task literally named that; the smart date/tag/dial parsing stays on the : command line (see Natural-language capture), because Tasks never silently guesses a date. The row is its own keyboard context (a typed letter like "x" or "d" types into it, never firing a row shortcut), guards against a double-Enter creating two tasks, and won't submit mid-IME-composition. An empty project now shows its heading + this add row in the giant view (empty projects are no longer hidden — see The giant default view), so you can add the first task right there. One honest limit: the synthetic "(Unfiled)" group — where orphan tasks land when they've lost their project — has no add row, since there's no real project to file a new task into. Lives in TaskV2NewTaskRow.tsx; created via the store's optimisticCreate.
Empty list opens ready to type
When a list has no tasks, it doesn't make you press Enter first — it shows a focused "Add your first task…" field with the cursor already in it, so you type the first task immediately (Enter then creates it and settles it to the normal highlighted-row border — one Enter finishes it; it no longer stays in the green "editing" state). This works on both surfaces: a single empty project (the per-project list you switch to) and the consolidated "All Projects" view when every project is empty (there the first task files into Inbox, never "Unfiled"). Same no-blank-task rule as the inline row — typing only creates on Enter — except a bare Enter still makes a blank editable row (the legacy affordance the keyboard's create-then-cancel flow relies on, so nothing about that flow regresses). Desktop auto-focuses on entry and on re-entry (switching back into a kept-warm panel); mobile shows the field but doesn't auto-focus, so it never pops the on-screen keyboard. Lives in TaskV2EmptyStateEntry.tsx (shared by both hosts); locked by the empty-state-entry invariant in the contract.
Related
The overview, the other parts, and everything else worth reading next all sit on Tasks (markdown outliner, in development).
Last verified 2026-09-29