todos: a backburner column, and the parked items on it

backburner joins TODO_STATUSES and the board's columns; the three items in the
backburner theme move onto the status, so parked is now a lane rather than a
folder you have to remember to open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 04:34:41 +00:00
co-authored by Claude Fable 5
parent 03601db69c
commit db96f53bc2
7 changed files with 255 additions and 39 deletions
+33 -33
View File
@@ -22,39 +22,39 @@ the owner's OS user and can never be granted. Indirection there really is accide
**90 open, 29 closed**, across 31 themes. **90 open, 29 closed**, across 31 themes.
| Theme | Open | Closed | | Theme | Open | Closed |
| --- | --- | --- | | ------------------------------------------------------------------------------------------ | ---- | ------ |
| [Affordances](todo/affordances/) | 1 | 0 | | [Affordances](todo/affordances/) | 1 | 0 |
| [Agents coordinating agents](todo/agent-coordination/) | 2 | 0 | | [Agents coordinating agents](todo/agent-coordination/) | 2 | 0 |
| [Speaking into a session from the backend](todo/agent-delivery/) | 1 | 1 | | [Speaking into a session from the backend](todo/agent-delivery/) | 1 | 1 |
| [Backburner](todo/backburner/) | 3 | 0 | | [Backburner](todo/backburner/) | 3 | 0 |
| [Background jobs](todo/background-jobs/) | 2 | 1 | | [Background jobs](todo/background-jobs/) | 2 | 1 |
| [Chat interface](todo/chat/) | 1 | 0 | | [Chat interface](todo/chat/) | 1 | 0 |
| [Code editor](todo/code-editor/) | 1 | 0 | | [Code editor](todo/code-editor/) | 1 | 0 |
| [Components — the shared UI layer](todo/components/) | 1 | 0 | | [Components — the shared UI layer](todo/components/) | 1 | 0 |
| [File watchers](todo/file-watchers/) | 1 | 0 | | [File watchers](todo/file-watchers/) | 1 | 0 |
| [Fleet — one command across every server](todo/fleet/) | 5 | 2 | | [Fleet — one command across every server](todo/fleet/) | 5 | 2 |
| [Headscale](todo/headscale/) | 0 | 5 | | [Headscale](todo/headscale/) | 0 | 5 |
| [Infra (alpha)](todo/infra/) | 7 | 0 | | [Infra (alpha)](todo/infra/) | 7 | 0 |
| [Known bugs](todo/known-bugs/) | 3 | 2 | | [Known bugs](todo/known-bugs/) | 3 | 2 |
| [Mobile apps](todo/mobile-apps/) | 3 | 0 | | [Mobile apps](todo/mobile-apps/) | 3 | 0 |
| [Multi-user](todo/multi-user/) | 16 | 5 | | [Multi-user](todo/multi-user/) | 16 | 5 |
| [Notifications](todo/notifications/) | 1 | 0 | | [Notifications](todo/notifications/) | 1 | 0 |
| [Plugin ecosystem — before it opens to third parties (2026-08-18)](todo/plugin-ecosystem/) | 3 | 0 | | [Plugin ecosystem — before it opens to third parties (2026-08-18)](todo/plugin-ecosystem/) | 3 | 0 |
| [Plugin infrastructure](todo/plugin-infrastructure/) | 2 | 0 | | [Plugin infrastructure](todo/plugin-infrastructure/) | 2 | 0 |
| [Plugin repos — what writing their READMEs turned up](todo/plugin-repos/) | 10 | 9 | | [Plugin repos — what writing their READMEs turned up](todo/plugin-repos/) | 10 | 9 |
| [Plugin system — gaps the email extraction found (2026-08-18)](todo/plugin-system/) | 3 | 0 | | [Plugin system — gaps the email extraction found (2026-08-18)](todo/plugin-system/) | 3 | 0 |
| [Prompt library](todo/prompt-library/) | 1 | 0 | | [Prompt library](todo/prompt-library/) | 1 | 0 |
| [SendOff](todo/sendoff/) | 1 | 0 | | [SendOff](todo/sendoff/) | 1 | 0 |
| [Settings — portable, and readable by an agent](todo/settings/) | 2 | 0 | | [Settings — portable, and readable by an agent](todo/settings/) | 2 | 0 |
| [Shell — spotlight and the dock](todo/shell/) | 4 | 1 | | [Shell — spotlight and the dock](todo/shell/) | 4 | 1 |
| [Sub-agents — agents spawning agents](todo/sub-agents/) | 0 | 2 | | [Sub-agents — agents spawning agents](todo/sub-agents/) | 0 | 2 |
| [Sudo terminal modal](todo/sudo-terminal-modal/) | 1 | 0 | | [Sudo terminal modal](todo/sudo-terminal-modal/) | 1 | 0 |
| [Tooling — the list about the list](todo/tooling/) | 0 | 1 | | [Tooling — the list about the list](todo/tooling/) | 0 | 1 |
| [Transmission](todo/transmission/) | 6 | 0 | | [Transmission](todo/transmission/) | 6 | 0 |
| [URL state — routing, query strings and selection](todo/url-state/) | 4 | 0 | | [URL state — routing, query strings and selection](todo/url-state/) | 4 | 0 |
| [Waiting on the owner to run it](todo/waiting-on-owner/) | 3 | 0 | | [Waiting on the owner to run it](todo/waiting-on-owner/) | 3 | 0 |
| [Workspaces & Panels](todo/workspaces-panels/) | 2 | 0 | | [Workspaces & Panels](todo/workspaces-panels/) | 2 | 0 |
## Records — no items, kept for the reasoning ## Records — no items, kept for the reasoning
+1 -1
View File
@@ -16,7 +16,7 @@ import { join, resolve } from 'node:path';
/** Resolved per call, not at module load: the server asserts its cwd is the repo root at boot. */ /** Resolved per call, not at module load: the server asserts its cwd is the repo root at boot. */
const todoDir = () => resolve(process.cwd(), 'todo'); const todoDir = () => resolve(process.cwd(), 'todo');
export const TODO_STATUSES = ['open', 'doing', 'blocked', 'done'] as const; export const TODO_STATUSES = ['open', 'doing', 'blocked', 'backburner', 'done'] as const;
export type TodoStatus = (typeof TODO_STATUSES)[number]; export type TodoStatus = (typeof TODO_STATUSES)[number];
export type TodoItem = { export type TodoItem = {
@@ -0,0 +1,215 @@
import type { DragEvent } from 'react';
import { useState } from 'react';
import { Link, useSearchParams } from 'react-router';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { AlertTriangle, CalendarClock, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { TodoItem } from './useTodos';
import { useTodoItem, useTodoPatch, useTodoTree } from './useTodos';
/**
* The board over `todo/` — the platform's own backlog, one file per item (todo/README.md).
*
* Selection and the theme filter live in the URL (`?selected=`, `?theme=`), per the navigation audit:
* a card is a real link, a filtered board is an addressable page, and back means what it should.
*
* Writes go through PATCH /todos/item, which touches frontmatter only and regenerates TODO.md — this
* board never holds state the files do not; a drag is a file edit and a refetch.
*/
const COLUMNS = [
{ status: 'open', label: 'Open' },
{ status: 'doing', label: 'Doing' },
{ status: 'blocked', label: 'Blocked' },
{ status: 'backburner', label: 'Backburner' },
{ status: 'done', label: 'Done' },
] as const;
export const TodoBoardPanel = () => {
const { data, isLoading, error } = useTodoTree();
const patch = useTodoPatch();
const [params] = useSearchParams();
const selected = params.get('selected');
const themeFilter = params.get('theme');
const [dragOver, setDragOver] = useState<string | null>(null);
const href = (changes: Record<string, string | null>): string => {
const next = new URLSearchParams(params);
for (const [key, value] of Object.entries(changes)) {
if (value === null) next.delete(key);
else next.set(key, value);
}
const qs = next.toString();
return qs ? `?${qs}` : '.';
};
if (error) {
return (
<div className="flex h-full items-center justify-center p-8 text-center text-sm text-muted-foreground">
The todo board is owner-only, and this account was refused. Nothing here is broken — there is simply nothing to
show.
</div>
);
}
if (isLoading || !data) {
return <div className="flex h-full items-center justify-center text-sm text-muted-foreground">Reading todo/…</div>;
}
const themes = data.themes.filter((t) => t.items.length > 0);
const items = themes.flatMap((t) => t.items);
const visible = themeFilter ? items.filter((i) => i.theme === themeFilter) : items;
const drop = (status: string) => (ev: DragEvent) => {
ev.preventDefault();
setDragOver(null);
const id = ev.dataTransfer.getData('text/todo-id');
if (id) patch.mutate({ id, status });
};
return (
<div className="flex h-full min-h-0">
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex flex-wrap items-center gap-1.5 border-b border-border px-4 py-2">
<Link
to={href({ theme: null })}
className={cn(
'rounded-full px-2.5 py-0.5 text-xs',
!themeFilter
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:text-foreground',
)}
>
All · {items.filter((i) => i.status !== 'done').length}
</Link>
{themes.map((t) => {
const open = t.items.filter((i) => i.status !== 'done').length;
return (
<Link
key={t.theme}
to={href({ theme: t.theme === themeFilter ? null : t.theme })}
className={cn(
'rounded-full px-2.5 py-0.5 text-xs',
themeFilter === t.theme
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:text-foreground',
)}
>
{t.heading.split('—')[0]?.trim()} · {open}
</Link>
);
})}
</div>
<div className="flex min-h-0 flex-1 gap-3 overflow-x-auto p-4">
{COLUMNS.map((col) => {
const cards = visible
.filter((i) => i.status === col.status)
.sort((a, b) => (b.priority === 'urgent' ? 1 : 0) - (a.priority === 'urgent' ? 1 : 0));
return (
<div
key={col.status}
onDragOver={(ev) => {
ev.preventDefault();
setDragOver(col.status);
}}
onDragLeave={() => setDragOver(null)}
onDrop={drop(col.status)}
className={cn(
'flex w-64 shrink-0 flex-col rounded-lg border border-border bg-muted/30',
dragOver === col.status && 'border-primary bg-primary/5',
)}
>
<div className="flex items-baseline justify-between px-3 py-2 text-xs font-semibold text-muted-foreground">
<span>{col.label}</span>
<span>{cards.length}</span>
</div>
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-2 pb-2">
{cards.map((item) => (
<TodoCard
key={item.id}
item={item}
href={href({ selected: item.id })}
selected={selected === item.id}
/>
))}
</div>
</div>
);
})}
</div>
</div>
{selected ? <TodoDetail id={selected} closeHref={href({ selected: null })} /> : null}
</div>
);
};
type TodoCardProps = { item: TodoItem; href: string; selected: boolean };
const TodoCard = ({ item, href, selected }: TodoCardProps) => (
<Link
to={href}
draggable
onDragStart={(ev) => ev.dataTransfer.setData('text/todo-id', item.id)}
className={cn(
'block rounded-md border border-border bg-card p-2.5 text-left shadow-sm hover:border-primary/50',
selected && 'border-primary',
)}
>
<div className="text-xs leading-snug text-card-foreground">{item.title}</div>
<div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[10px] text-muted-foreground">
<span className="rounded bg-muted px-1 py-px">{item.theme}</span>
{item.priority === 'urgent' && (
<span className="flex items-center gap-0.5 rounded bg-destructive/15 px-1 py-px text-destructive">
<AlertTriangle className="h-2.5 w-2.5" /> urgent
</span>
)}
{item.due && (
<span className="flex items-center gap-0.5">
<CalendarClock className="h-2.5 w-2.5" /> {item.due}
</span>
)}
<span>{item.created}</span>
</div>
</Link>
);
type TodoDetailProps = { id: string; closeHref: string };
const TodoDetail = ({ id, closeHref }: TodoDetailProps) => {
const { data, isLoading } = useTodoItem(id);
const patch = useTodoPatch();
const status = data?.meta.status ?? 'open';
return (
<div className="flex w-[28rem] shrink-0 flex-col border-l border-border">
<div className="flex items-center justify-between gap-2 border-b border-border px-4 py-2">
<span className="truncate font-mono text-xs text-muted-foreground">{id}</span>
<div className="flex items-center gap-2">
<select
value={status}
onChange={(ev) => patch.mutate({ id, status: ev.target.value })}
className="rounded border border-border bg-background px-1.5 py-0.5 text-xs"
>
{COLUMNS.map((c) => (
<option key={c.status} value={c.status}>
{c.label}
</option>
))}
</select>
<Link to={closeHref} className="text-muted-foreground hover:text-foreground">
<X className="h-4 w-4" />
</Link>
</div>
</div>
<div className="file-viewer-md min-h-0 flex-1 overflow-y-auto px-4 py-3 text-sm">
{isLoading || !data ? (
<span className="text-sm text-muted-foreground">Reading…</span>
) : (
<ReactMarkdown remarkPlugins={[remarkGfm]}>{data.body}</ReactMarkdown>
)}
</div>
</div>
);
};
+3 -2
View File
@@ -38,8 +38,9 @@ process everywhere, report which host is behind…
- **`id`** — `<theme>/<slug>`, and it is the filename. Stable: rename the title freely, leave the id - **`id`** — `<theme>/<slug>`, and it is the filename. Stable: rename the title freely, leave the id
alone. Anything pointing at an item points at this. alone. Anything pointing at an item points at this.
- **`status`** — `open` or `done` today. The board will want more (`doing`, `blocked`); adding one is a - **`status`** — `open`, `doing`, `blocked`, `backburner` or `done`. The board (`/todos`) renders one
new value here, not a new file or a new directory. column per value; adding one is a new entry in `TODO_STATUSES` (`api/todos/todo-tree.ts`), not a new
file or directory. `backburner` is parked-not-dead — still counted as open by the index.
- **`created`** — the date the item was raised. **`date_source`** says how much to trust it: - **`created`** — the date the item was raised. **`date_source`** says how much to trust it:
`stated` (the prose named a date — true), `section` (inherited from its old section heading — close), `stated` (the prose named a date — true), `section` (inherited from its old section heading — close),
`assumed` (unknown, floored at 2026-08-20, the day before the split — a bounded "before today" `assumed` (unknown, floored at 2026-08-20, the day before the split — a bounded "before today"
@@ -2,7 +2,7 @@
id: backburner/four-foreign-keys-are-over-postgres-s-63 id: backburner/four-foreign-keys-are-over-postgres-s-63
title: "Four foreign keys are over Postgres's 63-character limit and re-create on every `db:push`" title: "Four foreign keys are over Postgres's 63-character limit and re-create on every `db:push`"
theme: backburner theme: backburner
status: open status: backburner
created: 2026-08-19 created: 2026-08-19
date_source: stated date_source: stated
order: 1 order: 1
+1 -1
View File
@@ -2,7 +2,7 @@
id: backburner/music-tagging-feature id: backburner/music-tagging-feature
title: "Music tagging feature" title: "Music tagging feature"
theme: backburner theme: backburner
status: open status: backburner
created: 2026-08-20 created: 2026-08-20
date_source: assumed date_source: assumed
order: 2 order: 2
+1 -1
View File
@@ -2,7 +2,7 @@
id: backburner/resurrect-projects-apps id: backburner/resurrect-projects-apps
title: "Resurrect Projects/Apps" title: "Resurrect Projects/Apps"
theme: backburner theme: backburner
status: open status: backburner
created: 2026-08-25 created: 2026-08-25
date_source: stated date_source: stated
order: 3 order: 3