Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
55 KiB
ADR 0033: Halkyon Todo, what it does and how it looks
- Status: decided 1 Nov 2026, by the developer (D1–D10 below, the agent's recommendations taken as a whole). Built the same day: steps 1 to 3, Today and Upcoming as lists, recurrence, reminders and the week strip (step 4), the task pane on wide windows and search (D2).
- Numbering: 0025–0029 are held (ADR 0030's note), 0032 is Jellyfin's; this is the next free number.
- Builds on: ADR 0030, D7, D12, D14, D15 and D60 (Train's sealed rows, its shell, its frame and settings, which Todo takes); ADR 0017 (the account's keys); ADR 0031 (Todo's icon); Train's D61 (What's new).
Context
On 1 Nov 2026 the developer asked for work on Todo to start from 68 screenshots of
Todoist's Android app (31 Oct 2026, ~/Downloads/todoist-screenshots-mobile/), "following
the same foundational reasoning of Halkyon Learn and Halkyon Train": when the bar and when
the rail, a wash of strawberry, settings grouped as theirs. What the screenshots show:
- Five tabs (Inbox, Today, Search, Upcoming, Browse), a floating add button above the bar, the bar's items and the button's side set by the person; Browse holds the projects, Reporting, templates and the cog to Settings.
- A task has a title, a description, a date (optional time and duration, "06:30–07:00") and, apart from it, a deadline (date only, its own icon, neutral colour; a task may have a deadline and no date), a priority (P1–P4), labels, reminders (relative to the time, automatic 30 minutes before a timed task; by location in the paid plans), a recurrence (completing it moves it on; its sub-tasks reset), sub-tasks (counted "0/5" on its row), comments with attachments. Rows colour the date by meaning (overdue red, tomorrow orange, this week purple).
- Projects have sections (name, description, count), seen as a list, as a board (a column a section, paged) or as a calendar (a week strip opening into the month, an agenda below). Today puts Overdue first, with Reschedule for all of them; Upcoming is an agenda from today on, or a 3-day grid of hours where a long press picks 30 minutes. Every view has its own Display sheet: layout, completed tasks, grouping, sorting, filters by date, deadline, priority and label.
- The task sheet shows a row for each field set and a chip for each one not, the sub-tasks, and a comment field at its foot; quick add is a card above the keyboard with chips (date, deadline, priority, reminders, project), dates recognised in what is typed.
- Productivity: a daily and weekly goal, streaks, karma, days off and a vacation mode; Reporting, the completed tasks by day.
- Settings: Account, General (home view, smart dates, reset sub-tasks, the week's first day, how "next week" and "weekend" are read, swipe actions), Theme, Navigation bar, Quick Add, Productivity, Reminders, Notifications (a morning overview and an evening review), About, What's new.
Decisions (1 Nov 2026, the developer's)
- D1 Todo's frame is Train's (ADR 0030, D60): go_router with a
StatefulShellRoute, every tab keeping its own stack; halkyon_ui'sNavigationFrame, a bar floating above the foot below medium windows and a rail with Todo's icon on top from medium on, a tick on every choice; Todo's account sheet goes. Over a frame of Todo's own, which would make the three apps behave differently on the same window. - D2 Four tabs: Today, Upcoming, Inbox, Browse (recommended, over Todoist's five and over
a bar the person arranges). Today opens. Browse holds the projects, the labels and,
later, saved filters, with the cog to Settings atop it as Train's Home and Profile carry
theirs. Search is an icon in every tab's app bar, not a tab, as neither Learn nor
Train gives search a tab. Adding a task is a floating button above the bar
(Train's
TabScreenplaces it clear of the bar), on the rail's screen from medium windows on. From expanded windows on, a task opens in a pane beside its list, as Train's Maps opens a route's map beside the list (Material's list–detail layout), and in a bottom sheet below. Arranging the bar may come later; Search may become a tab if it proves to be used as one. As built (1 Nov): the pane is the frame's, beside whichever tab is shown (TaskPanearound the shell's tabs), 420 wide, the list keeping the rest; a task tapped there opens in it, another replaces it, what was typed into the last kept. Narrowed below expanded, the pane hides and tasks open in sheets again. Search is a screen pushed onto the tab's own stack (back returns to the tab, a task found opens in the pane where there is one): as typed, the projects whose name holds it and the tasks every word is found in, in their title, description or labels' names, whatever the case and the accents, open ones first by their date, done ones last; on the device alone, which the screen says, as the instance reads no task (D5). - D3 Every screen washes down from the top in strawberry (the developer's):
TopGradientunder the app'sScreenColor,PaletteColor.strawberry, as Learn's in cobalt and Train's in carrot; buttons and links stay cobalt. - D4 Settings as Learn's and Train's (the developer's): a screen over the tabs,
grouped in
SectionCards. Sync's card first (as Learn's), the operator's card for whoever operates the instance, then Todo's own groups (Tasks: the week's first day from the language unless set, resetting sub-tasks; Reminders: the automatic reminder before a timed task, snoozing), the account near the foot (halkyon_app'sAccountTiles,SimpleLoginSection,AccountSecuritySection), What's new and About Halkyon Todo last, the version small below them. Groups come as their settings are built; none is shown empty. Tasks and Reminders (as built, 1 Nov, the developer's "as much as possible without my input"), kept in the person's sealed settings row (todo_settings, D17), so on every device: Start the week on (the language's unless set; the week strip, the date picker's next week, the filters' this week and the repeat picker follow it,firstWeekdayOf); Reset sub-tasks (on at first, as Todoist's; off, a recurring task done leaves its sub-tasks done); Automatic reminder (none at first, so that nothing reminds a person who didn't ask; at the time, or 5 to 60 minutes before): a task gaining a time, added so or given one, without reminders of its own gets it, written into the task where the person sees and may remove it, and a task that already had a time keeps what the person left it (TaskRepository, every way of giving a time at once); When snoozed (15 minutes at first; 5 to 60): each reminder's notification has a Snooze action, the reminder coming back that much later under an ID of its own, scheduled from what the notification carries (snooze.dart), so on Android with the app closed too (snoozeInBackground, the plugin'sActionBroadcastReceiverdeclared), and on Linux by the running app, as its reminders; the plan of the person's reminders leaves the snoozed ones alone. On iOS, macOS and Windows since 1 Nov 2026 (the developer asked for it): Apple's systems take a notification's actions from categories registered as the app starts, so there is one for each length a snooze can be (snoozeCategory,snoozeCategories), its action in the language the app starts in (the device's, asresolveAppLocalepicks it; a language changed meanwhile shows from the next start) and not to the foreground, so that on iOS the plugin's isolate snoozes with the app closed, as on Android (AppDelegateregisters the plugins for it). Windows hands back only the button's arguments, so the button carries the notification's ID and payload (windowsSnoozeArguments, read bysnoozeOfas the others' action). macOS and Windows launch the app for an action while it is closed: the gateway snoozes what launched it as it starts. Built and tested on Linux alone: none of the three was tried on a device yet. - D5 Todo's rows are sealed, always, as Train's (ADR 0030, D7, D15), synced with
serverpod_offline_sync: every payload ADR 0017's sealed content under the person's data key (content_key, halkyon_app'sPayloadEnvelope), nothing readable in a column, not a title nor a date. Recommended over readable rows (ADR 0002's default), which would let the server send reminders by email and a morning overview, and would make shared projects simpler: what a person plans to do is theirs, the server needs to read none of it to sync it, and Todo's rows can't be unsealed later without the person's devices. So reminders are scheduled on the device, as local notifications on each device that holds the account's keys; there is no email reminder and no overview the server writes. A project shared later is sealed under a key its members share, which is that step's to decide. As built (1 Nov, the developer's choice offlutter_local_notificationsoverawesome_notificationsand platform code of Halkyon's own): a task's reminders are minutes before the time on its date, in its sealed payload, so every device of the person's reminds; a task with a day alone has none until the automatic reminder (D4) is a setting. Each device plans its reminders whenever the tasks change, comes to the front, or every six hours: the next 48 within 30 days (iOS keeps 64 pending), a recurring task's next time alone, each under an ID hashed from its task and offset so that planning again replaces it. They are scheduled with the system on Android, iOS, macOS and Windows, so they come while the app is closed, at their instant: a device moved to another zone has them moved once the app is opened. Linux schedules none, so there the running app shows them. Android brings them at the minute only where the person allows exact alarms (SCHEDULE_EXACT_ALARM, off by default since Android 14), else within a few minutes; the app doesn't send the person to that setting. The permission to notify is asked when a task first gets a reminder (both amended by D31: exact alarms throughUSE_EXACT_ALARM, the permission asked wherever a reminder is planned); a notification's text, the task's title, is hidden on a locked Android screen. Signing out, or switching person, cancels the person's reminders on the device. Tapping one opens the app, not yet its task. - D6 Four tables:
project,section,task,label(recommended, as Train keeps a workout and its series apart, D14): one row each, changed when edited, the last writer winning, every payload JSON with aschemaVersion, UUID v7 ids. Inbox is a project the app makes once per person, under a row ID keyed by the account (Train's D57,keyedRowId), not marked in its payload (as built, 1 Nov): every device makes the same row, so two making it offline write one, and the instance can tell neither which row is the Inbox nor whose it is; a payload's mark would let a moved row claim to be it. A sub-task is a task naming its parent. A task's payload: title, description, its project and section, its parent, its place among its siblings, a date (a day, or a day and a time with an optional duration, the time as the person's wall clock with its time zone), a deadline (a day), a priority (1–4), its labels' ids, a recurrence (D7), its reminders, when it was completed. Completing a recurring task moves its date on and writes the occurrence done into its payload's history, so Reporting can count it. Comments and attachments are a later step (attachments need sealed files, which no Halkyon app has yet). - D7 A recurrence is a structured rule, set through a picker (recommended, over
parsing what is typed): every n days, weeks (on which weekdays), months (on which day,
or the nth weekday), years, from the date or from completion, until a day or for n
times. Dates and tokens typed into quick add ("tomorrow 6am", "every Monday",
#project,p1), in English and German, are a later step, built on the same rule. As built (1 Nov): the rule is part of the task's date (RepeatinTaskDate, its payload'sdate.repeat), so a task without a date doesn't recur and clearing the date ends it; schema 1 holds it, as no task was written before. Done, a task counted from its date moves to the rule's first day after both its date and today (an overdue one isn't moved onto a day already past), counted from completion it moves the rule's units on from today, on no particular weekday; its deadline moves as far, its time is kept, its done sub-tasks open again, and when it was done is added to itscompletions. Undo writes back every task that changed. The picker offers the rules that fit its day (every day, every weekday, its weekday, its day or nth weekday of the month, its day of the year) and a rule of the person's own; weeks start on Monday for every n weeks, until the week's first day is a setting (D4). - D8 The first version (recommended): Inbox and projects with sections, shown as a list; sub-tasks; Today (Overdue first, rescheduling them together, then today's); Upcoming as an agenda with the week strip opening into the month; the task sheet (rows for the fields set, chips for the others, the sub-tasks); quick add; labels; completed tasks shown or hidden per view; reminders on the device. Later, each put to the developer when its turn comes: the board and calendar layouts, Upcoming's 3-day grid, each view's Display sheet (grouping, sorting, filters), saved filters, Reporting and goals (karma not before the developer asks for it), location reminders (come with D32), comments and attachments, typed dates, sharing.
- D9 Colours that mean something come from halkyon_ui's palette (recommended): a
project's colour is picked from
PaletteColor, with its localised name, as Learn's colours are; priorities and dates take palette colours chosen not to blur against the strawberry wash, so overdue and P1 are cerise, not strawberry, P2 carrot, P3 cobalt, P4 none; tomorrow and this week as Todoist's meanings go, in palette colours. Each is checked against the wash in light and dark before it is kept. - D10 The icon stays as ADR 0031 drew it (cobalt through pacific into fern), though the wash is strawberry: Learn's icon ends in strawberry and washes in cobalt, so an app's wash and its icon already differ, and Todo's and Meditate's icons are kept related (ADR 0031, D1). The agent made no recommendation; this is the default the decision of 1 Nov left in place, to be asked again if the screens look wrong beside it. Asked again on 2 Nov: changed by ADR 0031, D5, the icon now fading from cobalt through violet into strawberry, its backdrop.
Decisions after trying it (1 Nov 2026, the developer's)
The developer used the first build beside Todoist's and asked for these; where an alternative was put to them, the choice is theirs.
- D11 Sheets are dialogs on the desktop, at every width (over the window's size
alone, and over Todo alone): halkyon_ui's
showAdaptiveSheetshows a centred dialog (Material's term) on Linux, macOS and Windows however narrow the window, and on phones and tablets a bottom sheet below expanded windows as before, so Learn and Train follow. A bottom sheet is a touch surface; rising from the foot of a desktop window it reads as a phone's screen. In a narrow window the dialog keeps a smaller margin. A sheet that can't go through it askssheetShowsAsDialogand follows the same rule: Learn's conversation is a dialog as tall as the window there, nothing to drag. Train's route import still opens its bottom sheet everywhere, until Train 0.1.1 is out. - D12 The tabs are Inbox, Today, Upcoming, Browse; Today still opens (amends D2's order, not what opens).
- D13 A task's sheet names where the task is on top (its project, an icon once tasks
are above it, then each task above it; the project goes to its list, a task opens in
the sheet's place), as Learn's path through a studyset, and has a ⋮ menu in place of
its close button (
SheetScaffold'sheadingandcloseButton): when it was added (from its UUID v7, so the payload needn't say it), Duplicate (with its sub-tasks, right after it), hiding or showing its done sub-tasks (hideDoneSubTasksin its payload, so every device hides them, schema 1 as nothing is released), Close in the pane, and Delete after asking. Copy link, View activity, Email to the task, Print and Open in a new window wait for what they need (deep links, an activity log; mailing in is ruled out by D5). The description's icon stays by its first line. - D14 Rows and menus: a row shows its project and section ("Inbox / Errands" with the
project's icon) at the end of the line of its date, not beside the whole row; every
menu entry has its icon, a destructive one in the error colour (
MenuEntry). A section's heading stays atop the list while its tasks scroll beneath it, until the next pushes it away (PinnedHeaderSliver). Upcoming's month unfolds in place beneath its strip, from its name or its handle dragged down, the arrows then going by month, the handle dragged up folding it; a button beside the month goes back to today once another week shows. Tasks are chosen to act on together, as Todoist's: a row swiped right or held, or Select tasks in Today's, Upcoming's and a project's ⋮, a tap then choosing or letting go (a row held is dragged since D16); the app bar counts them, with an arrow (or back) ending it, and a bar takes the navigation bar's place: date, move, labels (those all have ticked; a tick added to each, one unticked taken off each), priority, and ⋮ with complete (undone together), duplicate, deadline and delete (asked first). Assign waits for shared projects and View activity for a history, neither of which Todo has. A row swiped left asks for its date. Neither swipe removes the row; a sheet's sub-tasks and search's results aren't chosen (TaskSelection, one for the frame). - D15 Typed tokens in quick add (amends D7's later step; the developer's, 1 Nov, after
asking whether the keyboard's language could choose):
#project,@label,{deadline},!!1andp1top4,!30m, dates and times (9am,Mon 18:00,ev Tue 7pm) and recurrences (every day,every 2 days,every second day), each highlighted as it is recognised and undone with a tap. The words are data, a table per language (weekdays, months, tomorrow, every, next, times), read in the app's language and English, so a new app language adds a table, not a parser. Not the keyboard's language: Flutter tells nothing of it, Android doesn't reliably, and it changes as the person switches keyboards while typing. Symbols hold in every language. As built (parseQuickAdd,QuickAddWords.enand.de): a#naming no project stays text, an@naming no label makes one as the task is added; only the first of each kind counts, the rest stays in the title (but for labels and, since D18, reminders); a time alone is its next to come (tomorrow once it has passed today), a weekday alone the coming one (today too),nextone after today; a month needs its day (mayalone is a word), a number alone is none; a recurrence starts on its first day from today (every weekdayon a Sunday: Monday).!30mmakes a task without a time due 30 minutes from now, reminded then, and one with a time reminded 30 minutes before it; with a day but no time it is text. German's weekdays only by their whole names, its short forms (so,do,die,mit) being everyday words. Quick add shows its title alone at first, its chips once typing starts, a chip picked taking its token out of the title, and the description and the deadline behind its + (the description also from the down arrow). As#or@is typed, up to five projects or labels whose name, or a word of it, starts with what follows are offered below the title, one tapped completing it; none for a symbol inside a word (C#), nor once a name is typed whole. - D16 Dragging (the developer's, 1 Nov, as Todoist's): a row of a project's list
(the Inbox's too), held, is dragged into another place, a line showing where: the
upper half of a row puts it before that row, the lower half after it, and the lower
half reached 48 px to the right of where the drag began makes it that row's last
sub-task; a section's heading takes it first in the section, or last in the group
before from its upper half. It goes with its sub-tasks, and Order changed offers
to undo it (
TaskRepository.place, never under itself or its own sub-task). The list scrolls at its edges. Holding a row therefore drags it everywhere it can be dragged and no longer selects it (D14): a swipe to the right does. The add button is dragged straight away, without a hold, its place meanwhile a larger × where dropping it gives up: dropped between a project's rows, quick add adds there (each next task below the one before); on a day in Upcoming, for that day; on Today, for today. Upcoming's rows, overdue ones too, held and dropped on another day move there, their time, duration and recurrence kept, Moved to … offering to undo it; their order within a day stays their times' and priorities', and Today's rows aren't dragged (its Plan's are since D30). - D17 Each project's Display, synced (the developer's, 1 Nov, as Todoist's; D8's
later step): a project's screen, the Inbox's too, has a Display button beside its ⋮, a
dot on it while anything differs from how a project shows at first. Its sheet sets
completed tasks (moved out of the ⋮), grouping (none, which keeps the sections; date,
deadline, added date, priority, label), sorting (manual, name, date, deadline, added
date, priority; a tie, or a task without what is sorted by, last and by hand), and
filters by date and deadline (all, today, this week from the language's first
weekday, next 7 days, this month, next 30 days, none) and by priority and label (any
ticked). Each change is written at once; Reset all puts the project back. Grouped,
the groups stand in the sections' place under headings that stay atop the list
(overdue first, then each day, none last; P1 first; a task under each of its labels);
rows are dragged (D16) only while sorted by hand and grouped by sections. The choices
are kept in the project's sealed row, so every device of the person's shows the
project alike (
ProjectView, the payload'sview, written only once set; values a version doesn't know read as at first). The schema stays 1: an older version reads such a project, and renaming it there drops its Display, which beats a newer schema it couldn't read at all. Upcoming's Display (Today had none until D30) is in a sealed person-settings row (todo_settings, below), which is also to hold D4's settings. The Board (as built, 1 Nov), the Display's Layout (List, Board): a column for the tasks in no section, (No section), always first, then one a section with its menu, each heading counting its open tasks, each task a card, each column ending with Add task, a last column adding a section; grouped otherwise, a column a group, nothing dragged. Below a medium width (the board's own, not the window's, so the pane beside the tabs counts) one column at a time, paged, the next peeking in, settling on the nearest when let go, dots below saying where; from it on, columns 320 wide side by side, scrolled across. Cards are dragged as rows (D16), within and between columns, every scrollable they are in scrolling at its edges; a column takes a card or the add button dropped below its cards, last in it. The Calendar (as built, 1 Nov, the developer's choices): Upcoming's week strip and agenda, shared (DayAgenda), over the project's tasks its Display keeps, sub-tasks too, a day's tasks by time or, sorted otherwise, in that order; No date beside the month opens those without one. Grouping is left out of its Display, and Future occurrences (off at first, kept with the rest) shows each repeating task not done on the days it will come back, the first as if done today (Repeat.next), until its recurrence ends, 400 at most: muted rows with the recurrence's mark in place of the check, opening the task, neither ticked off nor dragged (calendarOf). The month (built 1 Nov 2026, the developer asked for it): from 720 pixels of the Calendar's own width on (seven days of a little over a hundred each, a desktop's window beside its rail; below it the agenda, as on a phone), the month's weeks from the week's first day (monthWeeks), the days of the months beside it muted, each day with as many of its tasks as fit, a line each (its time, its title, its mark in its priority's colour), and the rest counted (3 more), opening all of the day's in a sheet. Past days show their tasks too, overdue or done (monthOf; the agenda counts those as overdue, a month has no such place), later times of repeating ones from today on. The month's name, back to today, No date, the month before and after above it. A task held and dropped on another day moves there as on Upcoming's (DayDropArea,moveTask), a day's free space tapped opens quick add on it in the project (where the agenda has the add button, which can still be dropped on a day), a line tapped opens its task. The 3-day grid stays Upcoming's. Upcoming's Display (as built, 1 Nov, the developer's "as much as possible without my input"): the same sheet over another target (DisplayTarget:projectDisplay,upcomingDisplay), its layout Agenda or 3-day, done tasks, future occurrences, sorting (Smart, by time then priority, as Todoist calls it; name, deadline, added date, priority) and the filters by deadline, priority and label; no grouping nor date filter, Upcoming being days already. In the 3-day grid, Colouring: by priority, by the first label with a colour, or by project. It is kept intodo_settings, a synced table of the same shape aslabel(one row a person, under an ID keyed by the account as the Inbox's,todo/settings, sealed for its table and row;SettingsRepository), the migrationsettingscreating the table, its index onspaceIdand its foreign key to the space, cascading on delete, so a person's deletion takes it with their space. None kept, or one that doesn't open, reads as at first. The 3-day grid (ThreeDayGrid): today and the two days after (never before today), stepped or swiped three days at a time; tasks without a time All day above, timed ones as blocks as long as they last (half an hour at least, to midnight at most), overlapping ones side by side in lanes (layOutDay); a long press on a free half hour marks it and opens quick add there for half an hour; a block opens its task; Overdue opens a sheet of the overdue tasks with Reschedule. A block held and dragged (built 1 Nov 2026) moves its task to another of the three days and another time, its top where it is dropped snapping to a quarter hour (the long press's half hour is too coarse to move a block a little), the time it would take shown under the pointer, its duration, recurrence and reminders kept; dropped on a day's All day, it loses its time and duration. Either can be undone for a moment, as a row dropped on Upcoming's day (moveTask, shared withDayDropArea). A later time of a repeating task doesn't move: only the task itself, at its next time, does. Dragging past the three days shown waits until someone misses it. - D18 Quick add as Todoist's newsletter writes it (the developer's, 3 Nov 2026, the
four recommended, after comparing a newsletter's examples): Check work emails every
weekday at 9am !30m, Prepare document on Monday at 10am for 2h, Stop watching that
viral cat video !1pm !2pm !3pm !4pm, File my tax return every year on 1st March.
onbefore a day is taken with it (Germanam,onandambeing words of the tables,QuickAddWords.on), asat(um) before a time is; before anything that is no day it stays text (Work on report, Treffen am Bahnhof).amafter a number is still the morning (9 am), read first as a time.- Durations:
for(für) and an amount with its unit,for 2h,for 30m,for 1h30m,for 90 min,for 2 hours,für 2 Std,für 30 Min,für 1,5 Std., a decimal point or comma either way, a day at most, intoTaskDate.duration. The units are the tables' (durationUnits), read by a pattern over the text, as a word splits at a comma. Only with a time: a duration lasts from one, so without one (Read for 2h, Read tomorrow for 2h) it stays text, as!30mon a day without a time does (recommended, over giving the task a time to hold it, which would invent one). The date sheet sets it once there is a time (Duration: none, 15, 30, 45 minutes, 1, 1½, 2, 3, 4 hours, as Todoist's time sheet offers), a time cleared taking it with it; a row and the sheet show the span (Tomorrow 14:00–15:30) and the 3-day grid a block as long, a block dragged keeping it (D17). - Every
!30mcounts, each once, sorted ([30, 60]), as do the@labels; the other kinds still count once. With a time, each is so long before it; without a date, the task is due at the latest and each reminds that long from now (Leave !30m !1h: due in an hour, reminded in 30 minutes and then); on a day without a time they stay text. The title's highlighting marks each token, whatever its kind. - Reminders at a time of their own:
!1pm,!13:00,!tomorrow 9am,!Monday 8:30,!morgen um 9 Uhr, as many as typed, into a task'sreminderTimes(ReminderTime: a day and minutes after midnight, the wall clock without a zone, asTaskDate's, so one comes at that hour wherever the device is). A time without a day is on the task's day where it has one (a recurrence's first), else today, or tomorrow where that time has passed, as a time alone is (D15). Each is a moment of its own: it neither needs a date nor follows one, a recurring task's coming once, not every time. A!before no time (!tomorrow, !9) stays text. Planned with the rest (planReminders, an ID of their own each), the notification saying Due at 14:00 for a task with a time, Reminder for one without; the task's sheet now always has its reminders' row, the reminders before its time only where it has one, and those at a time of their own each with its ✕ and Remind at a time (a day, then a time), the permission asked as a first one is chosen. - The payload's schema stays 1 (as D17's view):
reminderTimes, as[{"day": "2026-11-03", "time": 780}], left out where there are none, so older payloads read with none. And from now on a task's payload keeps what this version doesn't know (TaskContent.unknown, the fields at its top that it doesn't read, written back beneath the ones it does, which win): Todo isn't released yet, so every version out keeps a newer one's fields as it changes a task, where Learn's and Train's payloads drop them. Recommended over bumping the schema for each field, which would show a task locked on every device not yet updated. A field a newer version adds within the date, or one whose meaning hangs on fields an older one changes, isn't kept so; such a change still bumps the schema. No server model changes: the payload is sealed.
Decisions after trying 0.1.0 on a phone (3 Nov 2026, the developer's)
The developer's review of 3 Nov 2026, with Todoist's screens beside Todo's; the project label at a row's end the developer's answer (right-aligned, as Todoist's, over left after the date), the rest the developer's asks, carried out as described.
- D19 Today and its tabs' icons: Today's tab shows today's day of the month inside a
blank calendar, as Lucide's
calendar-1holds a 1, drawn again at midnight (TodayIcon); Browse is Lucide'smenu, Display's buttonsquare-menu. Today has at most two groups: what is overdue, folding under its heading (its chevron, beside Reschedule; the fold not kept), and today's, headed by the day alone. A day's heading reads as Todoist's, "Tuesday, 3 Nov" ("Dienstag, 3. Nov.", the pattern a text of each language,dayHeadingPattern); Upcoming's days keep "· Tomorrow" where it helps, Today's has no "· Today". Upcoming's way back to today shows whenever another day than today is picked, not only off this week. - D20 Rows in rounded boxes, a list's rows a group (as Todoist's): each row its own
surface (
TaskTile's box,TaskRowPosition,taskGroup), rounded at the group's ends and slightly at the joins, so a chosen row's colour stays inside it as the list scrolls under a pinned heading (it showed under Upcoming's week strip, as the row's tile was painted on the screen's surface below the list). Its project and section end the line of its date at the row's right edge. - D21 Lists pulled down sync now, as Learn's: Today, Upcoming's agenda, the Inbox and
every project (
TaskRefresh, halkyon_ui'sAppRefreshIndicatorover halkyon_app'ssyncAndWait), an empty list too (PullRoom). A board's columns and the 3-day grid don't, as they scroll sideways or both ways. A project's screens wash in its own colour, as a studyset's in Learn; one without keeps Todo's strawberry. - D22 Display as Todoist's: each layout a phone's outline with its picture, its name below on one line, a radio button marking the one in use; the rest in rounded groups under plain headings. "Add section" in place of "New section".
- D23 Quick add a small bar docked above the keyboard (amends D8, D15): the title, then one row of boxed buttons (+ for the description and deadline, the date, the priority, the labels, the project), each its icon alone until something is set, and a send button, the list in view behind a lighter barrier. Its buttons show from the start (D15 had its chips come with the first letter), as Todoist's bar has them. Where sheets are dialogs, the same bar in a dialog.
- D24 A task's sheet partway up, its parts in rounded boxes (amends D13): on a phone
it opens as high as its content, at most 65 % of the window, and is dragged up to the
whole window or down to close (
DraggableScrollableSheet); the title, description, date and reminders each a box, the priority, labels, deadline and project a row of chips (named while unset), the sub-tasks a box ending in Add sub-task. No principle of the project's argues against Todoist's layout: grouped rounded boxes are what the review asks for across the apps. Todo keeps no comments, so there is no comment field. - D25 Explanations one tap away: what a setting does stands behind halkyon_ui's
HelpButtonbeside its name (TitleWithHelp), not always below it, in Todo and Meditate; what is the person's own (a value) stays in view, as do an empty list's message and quick add's hint while words are highlighted.
Decisions after trying 0.2.0 (4 Nov 2026, the developer's)
The developer's review of 4 Nov 2026, again beside Todoist's screens.
-
D26 What the review fixed and added: a time or duration picked in the date sheet counts however the sheet closes (it had counted only with a day tapped), on the task's day, or today where it had none, as Todoist's; editing a project keeps its Display. Today counts its tasks under its title, overdue and today's together ("14 tasks", as Todoist's), not while tasks are chosen. A row's check stands by its title's first line, not in the middle of the row: a
ListTilecentres its leading or tops it at its padding, never at the title's line, so the check and the texts share its title, the texts lowered by half of what the 48 dp tap target is taller than a line. A sub-task opens in its task's sheet, as the sheet's path did, not in a sheet over it; the sheet keeps what it showed, the system's back returning to the last (agent's choice: aBackButtonListener, over aPopScope, which would also have held back a tap beside the sheet, a swipe and Escape, which close it whole; a picker over the sheet is closed by back first). -
D27 Listed in F-Droid as Task and Calendar & Agenda (the developer's, both, over Time, which F-Droid's categories give to clocks and timers): no other app of the repository used Time, so the repository no longer lists it.
-
D28 A project's editor as Todoist's Add project, with sub-projects and favourites (the developer's, over the dialog of a name and a colour): a screen of its own on a phone (Add project, Edit project, ← and ✓ in its app bar), the same in a dialog where sheets are dialogs; the name counted to 120, a description, then rounded rows: the colour (its palette name, opening the picker), the parent project (No parent or a project, from a sheet listing them indented; never the project itself nor any project below it,
parentChoices, so none becomes its own ancestor), a Favourite switch; then the Layout, Display's pictures (LayoutPicker). All of it is in the project's sealed payload (description,parentId,favorite,defaultView), no server model changed, no migration, the schema still 1; the payload now keeps the fields it doesn't know, as a task's (D18), so a later version's survive an edit here (a 0.2.0 device editing a project still drops these). The default Display: a new project starts with the layout picked, and Display's Reset all returns to it (defaultView, the dot on Display marking a Display that differs from it); the editor sets its layout, and a layout picked anew there shows at once. Projects made before keep List as their default. Browse lists each project's sub-projects indented under it, folding under its chevron (the fold not kept, agent's choice: a choice of the device, and quick to make again), and the favourite projects in a Favourites group above the projects. A project whose parent is gone or doesn't open here, or projects whose parents run in a circle (two devices each moving one under the other), stand as projects of their own, none lost (projectTree). Deleting a project deletes its sub-projects, with their sections and tasks, as Todoist does, the confirmation naming them first (TodoPerson.deleteProject). Alternatives: the sub-projects moved up a level (kept, but surprising after a confirmation that named only the parent); a depth limit as Todoist's (four levels), left until deep trees turn out to be a problem. -
D29 Quick add keeps a word as text where it stands, and from Backspace too (amends D15; the developer's, 4 Nov 2026). A word tapped had been kept as text by its text, so in "Watch The Daily Show Daily" tapping the first Daily also stopped the second one making the task repeat. A word kept is now that occurrence, a range of the title (
QuickAddRange,parseQuickAdd(ignored:); a token overlapping one stays text), which follows the edits: the edit found from the common start and end of the text before and after, ranges after it moved, one it touches dropped as its word has changed (shiftKept); typing right before or after a kept word leaves it kept. Backspace right after a highlighted word keeps it as text, as Todoist's: the character isn't deleted, the next Backspace deletes it (a quick add input formatter, so the keyboard's own text is put back too); the hint under the title says so. And a word is highlighted while the keyboard still composes it: Android's keyboards (Gboard with suggestions, Japanese ones) compose a word until a space, and the title had shown the plain composing text until then; the highlight is drawn with the composing underline over it. Alternatives: keeping by text but only the word tapped and those before it (still surprising, and lost on reordering); a long press instead of Backspace (undiscoverable, and the tap already does it). The last button dictates while the title is empty, as Todoist's and as Learn's assistant field (ADR 0021, D69): the microphone, filled as the send button is, dictating into the title where its cursor stands and listening again after each pause (halkyon_app'sDictationon the device's recogniser,TodoDependencies.speech), what is said read as if typed; a stop button in the container colour while it listens; the send button once there is text, and alone where the device recognises no speech (Linux). A phone without a recogniser (GrapheneOS without Google's services) is told how to get one (halkyon_app'sspeechNoRecognizer). Android declares the microphone and the recognition service it queries, iOS and macOS the usage texts and macOS the audio input entitlement, as Learn's. Alternatives: a microphone beside the send button always (one more button in a bar already full, and Todoist's swaps them). -
D30 Today planned on a calendar, as Todoist's desktop Today (the developer's, 4 Nov 2026, from their screenshots of Todoist's desktop app): Today gets a Display (amends D17's "Today has none"), its Layout alone, List or Calendar, with Display's pictures (
LayoutPicker; no Board, a day having no sections), kept in the person's sealed settings row as Upcoming's (TodayViewintodo_settings, the payload'stoday, schema still 1: an older version reads it, and writing its settings there drops it, as D17 accepted for a project's view). Calendar is one day of hours, Upcoming's 3-day grid generalised to any number of days (HourGrid, D17'sThreeDayGridrenamed,days: 1): today's tasks without a time All day on top, timed ones as blocks as long as they last, overlapping ones side by side, a line at the time it is now (its time in cerise in the hours' column, drawn again each minute; Upcoming's grid has it too), a block dragged to another hour moving there (snapping to a quarter hour), onto All day losing its time, a long press adding a task, as D17. With one day there are no day names, arrows nor Overdue button: what is overdue waits in the Plan. The day scrolls to the hour before now at first. The Plan (PlanPanel): Plan: 3 in Today's app bar, in the Calendar layout alone, shows or hides it, counting what it holds. Its groups, each folding under its heading with its count (the fold not kept): Overdue with Reschedule (Today's, in cerise as Todoist's), To plan (today's tasks without a time; the developer's working name, Todoist naming no such group), Time-blocked (today's with a time, Todoist's word; German Zu planen and Eingeplant); done tasks and later times of repeating ones are never in it, nor in the day, so neither is planned. All projects opens Filter by project, a field searching the projects' names and a box to tick for each (the Inbox first, a sub-project with its parents' path, "Life / Home", D28), several at once, applied as ticked; it narrows the Plan alone, not the day, and is kept while the app runs, not synced (agent's choice: a filter for one planning session). A row held and dropped on an hour is moved to today at that time (its top where the pointer is, snapped as a block's), its duration kept or, without one, half an hour drawn; a block dropped on the Plan loses its time and stays today; both throughmoveTask, Moved to … offering to undo it; a Plan's row dropped back on the Plan changes nothing. Where it stands: from a medium width of Today's own on (the window-size classes over the body's width, as the board's D17, so the task pane beside the tabs counts), a panel 340 wide beside the day, shown or hidden as the person last left it, remembered with the settings (TodayView.plan, open at first); on a phone, a sheet over the day's foot (the developer's choice, over the Plan on wide windows alone and over a strip of tasks above the day), half the body high above the floating bar, closed until asked and while the app runs; a row held there lowers the sheet to its heading, the day free behind it, and it rises again once the row is dropped. The sheet is part of Today's screen, not a modal route (agent's choice): a modal sheet's barrier would take the drop meant for the day behind it, and closing the route under a drag ends the row's draggable. The add button hides while the phone's sheet is open. Alternatives: the Plan on wide windows alone (a phone couldn't plan by dragging), a strip above the day (too little room for three groups), the project filter synced (a planning session's narrowing would follow the person to their other devices). -
D31 Reminders at the minute, and the permission to notify asked wherever one is planned (the developer's, 4 Nov 2026, after Todo's reminders never showed on a phone, GrapheneOS on Android 14, installed afresh from F-Droid). Two faults: the permission to notify (Android 13's
POST_NOTIFICATIONS, iOS' and macOS' alert) was asked only where a task's sheet chose its first reminder, so reminders typed in quick add (!30m,!1pm), added by the automatic reminder (D4) or brought by sync were planned and scheduled, and the system dropped every one without a word; and exact alarms,SCHEDULE_EXACT_ALARM, are off from Android 14 on until the person allows them in Alarms & reminders, so they came inexact, minutes late.- Asked wherever a reminder is planned: whenever the plan of the device's
reminders holds one (
ReminderScheduler),NotificationAccessreads whether the device may notify (ReminderGateway.notificationsAllowed: Android'sareNotificationsEnabled, Apple'scheckPermissions; Linux and Windows ask nothing) and where it may not, asks, once an app start, the device's and not the person's: refused, nobody is asked again and again (Android itself stops showing its dialog after two refusals, iOS after one). The task sheet's ask goes through the same place, so the plan's doesn't follow it. - Settings › Reminders says Notifications are off while reminders are planned
and the device may not show them, with Turn on: asks again, and where that doesn't
allow it (refused again, or the system asks no more), opens the system's
notification settings of the app (the plugin's
openAppNotificationSettings, no package or platform code of Halkyon's added). Read again whenever the app comes back, so notifications allowed there count at once. (Agent's choice: Turn on asks first and opens the settings after a refusal, over telling the two apart with Android'sshouldShowRequestPermissionRationale, which would have needed a method channel of the app's own for a case that ends in the settings either way.) - Exact alarms through
USE_EXACT_ALARM(the developer's choice), granted with the install from Android 13 on to an app whose purpose is reminding of what a person planned (calendars, alarm clocks, reminders), withSCHEDULE_EXACT_ALARMstill declared for Android 12 and 12L, which don't know the other, up to API 32 alone (test/android/exact_alarms_test.dart).AlarmManager.canScheduleExactAlarmsis true with either, so the plugin'scanScheduleExactNotificationsis, and reminders areexactAllowWhileIdle, a snoozed reminder too (the developer's, as it had been inexact while snoozes were minutes long; an idle phone may bring an inexact one later than the snooze lasts). Alternatives: leading the person to Alarms & reminders (one more permission to ask and explain, which most would never find again once refused), staying inexact (reminders minutes late, which a reminder at a time can't be). Google Play reviewsUSE_EXACT_ALARMagainst that purpose; Todo is one, and F-Droid asks nothing of it.
- Asked wherever a reminder is planned: whenever the plan of the device's
reminders holds one (
-
D32 A task's locations, with reminders on arriving or leaving (the developer's, 5 Nov 2026): a location says where a task happens; a task may have several, which count as alternatives, each with its own reminder: none, on arriving, or on leaving.
TaskLocation(name, address text, position, optional OSM type and ID, trigger) goes in the task's sealed payload aslocations, schema still 1 (D18's unknown fields stay); recent places are derived from the tasks (recentLocations), not kept.- Finding a place. People don't know addresses, so a search starts from a city or
district and a name (
Lindau EDEKA,Aeschach EDEKA), and the map is part of picking, not an extra (the developer's): results list name, then street, district and city, so that two EDEKAs differ, and show as pins; a pin or a row chooses, and a tap on the map places a pin by hand. The phone's last known position centres the map and biases the search, where it may be read.PlaceSearchGatewayis the seam:TodoDependencies.placesis the instance's search once wired, and until thenPublicPhotonGatewayto komoot's public Photon, in a debug build asked for it alone (--dart-define=TODO_PUBLIC_PHOTON=true), with a generic User-Agent and no personal data; without either the picker says search isn't available. Car parks and charging stations named after a place are left out (seen onLindau EDEKA). The map is Train's (flutter_map,vector_map_tiles, OpenFreeMap's Liberty and Dark), a small copy ofvectorTiles, no tile layer in widget tests. Alternatives named: a street-address form (refused: nobody knows them), the picker without a map (refused by the developer). - Quick add.
+is a keyword as#and@: recent places suggested as it is typed, below them the way into the search with what was typed; a place named before becomes a token (parseQuickAdd'slocations), kept as text by a tap or Backspace as any other (D29); an unknown+word, or a+a space follows, stays text. A + menu entry and a pin button with the name or a count do the same by hand. The task sheet has a Where box, each place with its ⋮; a task's row shows the name and how many more. - Reminders are on the device, Android alone (
LocationReminderGateway, a no-op elsewhere, which offers none), as the instance reads no task: the system'sLocationManager.addProximityAlert, 150 m, no Google services, which GrapheneOS lacks. Planned from the open tasks asReminderSchedulerplans reminders (planLocationWatches), cancelled inTodoPerson.dispose, kept in the app's private preferences (titles unsealed there, as reminders' are;allowBackupis off), registered anew after a restart or an update and on the process's first plan, as a force stop drops them unseen. Fused-provider geofencing was refused: it is Google's. - Dwell, so that driving past an EDEKA on the way to the next doesn't remind (the
developer's question): crossing the circle schedules a check two minutes later, a
crossing back cancels it, and the check notifies only if the phone is still on that
side (
LocationDwell, pure, with JUnit tests CI doesn't run yet), by a fix at most 90 s old where there is one, else by the system's last event. A red light of over two minutes beside the shop still reminds; that is accepted for now. - Once: a task reminds once until it changes (title, day, places:
stamp), and an alternative that reminded stops the task's others. The notification goes on the reminders' channel and opens the app. - Permission: the position first, then, told why, Allow all the time in the system's settings, asked as a person chooses such a reminder and never by planning (a task may sync in); Settings' Reminders says while it is off, with Turn on, as D31.
- A debug build installs beside the release (
.debug, Todo Debug), the keys differing; SimpleLogin's way back keeps the scheme the instance allows, so with both installed Android asks which app answers it. Debug builds log toadb logcat -s HalkyonLocation.
- Finding a place. People don't know addresses, so a search starts from a city or
district and a name (
Steps
- The frame: go_router and
NavigationFramewith the four tabs (empty screens that say what comes), the strawberry wash, the cog, Settings as D4 with what exists today (the account, What's new, About, the version); the account sheet removed. - The first synced model:
content_keyandproject(Inbox made once), its gateway and repository on Learn's and Train's pattern, the server's access rules each with a test asserting denial; walked through with the developer as the first of Todo's. - Tasks:
section,task,label; Inbox and a project's list; the task sheet; quick add. - Today and Upcoming, recurrence (D7), reminders on the device.
- Todo's first release (0.1.0), its What's new drafted by
tool/release.dart. Decided 2 Nov 2026 (the developer's, the first two the agent's recommendations):- Deployed beside Learn's server, as Train is (ADR 0030, D46):
deploy/compose.todo.yamlat todo.halkyon.eu, a member of Learn's register, its databasehalkyon_todounder a role of its own, taken in byredeploy.shandbackup.shwhere.envswitches it on, both now going over every app server so switched on (train,todo). Each compose file names its server to the register in a variable of its own,INSTANCE_MEMBER_TRAIN,INSTANCE_MEMBER_TODO, which halkyon_base adds toINSTANCE_MEMBERS, each address once (agent's choice): a second compose file's default ofINSTANCE_MEMBERSwould have replaced Train's, and Learn's register would no longer have told Train of an account gone. Alternatives: the release config alone, Todo's instance later. - A key of its own (as Train's D58): a PKCS #12 keystore at
~/.local/share/halkyon-todo/android-release.p12, aliashalkyon-todo, in Proton Pass as Halkyon Todo – Android release keystore, pinned intool/release/todo-android-certificate.sha256(made 2 Nov 2026, in memory alone,$XDG_RUNTIME_DIR, and kept nowhere but Proton Pass). Alternative: Learn's key, which would let one app's build pass for the other's update. - The APK, in the F-Droid repository, and a snap (the developer's, over the APK
and F-Droid alone): the listing
app.halkyon.todo.yml(categories Task and Calendar & Agenda since D27,NonFreeNetfor the server's SSPL, as Learn's says), the snaphalkyon-todoto the Store's edge channel, as Learn's, fromapps/todo/snap/. snapcraft builds one recipe a folder,snap/snapcraft.yaml, and the root's is Learn's, so the release tool builds Todo's in a clone of the repository at the tag, kept in~/.cache/halkyon-release/snap/halkyon-todoso that its build machine and Flutter part stay from one release to the next, Todo's folder in place of the clone'ssnap/but forsnap/local/, which the recipes share (agent's choice; over a worktree, whose.gitfile points to a folder LXD doesn't see, and over moving Learn's recipe, which its releases and docs name). The other platforms come later. - Android's backup and transfer to another phone are off, as Train's D53 (agent's choice): the reminders scheduled keep their tasks' titles unsealed on the phone.
- Deployed beside Learn's server, as Train is (ADR 0030, D46):
Consequences
- No email reminder, morning overview or evening review: the server can't read what is due. A device without the account's keys neither reads nor writes Todo's rows, as in Train.
- Todo's server holds no logic over tasks; what Today, Upcoming and Reporting show is worked out on the device, over every task the person has. That is fine for thousands of tasks; a person with far more would need the device to keep an index of its own.
- Sharing a project is harder than with readable rows (a key per shared project, its members' keys wrapping it); D5 accepts that for now.