---
title: Tasks (markdown outliner, in development) (part 5)
---
# Tasks (markdown outliner, in development) (part 5)

## What it is

This is part 5 of the [Tasks (markdown outliner, in development)](tasks-v2.md) 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)](tasks-v2.md) — 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.

### Rich fields & fast capture

Every task can carry a little more than its text — all optional, all surfaced as small chips on the row **only when set** (so rows stay clean):

- An **Importance** dial — None / Low / Medium / High. Stored as a real column (`importance`, integer 0–3) so a later "what should I do right now" view can sort on it quickly. (Urgency was removed from the Tasks surface — its `urgency` column stays but is no longer shown.)
- A **scheduled start date** (`start_at`) — "when I plan to start this," separate from the due deadline.
- An **estimated time** (`estimated_minutes`) — how long the task is expected to take. Type it naturally in the details pane — `30m`, `1h`, `1h 30m`, `2h`, or a bare number for minutes (`90`) — and it shows back tidy (`1h 30m`) both in the field and as a ⏱ chip on the row; junk or empty clears it. Bounded to 1 min…100 h. A **recurring** task carries its estimate to the next occurrence (a stable property, unlike the start date). Parse/format live in one shared helper (`src/shared/tasks-v2-duration.ts`).
- **Tags** — free-form labels, stored in the task's `metadata.tags` JSON (no column).

You edit all of these in the **details pane** — opened from the row's sliders button, `Cmd/Ctrl+;`, or by **clicking a due / importance / tags chip** on the row — or set importance fast with the **`p` key** (a quick None / Low / Medium / High picker). Each field saves automatically as you leave it. **Snooze** in the details pane opens the same universal snooze palette as the `h` key (plus a Clear), not a raw date box. The importance dial uses a shared `PriorityDial` control (`src/renderer/src/features/tasks-v2/PriorityDial.tsx`).

### Emphasis — bold, highlight & star a task

Three lightweight ways to make an individual task stand out, all per-task and all **separate from the Importance dial** above (which is for prioritization — emphasis is purely visual):

- **Star** — flags the row with a ⭐ marker (a quick "favorite", distinct from priority).
- **Bold** — renders the whole row's text in bold.
- **Highlight** — gives the row a highlighter background in one of **five colors** (yellow, green, blue, pink, red). The colors are **theme-aware** — translucent, so they stay legible in both light and dark mode.

Set them two ways: in the task's **details pane** (a "Style" row beside the priority dial) or as quick toggles in a row's **right-click / ⋯ menu** (Star, Bold, and a color-swatch picker). Each saves instantly, shows in **both** the giant "All Projects" view and a single-project view, and **`Ctrl/Cmd+Z` undoes** it like any other edit. (Keyboard shortcuts for emphasis are not wired yet — they're an additive later step via the rebindable command registry.)

Under the hood these are **presentation only** — stored as `bold` / `star` / `highlight` keys on the task's `metadata` (the single source of truth for the keys, the 5-color allow-list, and the theme-aware classes is `src/renderer/src/features/tasks-v2/tasks-v2-emphasis.ts`). They drive no sorting or prioritizer and — unlike `ctx` / `tags` — **do not appear in the `tasks-v2.md` mirror**, so the export stays byte-identical. Both hosts render through the shared `TaskV2Row`, and the controls live in the shared details pane + row menu, so the two views can't diverge. Invariant `emphasis-flags` in the contract (`.claude/memory/contracts/tasks-v2-contract.md`).

### Natural-language capture (the command line)

The dedicated "Add a task" box was **removed** from Tasks — you add tasks via each project's **"+"** (a master-view group header), the per-project outliner's always-present **"Add a task"** row, the **empty-state's focused first-task field** (on an empty list the cursor lands in an "Add your first task…" box — just type and press Enter; see [Empty list opens ready to type](#empty-list-opens-ready-to-type)), Enter on an existing row, or the in-list **command line** (press **`:`**). The command line still turns natural language into a task — type something like:

> `call vendor due friday #work !high`

…and it parses the **due date** ("friday" → the upcoming Friday; dates lean future), the **tag** (`#work`), and the **importance** (`!high`) — also `start <date>` and an **estimated time** (`~30m`, `~1h30m`, `~2h`). (The `~` sigil does double duty by unit: `~` followed by a _time_ — with a `m`/`h` unit — sets the estimate, while a bare `~1|2|3` / `~high` is the shared parser's **urgency**, still dropped on the Tasks command line since importance is the only priority dial there. The two never collide because a unit-bearing `~30m` can't parse as a `~1..3` level.) **A date is never silently guessed** (a wrong due date is a missed deadline). The natural-language dates come from the **chrono** library, loaded on demand so it never weighs down startup. The pure parser is `src/renderer/src/features/tasks-v2/parse-quick-capture.ts` (shared with Tasks).

### Smart capitalization

As you type a task, the **first word and the start of each new sentence are auto-capitalized** — the same "smart" feel as the KMS notes editor. It is intelligent about what to leave alone: URLs, file paths, abbreviations (`Dr.`, `etc.`, `i.e.`), decimals (`3.14`), and `U.S.`-style initials all stay exactly as typed (as do already-capitalized words). It runs **live as you finish each word** in the per-project "Add a task" row (caret-safe — the transform only flips one letter's case in place, so your cursor never jumps to the end) and **on blur** when you edit an existing task's text (the contenteditable's caret is fragile, so the edit path tidies up only when you click away). A task you capture from the **Ctrl+Space Quick Launch** modal's Task tab is capitalized too — its saved text comes out exactly as if you'd typed it into the list. It is setting-gated — **Settings → Lab → "Auto-capitalize task titles"** (`autoCapitalizeTasks`, default on); turn it off and your text is stored exactly as typed. The rules (the URL/abbreviation/decimal/initials matrix) live in one shared module, `src/shared/smart-capitalize-core.ts`, used by Tasks, the Quick Launch task capture, and the KMS editor's `AutoCapitalize` extension, so they can't drift. The plain-string wrapper is `src/shared/smart-capitalize.ts`; invariant `smart-capitalize` in the contract locks that the transform is length-preserving and never flips a row blank.

### Whitespace trimming

When a task is **saved** — whether you create it or edit an existing one — any **leading or trailing whitespace is stripped** (so `"  buy milk  "` is stored as `"buy milk"`). Spaces _between_ words are left exactly as typed; this is a trim, not a re-spacing. Unlike smart capitalization it is **not** setting-gated — it always applies, and it happens at the save (persistence) layer, so every entry point gets it: the outliner, the `:` command line, the Quick Launch capture, and the CLI routes. It never leaves a task blank — a title that is only whitespace is treated as an empty edit and dropped, exactly like the "no blank tasks" rule. The invariant `auto-trim` in the contract locks this for both the create and edit save paths.

### Add tasks at the top or bottom

New tasks added via the always-present **"Add a task"** row land at the **top** of the list by
default — the newest task sits above the existing ones. You can flip this under **Settings →
Tasks → "Add new tasks at"** (`tasksV2QuickAddPosition`, an enum `'top' | 'bottom'`, default
`'top'`): choose **Bottom** to append new tasks to the end instead (the pre-setting behavior). The
control shows only when the Tasks Lab feature is revealed. **Both** "add a new task" affordances
honor the setting — the "Add a task" row AND the **"+" on a project's header** in the all-projects
view — so a new task lands in the same spot however you add it (before this, the project-header "+"
always appended, so a top-preferring task landed at the bottom there). The POSITIONAL edits keep
their own explicit positions and are unaffected: inline **Enter** (new sibling below), **Shift+Enter**
(insert above), and multi-row **paste**. Under the hood **top** reuses the same insert-above
mechanism as Shift+Enter: it resolves to a `beforeId` of the **first row of the target list**
(scoped by list, since the giant view interleaves every project's rows in one order), so no backend
or schema change was needed. The setting lives in `src/shared/types/settings/tasks-v2-settings.ts`;
the invariant `quick-add-position` in the contract locks the default, the target-list scoping, and
that every add-a-new-task affordance opts in through one shared reader.

### Quick capture from anywhere (Ctrl+Space)

The global **Quick Launch** popup's **Task** tab captures a title straight into Tasks — filed in the default **Inbox** list — whenever Tasks is enabled (it falls back to the old v1 task list otherwise). It's the "catch a thought from anywhere, triage later" entry point, decided in the main process via the `QUICK_LAUNCH_CREATE_TASK` router so the storeless Quick Launch window never has to know which task system is active. See [Quick Launch modal](quick-launch-modal.md).

### Sort & filter

The header's **Filter** funnel opens a popover that lets you **sort** the view (manual / importance / due date / start date) and **filter** it (minimum importance, or by tag); a dot on the funnel marks an active filter. Both are a **view overlay only** — they never rewrite your hand-arranged order, and filtering keeps a matched task's parent rows so the tree stays readable. (Urgency sort/filter was removed from the Tasks surface — the `urgency` column stays dormant.) The flatten powering it is `flattenTasksV2` / `useTaskTreeV2` (`src/renderer/src/features/tasks-v2/task-tree-v2.ts`), kept separate from v1's so Tasks (v1) is untouched.

All of this is additive and stays behind the Tasks Labs gate. (Dedicated link / document fields live in [Context attachments](#context-attachments) — URLs, notes/markdown, attached files, and linked ContextDock bundles all attach there.)

### Prioritizing AI — "Right now" + the daily check-in

> **"Right now" is hidden as of 2026-06-22.** By user request, the **Right now** pill +
> panel were removed from the header and their automatic fetch was turned off, so the
> prioritizer's paid ranking is **never called on its own** (the description below documents
> how it works for when it's restored — a clean revert). The **daily check-in** and
> **break it down** are unaffected; each runs on a single click (no confirm pop-up) and
> stays under the shared daily cap.

Tasks can tell you the **single highest-value thing to do next** and run a short **daily check-in**. It reasons over the rich fields above (importance + urgency dials, due/start dates, and each task's **time estimate** when set) plus how much context a task carries. The estimate is context it _may_ weigh (a quick win vs. a long haul) — in the check-in it surfaces a rough total only if you ask about time or the day's focus clearly runs long, never unprompted.

**It's built to be cheap.** A free, instant **local scorer** is the engine — it ranks your tasks with no AI at all (`src/shared/tasks-v2-priority.ts`), and that ranking always shows immediately. A cheap **AI layer** then refines it. The AI's verdict is **cached** and only recomputed when your tasks actually change (a new task, a changed dial/deadline, a completion), so it doesn't spend tokens on every screen refresh. If the AI is turned off, over the daily budget, or unavailable, everything quietly falls back to the free local ranking.

- **"Right now"** (the header **pill**, which shows the pick's title inline → click opens a focused panel) — the one next action, with a plain-English "why" (factor chips like _High importance · Due today_, plus the AI's one-line reason). **Act on it** focuses the task, **Not now** cycles to the next-best, **Re-rank** asks the AI for a fresh take. Empty / all-done / all-snoozed / all-future show friendly copy, never a blank card. It's assistive — it suggests, you decide.
- **Daily check-in** (the header's **⋯ More → Daily check-in**) — an interactive conversation that reviews what's due and slipping and proposes today's focus. Starting it runs one bounded AI call (a single click, no confirm pop-up); then you can reply (each reply is one bounded AI call) and **Accept** the proposed focus. Today's thread is saved, so reopening it doesn't re-spend. It can also run automatically once a day at an hour you set — that scheduled run is **off by default** (opt-in).

**Cost controls:** a cheap model (Haiku 4.5 via OpenRouter), a **shared daily $ cap** across both surfaces (default `$0.50`, in the `tasksV2AiDailyCapUsd` setting), every call cost-tracked, and a setting to turn the AI off (falls back to the free local ranking). Settings live in `src/shared/types/settings/tasks-v2-settings.ts`: `tasksV2RightNowAiEnabled` (default on), `tasksV2DailyCheckinEnabled` (default off), `tasksV2CheckinHour` (default 8), `tasksV2AiDailyCapUsd` (default 0.5).

The whole surface is gated behind the Tasks flag and **paid operations are IPC-only** (never exposed over the CLI), so background agents and tests can't trigger spend. Channels (`src/main/ipc/tasks-v2-ai-handlers.ts`): `TASKS_V2_RIGHT_NOW` / `TASKS_V2_RIGHT_NOW_RERANK` / `TASKS_V2_CHECKIN_GET` / `TASKS_V2_CHECKIN_RUN` / `TASKS_V2_CHECKIN_SEND` / `TASKS_V2_CHECKIN_ACCEPT`, plus the `TASKS_V2_AI_CHANGED` push. The check-in thread persists in the `tasks_v2_checkins` table (migration `src/main/db/migrations/20260608031242-tasks-v2-daily-checkins.ts`).

> This is **Milestone M2** of the Tasks roadmap (the "minimum shippable" set is M1 + M2). It reasons over task data alone for now; an external "about-you" context system is a later, graceful-optional input. Not yet built: CLI read access to the prioritizer, and a Settings UI for the scheduled check-in's hour/cap knobs (the fields + scheduler exist; the on-demand check-in works without it).
>
> **M2 "Right now" status (feature-discovery audit F010):** The "Right now" pill and its prioritizer panel were built and working, then hidden 2026-06-22 by user request. The backend prioritizer infrastructure (local scorer, AI ranking layer, IPC channels, daily cap, caching) remains intact. Re-enabling requires a product decision, not engineering work -- a clean revert of the header hide restores it.

### Plan my day (M6)

> **Status: in development** — additional opt-in toggle **Settings → Lab → Plan my day** (`tasksV2PlanMyDayEnabled`, default OFF), nested under the Tasks lab gate.

### What it does

**Plan my day** is a smarter morning check-in that goes beyond reviewing what's due. Instead of a multi-turn conversation, it makes a single AI call, shows you a proposed plan — a focused task list, any new tasks it suggests adding, and optional "next action" splits for big items — and asks you to review and accept. Once you accept, the tasks are created and your focus list for the day is set.

**How to use it:**

1. Open Tasks → **⋯ More** → **Plan my day** (only visible when the toggle is on).
2. The AI thinks for a moment, then shows its proposal: a message, a suggested focus list, any new tasks, and any breakdowns.
3. Review the proposal — you can adjust the focus list and new tasks before accepting.
4. Hit **Accept** to commit the plan. New tasks land in your Inbox; split next-actions are created as children of the parent task; **Today's plate is arranged automatically** from the accepted focus (same as accepting a daily check-in — you shouldn't need to click "Re-arrange today").

The AI uses your AI Coaching "About you" profile (when AI Coaching is enabled) to personalize the plan — it reads how many items you like to focus on, your standing priorities, and any notes you've added about yourself. That profile grows over time: each time you accept a plan, the app learns how many tasks you're comfortable with today.

**Cost:** one AI call per morning, drawn from the same shared daily budget as the daily check-in (`$0.50` default, in Settings → AI → Tasks daily cap). No charge if the plan cannot be generated — a free local fallback is shown instead.

### How it works (for AI readers)

**Toggle gate.** Plan-my-day behavior is only active when `tasksV2PlanMyDayEnabled` is on. When off, the IPC handler returns an error immediately — no AI is called and no spend occurs. The classic M2 check-in (`CHECKIN_RUN` / `CHECKIN_SEND`) is unaffected.

**Open → single AI call → proposal.** `openPlanMyDay` builds a `CheckinContext` (overdue, due-today, top-picks, total-open count), injects two briefs into the **hidden system half** of the prompt:

- **Planning brief** — derived from the user's `TaskV2PlanningProfile` (preferred item count, granularity, standing priorities, freeform notes); `null` when the profile is absent or all fields are blank.
- **Coaching brief** — reads the AI Coaching core profile, gated by `isUnreleasedFeatureVisible('ai-coaching', settings)`, fail-open (any DB error → `null`), truncated at `COACHING_BRIEF_MAX_CHARS = 2000`. **The coaching brief is injected ONLY into the system half and never into the user turn, displayText, or the conversation mirror.**

The AI returns one JSON object: `{ message, focus, newTasks, splits, profileDelta }`. `parsePlanProposal` (pure, no IO) validates it: routes through `extractJson` (handles fenced-JSON blocks), filters out focus/split ids not in the eligible set, caps arrays at `MAX_FOCUS = 8` / `MAX_SPLITS = 8` / `MAX_NEW_TASKS = 10`, and returns `null` on a missing/empty `message` or unparseable input. A `null` result triggers `buildLocalPlanOpening` (free, local fallback using the priority scorer's top picks). A `newTask` may also carry an optional **`estimatedMinutes`** — coerced by the shared `clampEstimatedMinutes` (whole minutes, 1..6000; a non-number / below-min / missing value is dropped) and written onto the created task at commit. The system prompt tells the model to set it (and to note the focus's rough total) **only when the user asks about time**, never unprompted — the estimate field is exposed to the AI, not volunteered by it.

**Atomic commit.** `commitPlanMyDay` runs one SQLite transaction: (1) create new tasks in the default Inbox list; (2) create split next-actions as children of their parents, inheriting the parent's list; (3) set `proposed_focus_json` to the REAL ids of the final focus set (existing + newly-created, stale ids re-validated and dropped); (4) upsert the planning profile; (5) stamp `accepted_at`; (6) clear `proposed_plan_json`; (7) **`arrangeTodayPlate`** for the check-in's date so the accepted focus lands on Today (parity with interview-accept). A mid-commit failure rolls back entirely — no orphan tasks. A second commit on the same checkin id is a no-op (`accepted_at` guard).

**Profile learning.** The `tasks_v2_planning_profile` table (single row, id `'default'`) stores `preferredItemCount`, `preferredGranularity`, `standingPriorities`, `aboutFreeform`. On commit, `preferredItemCount` is set to `finalFocus.length` — but **only when > 0** (an empty/abandoned plan never overwrites the existing count with `0`). An explicit `profileDelta.preferredItemCount` from the AI response takes priority over the derived count. The profile is readable and editable via the `profile-get` / `profile-update` channels.

**Shared check-in row.** Plan-my-day and the classic M2 check-in share the same `tasks_v2_checkins` row per date, but they key on independent signals so both can run on the same day: the classic check-in short-circuits on its message thread, while plan-my-day short-circuits on the stored proposal (`proposed_plan_json`) and does not write to the message thread. Opening the classic check-in first no longer blocks plan-my-day from generating a proposal, and vice versa.

**IPC channels** (all `requireTasksV2Enabled()`-gated, then plan-my-day toggle-gated):

| Channel                   | String                    | Notes                                |
| ------------------------- | ------------------------- | ------------------------------------ |
| `TASKS_V2_PLAN_OPEN`      | `tasks-v2:plan-open`      | Open / generate today's plan         |
| `TASKS_V2_PLAN_COMMIT`    | `tasks-v2:plan-commit`    | Atomically commit the reviewed plan  |
| `TASKS_V2_PROFILE_GET`    | `tasks-v2:profile-get`    | Read the planning profile            |
| `TASKS_V2_PROFILE_UPDATE` | `tasks-v2:profile-update` | Upsert or clear the planning profile |

All four channels are classified `deferred-no-route` in the CLI-parity manifest (consistent with the sibling check-in AI channels); promoting them to real CLI routes is a noted follow-up.

**About-you view.** The planning profile (`preferredItemCount`, `preferredGranularity`, `standingPriorities`, `aboutFreeform`) is surfaced to the user as an editable "About you" panel in the Tasks UI. It is the user's own data, always editable and clearable.

**Contract:** all invariants + locking tests: [`.claude/memory/contracts/tasks-v2-contract.md`](/.claude/memory/contracts/tasks-v2-contract.md) → "Plan My Day (Milestone M6)".

## Related

The overview, the other parts, and everything else worth reading next all sit on [Tasks (markdown outliner, in development)](tasks-v2.md).
