+
+
+
+
+ Accent
+ H {tweaks.accentHue}
+
+
+ {HUE_PRESETS.map((p) => (
+
update('accentHue', p.h)}
+ />
+ ))}
+ update('accentHue', +e.target.value)}
+ style={{ flex: 1, marginLeft: 6 }}
+ />
+
+
+
+
+ Gap
+ update('islandGap', +e.target.value)} />
+ {tweaks.islandGap}px
+
+
+ Radius
+ update('islandRadius', +e.target.value)} />
+ {tweaks.islandRadius}px
+
+
+ Grain
+ update('grainOpacity', +e.target.value)} />
+ {tweaks.grainOpacity.toFixed(3)}
+
+
+ Sidebar
+ update('sidebarWidth', +e.target.value)} />
+ {tweaks.sidebarWidth}
+
+
+
Density
+
+
+
+
+
+
+ );
+};
+
+// Windows chrome
+const TitleBar = ({ search }) => {
+ return (
+
+
+
+
+ ClaudeDo · Rider Island
+
+
+
+
+
+
+
+
+ );
+};
+
+const Taskbar = () => {
+ const [clock, setClock] = useState(() => {
+ const n = new Date();
+ return {
+ time: n.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
+ date: n.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }),
+ };
+ });
+ useEffect(() => {
+ const id = setInterval(() => {
+ const n = new Date();
+ setClock({
+ time: n.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
+ date: n.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }),
+ });
+ }, 30000);
+ return () => clearInterval(id);
+ }, []);
+ const icons = ['windows', 'search', 'folder', 'inbox', 'note', 'calendar'];
+ return (
+
+ {icons.map((ic, i) => (
+
+
+
+ ))}
+
+
{clock.time}
+
{clock.date}
+
+
+ );
+};
+
+// ---------- App ----------
+const App = () => {
+ const [tweaks, setTweaks] = useState(TWEAK_DEFAULTS);
+ const [tweaksOpen, setTweaksOpen] = useState(false);
+ const [editMode, setEditMode] = useState(false);
+
+ const [tasks, setTasks] = useState(SEED_TASKS);
+ const [activeList, setActiveList] = useState('myday');
+ const [selectedId, setSelectedId] = useState('t1');
+ const [search, setSearch] = useState('');
+ const [showCompleted, setShowCompleted] = useState(true);
+ const [leavingIds, setLeavingIds] = useState([]);
+ const [enteringIds, setEnteringIds] = useState([]);
+ const [diffTaskId, setDiffTaskId] = useState(null);
+ const [worktreeTaskId, setWorktreeTaskId] = useState(null);
+
+ // Apply CSS tweaks
+ useEffect(() => {
+ const r = document.documentElement;
+ r.style.setProperty('--accent-h', tweaks.accentHue);
+ r.style.setProperty('--island-gap', tweaks.islandGap + 'px');
+ r.style.setProperty('--island-radius', tweaks.islandRadius + 'px');
+ r.style.setProperty('--grain-opacity', tweaks.grainOpacity);
+ r.style.setProperty('--sidebar-w', tweaks.sidebarWidth + 'px');
+ r.style.setProperty('--density', tweaks.density === 'comfy' ? 1 : 0.82);
+ }, [tweaks]);
+
+ // Tweaks host protocol
+ useEffect(() => {
+ const onMsg = (e) => {
+ const d = e.data;
+ if (!d || typeof d !== 'object') return;
+ if (d.type === '__activate_edit_mode') { setEditMode(true); setTweaksOpen(true); }
+ if (d.type === '__deactivate_edit_mode') { setEditMode(false); setTweaksOpen(false); }
+ };
+ window.addEventListener('message', onMsg);
+ try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch (e) {}
+ return () => window.removeEventListener('message', onMsg);
+ }, []);
+
+ // Counts per list
+ const counts = useMemo(() => {
+ const c = {};
+ c.myday = tasks.filter((t) => t.myDay && !t.done).length;
+ c.important = tasks.filter((t) => t.starred && !t.done).length;
+ c.planned = tasks.filter((t) => t.due && !t.done).length;
+ c.assigned = 0;
+ c.flagged = 0;
+ c.all = tasks.filter((t) => !t.done).length;
+ SEED_USER_LISTS.forEach((l) => {
+ c[l.id] = tasks.filter((t) => t.list === l.id && !t.done).length;
+ });
+ return c;
+ }, [tasks]);
+
+ // Filter tasks
+ const visibleTasks = useMemo(() => {
+ let ts = tasks;
+ if (activeList === 'myday') ts = ts.filter((t) => t.myDay);
+ else if (activeList === 'important') ts = ts.filter((t) => t.starred);
+ else if (activeList === 'planned') ts = ts.filter((t) => t.due);
+ else if (activeList === 'all') ts = ts;
+ else if (activeList === 'assigned' || activeList === 'flagged') ts = [];
+ else ts = ts.filter((t) => t.list === activeList);
+ if (search) {
+ const q = search.toLowerCase();
+ ts = ts.filter((t) => t.title.toLowerCase().includes(q) || (t.notes || '').toLowerCase().includes(q));
+ }
+ return ts;
+ }, [tasks, activeList, search]);
+
+ const selected = tasks.find((t) => t.id === selectedId);
+
+ // Actions
+ const toggleTask = (id) => {
+ setTasks((prev) => prev.map((t) =>
+ t.id === id ? { ...t, done: !t.done, completedAt: !t.done ? new Date().toISOString() : null } : t
+ ));
+ };
+ const starTask = (id) => {
+ setTasks((prev) => prev.map((t) => t.id === id ? { ...t, starred: !t.starred } : t));
+ };
+ const updateTask = (next) => {
+ setTasks((prev) => prev.map((t) => t.id === next.id ? next : t));
+ };
+ const deleteTask = (id) => {
+ setLeavingIds((l) => [...l, id]);
+ setTimeout(() => {
+ setTasks((prev) => prev.filter((t) => t.id !== id));
+ setLeavingIds((l) => l.filter((x) => x !== id));
+ if (selectedId === id) setSelectedId(null);
+ }, 280);
+ };
+ const addTask = (title) => {
+ const id = 't' + Date.now();
+ const newTask = {
+ id, title,
+ list: ['myday','important','planned','running','review','all'].includes(activeList) ? 'claudedo' : activeList,
+ myDay: true,
+ starred: false,
+ due: new Date().toISOString(),
+ notes: '',
+ tags: [],
+ subtasks: [],
+ created: new Date().toISOString(),
+ done: false,
+ agent: {
+ status: 'idle',
+ model: 'claude-sonnet-4.5',
+ worktree: `~/worktrees/${activeList}/new-task-${id.slice(1,6)}`,
+ branch: `agent/new-task-${id.slice(1,6)}`,
+ baseBranch: 'main',
+ commits: 0,
+ diff: { files: 0, additions: 0, deletions: 0 },
+ turns: 0,
+ tokens: 0,
+ log: [{ t: new Date().toISOString(), k: 'sys', m: 'Worktree ready. Agent not yet dispatched.' }],
+ },
+ };
+ setTasks((prev) => [newTask, ...prev]);
+ setEnteringIds((l) => [...l, id]);
+ setSelectedId(id);
+ setTimeout(() => setEnteringIds((l) => l.filter((x) => x !== id)), 300);
+ };
+
+ const agentAction = (id, action) => {
+ setTasks((prev) => prev.map((t) => {
+ if (t.id !== id || !t.agent) return t;
+ if (action === 'start') {
+ return { ...t, agent: { ...t.agent, status: 'running', startedAt: new Date().toISOString(),
+ log: [...(t.agent.log || []), { t: new Date().toISOString(), k: 'sys', m: 'Agent dispatched.' }] } };
+ }
+ if (action === 'stop') {
+ return { ...t, agent: { ...t.agent, status: 'review', finishedAt: new Date().toISOString(),
+ log: [...(t.agent.log || []), { t: new Date().toISOString(), k: 'sys', m: 'Stopped by operator.' }] } };
+ }
+ return t;
+ }));
+ };
+
+ const agentInput = (id, msg) => {
+ setTasks((prev) => prev.map((t) => {
+ if (t.id !== id || !t.agent) return t;
+ return { ...t, agent: { ...t.agent, log: [...(t.agent.log || []),
+ { t: new Date().toISOString(), k: 'msg', m: '[you] ' + msg }] } };
+ }));
+ };
+
+ return (
+
+
+
+
+
{ setActiveList(id); }}
+ counts={counts}
+ search={search}
+ setSearch={setSearch}
+ />
+
+
+ setDiffTaskId(id)}
+ onOpenWorktree={(id) => setWorktreeTaskId(id)}
+ />
+
+
+
+
+
+ {diffTaskId && (
+
t.id === diffTaskId)}
+ onClose={() => setDiffTaskId(null)}
+ />
+ )}
+ {worktreeTaskId && (
+ t.id === worktreeTaskId)}
+ onClose={() => setWorktreeTaskId(null)}
+ />
+ )}
+
+ {/* Tweaks: FAB (when edit mode is off) or panel (when toggled) */}
+ {editMode && (
+ setTweaksOpen(false)}
+ tweaks={tweaks}
+ setTweaks={setTweaks}
+ />
+ )}
+
+ );
+};
+
+// The inner window shouldn't render TitleBar twice — fix:
+// Actually we want ONE window with one titlebar. Remove the outer TitleBar.
+const AppFixed = () =>
;
+
+ReactDOM.createRoot(document.getElementById('root')).render(
);
diff --git a/docs/UI Rewrite/design_handoff_claudedo/data.jsx b/docs/UI Rewrite/design_handoff_claudedo/data.jsx
new file mode 100644
index 0000000..55c7727
--- /dev/null
+++ b/docs/UI Rewrite/design_handoff_claudedo/data.jsx
@@ -0,0 +1,228 @@
+// Seed data for ClaudeDo — Claude agent dispatcher
+const SEED_LISTS = [
+ { id: 'myday', kind: 'smart', icon: 'sun', name: 'My Day' },
+ { id: 'running', kind: 'smart', icon: 'pulse', name: 'Running' },
+ { id: 'important', kind: 'smart', icon: 'star', name: 'Important' },
+ { id: 'planned', kind: 'smart', icon: 'calendar', name: 'Planned' },
+ { id: 'review', kind: 'smart', icon: 'eye', name: 'Needs review' },
+ { id: 'all', kind: 'smart', icon: 'inbox', name: 'All tasks' },
+];
+
+const SEED_USER_LISTS = [
+ { id: 'claudedo', icon: 'folder', name: 'ClaudeDo', color: '#6b8e6b' },
+ { id: 'tuning-web', icon: 'folder', name: 'tuning-web', color: '#d4a574' },
+ { id: 'api-core', icon: 'folder', name: 'api-core', color: '#8b9d7a' },
+ { id: 'ops', icon: 'folder', name: 'ops', color: '#7a95a8' },
+];
+
+const now = new Date();
+const today = now;
+const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1);
+const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
+
+const mkISO = (mins) => new Date(Date.now() - mins * 60000).toISOString();
+
+// status: idle | queued | running | review | done | error
+const SEED_TASKS = [
+ {
+ id: 't1',
+ title: 'Refactor the auth middleware to use new session store',
+ list: 'api-core',
+ myDay: true, starred: true,
+ due: today.toISOString(),
+ notes: 'Swap the old Redis client for the new pool-aware wrapper. Keep the public API stable.',
+ tags: ['refactor', 'backend'],
+ subtasks: [
+ { id: 's1', title: 'Audit call sites', done: true },
+ { id: 's2', title: 'Swap client in middleware.ts', done: true },
+ { id: 's3', title: 'Update tests', done: false },
+ { id: 's4', title: 'Run full test suite', done: false },
+ ],
+ created: mkISO(120), done: false,
+ agent: {
+ status: 'running',
+ model: 'claude-sonnet-4.5',
+ worktree: '~/worktrees/api-core/auth-refactor',
+ branch: 'agent/auth-refactor',
+ baseBranch: 'main',
+ startedAt: mkISO(18),
+ commits: 3,
+ diff: { files: 7, additions: 142, deletions: 86 },
+ turns: 24,
+ tokens: 184200,
+ log: [
+ { t: mkISO(18), k: 'sys', m: 'Session started · worktree: api-core/auth-refactor' },
+ { t: mkISO(17), k: 'tool', m: 'read_file src/middleware/auth.ts' },
+ { t: mkISO(17), k: 'tool', m: 'grep "createSessionStore" src/' },
+ { t: mkISO(16), k: 'msg', m: 'Found 12 call sites across 4 modules. Starting with the middleware.' },
+ { t: mkISO(14), k: 'tool', m: 'edit_file src/middleware/auth.ts (+48 −22)' },
+ { t: mkISO(12), k: 'tool', m: 'edit_file src/lib/session/index.ts (+31 −14)' },
+ { t: mkISO(11), k: 'tool', m: 'run_shell "pnpm test auth"' },
+ { t: mkISO(10), k: 'stdout', m: ' ✓ auth/basic.test.ts (8)' },
+ { t: mkISO(10), k: 'stdout', m: ' ✓ auth/session.test.ts (14)' },
+ { t: mkISO(10), k: 'stdout', m: ' ✗ auth/expiry.test.ts (2 failed)' },
+ { t: mkISO(9), k: 'msg', m: 'Two expiry tests failing — investigating the TTL calculation.' },
+ { t: mkISO(6), k: 'tool', m: 'edit_file src/lib/session/ttl.ts (+12 −4)' },
+ { t: mkISO(5), k: 'tool', m: 'run_shell "pnpm test auth/expiry"' },
+ { t: mkISO(4), k: 'stdout', m: ' ✓ auth/expiry.test.ts (6)' },
+ { t: mkISO(3), k: 'msg', m: 'Expiry tests passing. Now running full suite…' },
+ { t: mkISO(1), k: 'tool', m: 'run_shell "pnpm test"' },
+ { t: mkISO(0.2), k: 'stdout', m: ' Running 284 tests across 41 files…' },
+ ],
+ },
+ },
+ {
+ id: 't2',
+ title: 'Add dark mode toggle to settings page',
+ list: 'tuning-web',
+ myDay: true, starred: false,
+ due: today.toISOString(),
+ notes: 'Match the palette from the design system. Persist via localStorage.',
+ tags: ['ui'],
+ subtasks: [],
+ created: mkISO(90), done: false,
+ agent: {
+ status: 'review',
+ model: 'claude-sonnet-4.5',
+ worktree: '~/worktrees/tuning-web/dark-mode',
+ branch: 'agent/dark-mode',
+ baseBranch: 'main',
+ startedAt: mkISO(45),
+ finishedAt: mkISO(8),
+ commits: 2,
+ diff: { files: 4, additions: 68, deletions: 12 },
+ turns: 14,
+ tokens: 92400,
+ log: [
+ { t: mkISO(45), k: 'sys', m: 'Session started · worktree: tuning-web/dark-mode' },
+ { t: mkISO(44), k: 'tool', m: 'read_file src/pages/settings.tsx' },
+ { t: mkISO(42), k: 'tool', m: 'read_file src/theme/tokens.css' },
+ { t: mkISO(38), k: 'tool', m: 'edit_file src/pages/settings.tsx (+32 −2)' },
+ { t: mkISO(30), k: 'tool', m: 'edit_file src/hooks/useTheme.ts (+24 −0)' },
+ { t: mkISO(22), k: 'tool', m: 'edit_file src/theme/tokens.css (+10 −8)' },
+ { t: mkISO(14), k: 'tool', m: 'run_shell "pnpm build"' },
+ { t: mkISO(12), k: 'stdout', m: ' ✓ Built in 4.2s' },
+ { t: mkISO(10), k: 'tool', m: 'run_shell "pnpm test"' },
+ { t: mkISO(9), k: 'stdout', m: ' ✓ 182 tests passed' },
+ { t: mkISO(8), k: 'done', m: 'Ready for review — 2 commits on agent/dark-mode' },
+ ],
+ },
+ },
+ {
+ id: 't3',
+ title: 'Investigate flaky checkout test',
+ list: 'tuning-web',
+ myDay: true, starred: false,
+ due: today.toISOString(),
+ notes: 'Fails ~1 in 8 runs on CI. Probably a race in the cart hydration.',
+ tags: ['bug', 'tests'],
+ subtasks: [],
+ created: mkISO(200), done: false,
+ agent: {
+ status: 'error',
+ model: 'claude-sonnet-4.5',
+ worktree: '~/worktrees/tuning-web/flaky-checkout',
+ branch: 'agent/flaky-checkout',
+ baseBranch: 'main',
+ startedAt: mkISO(55),
+ finishedAt: mkISO(40),
+ commits: 0,
+ diff: { files: 0, additions: 0, deletions: 0 },
+ turns: 6,
+ tokens: 28100,
+ log: [
+ { t: mkISO(55), k: 'sys', m: 'Session started · worktree: tuning-web/flaky-checkout' },
+ { t: mkISO(54), k: 'tool', m: 'run_shell "pnpm test checkout --repeat 20"' },
+ { t: mkISO(50), k: 'stdout', m: ' runs: 20 · passes: 18 · failures: 2' },
+ { t: mkISO(45), k: 'tool', m: 'read_file src/features/checkout/cart.tsx' },
+ { t: mkISO(42), k: 'tool', m: 'run_shell "pnpm tsc --noEmit"' },
+ { t: mkISO(41), k: 'stderr', m: ' src/features/checkout/cart.tsx(142,7): TS2339: ...' },
+ { t: mkISO(40), k: 'error', m: 'Blocked: cannot reproduce the race locally. Paused for operator input.' },
+ ],
+ },
+ },
+ {
+ id: 't4',
+ title: 'Write migration guide for v3 API',
+ list: 'api-core',
+ myDay: true, starred: false,
+ due: tomorrow.toISOString(),
+ notes: '',
+ tags: ['docs'],
+ subtasks: [],
+ created: mkISO(20), done: false,
+ agent: {
+ status: 'idle',
+ model: 'claude-sonnet-4.5',
+ worktree: '~/worktrees/api-core/v3-migration-guide',
+ branch: 'agent/v3-migration-guide',
+ baseBranch: 'main',
+ commits: 0,
+ diff: { files: 0, additions: 0, deletions: 0 },
+ turns: 0,
+ tokens: 0,
+ log: [
+ { t: mkISO(20), k: 'sys', m: 'Worktree ready. Agent not yet dispatched.' },
+ ],
+ },
+ },
+ {
+ id: 't5',
+ title: 'Upgrade Postgres client to v16',
+ list: 'ops',
+ myDay: true, starred: false,
+ due: yesterday.toISOString(),
+ notes: 'Coordinate with infra on the rolling restart window.',
+ tags: ['infra'],
+ subtasks: [],
+ created: mkISO(1440), done: false,
+ agent: {
+ status: 'queued',
+ model: 'claude-sonnet-4.5',
+ worktree: '~/worktrees/ops/pg-16',
+ branch: 'agent/pg-16',
+ baseBranch: 'main',
+ commits: 0,
+ diff: { files: 0, additions: 0, deletions: 0 },
+ turns: 0,
+ tokens: 0,
+ log: [
+ { t: mkISO(30), k: 'sys', m: 'Queued · waiting for api-core/auth-refactor to complete.' },
+ ],
+ },
+ },
+ {
+ id: 't6',
+ title: 'Fix favicon serving on preview domains',
+ list: 'tuning-web',
+ myDay: true, starred: false,
+ due: null, notes: '', tags: ['bug'],
+ subtasks: [],
+ created: mkISO(300),
+ done: true, completedAt: mkISO(60),
+ agent: {
+ status: 'done',
+ model: 'claude-sonnet-4.5',
+ worktree: '~/worktrees/tuning-web/favicon',
+ branch: 'agent/favicon',
+ baseBranch: 'main',
+ startedAt: mkISO(90),
+ finishedAt: mkISO(60),
+ commits: 1,
+ diff: { files: 2, additions: 14, deletions: 3 },
+ turns: 8,
+ tokens: 41800,
+ mergedInto: 'main',
+ log: [
+ { t: mkISO(90), k: 'sys', m: 'Session started' },
+ { t: mkISO(75), k: 'tool', m: 'edit_file nginx/preview.conf (+8 −3)' },
+ { t: mkISO(70), k: 'tool', m: 'edit_file public/favicon.ico (+6 −0)' },
+ { t: mkISO(65), k: 'done', m: 'Merged into main · closed PR #482' },
+ ],
+ },
+ },
+];
+
+window.SEED_LISTS = SEED_LISTS;
+window.SEED_USER_LISTS = SEED_USER_LISTS;
+window.SEED_TASKS = SEED_TASKS;
diff --git a/docs/UI Rewrite/design_handoff_claudedo/icons.jsx b/docs/UI Rewrite/design_handoff_claudedo/icons.jsx
new file mode 100644
index 0000000..90a3ecb
--- /dev/null
+++ b/docs/UI Rewrite/design_handoff_claudedo/icons.jsx
@@ -0,0 +1,123 @@
+// Icons for ClaudeDo (line icons, 1.5px, lucide-ish but original)
+const Icon = ({ name, size = 16, stroke = 'currentColor' }) => {
+ const common = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke, strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' };
+ switch (name) {
+ case 'sun': return (
+
+ );
+ case 'star': return (
+
+ );
+ case 'star-filled': return (
+
+ );
+ case 'calendar': return (
+
+ );
+ case 'user': return (
+
+ );
+ case 'flag': return (
+
+ );
+ case 'inbox': return (
+
+ );
+ case 'folder': return (
+
+ );
+ case 'search': return (
+
+ );
+ case 'plus': return (
+
+ );
+ case 'bell': return (
+
+ );
+ case 'repeat': return (
+
+ );
+ case 'note': return (
+
+ );
+ case 'tag': return (
+
+ );
+ case 'more': return (
+
+ );
+ case 'sort': return (
+
+ );
+ case 'eye': return (
+
+ );
+ case 'grip': return (
+
+ );
+ case 'trash': return (
+
+ );
+ case 'x': return (
+
+ );
+ case 'close': return (
+
+ );
+ case 'min': return (
+
+ );
+ case 'max': return (
+
+ );
+ case 'sliders': return (
+
+ );
+ case 'check': return (
+
+ );
+ case 'windows': return (
+
+ );
+ case 'pulse': return (
+
+ );
+ case 'branch': return (
+
+ );
+ case 'terminal': return (
+
+ );
+ case 'diff': return (
+
+ );
+ case 'play': return (
+
+ );
+ case 'pause': return (
+
+ );
+ case 'stop': return (
+
+ );
+ case 'folder-open': return (
+
+ );
+ case 'external': return (
+
+ );
+ case 'copy': return (
+
+ );
+ case 'send': return (
+
+ );
+ case 'cpu': return (
+
+ );
+ default: return null;
+ }
+};
+
+window.Icon = Icon;
diff --git a/docs/UI Rewrite/design_handoff_claudedo/islands.jsx b/docs/UI Rewrite/design_handoff_claudedo/islands.jsx
new file mode 100644
index 0000000..8414ffc
--- /dev/null
+++ b/docs/UI Rewrite/design_handoff_claudedo/islands.jsx
@@ -0,0 +1,650 @@
+// The three islands: ListsIsland, TasksIsland, DetailsIsland
+const { useState, useEffect, useRef, useMemo } = React;
+
+// ---------- Helpers ----------
+const fmtDate = (iso) => {
+ if (!iso) return null;
+ const d = new Date(iso);
+ const now = new Date();
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const target = new Date(d.getFullYear(), d.getMonth(), d.getDate());
+ const diff = Math.round((target - today) / 86400000);
+ if (diff === 0) return 'Today';
+ if (diff === 1) return 'Tomorrow';
+ if (diff === -1) return 'Yesterday';
+ if (diff < 0) return `${Math.abs(diff)}d overdue`;
+ if (diff < 7) return d.toLocaleDateString(undefined, { weekday: 'short' });
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
+};
+const isToday = (iso) => {
+ if (!iso) return false;
+ const d = new Date(iso); const n = new Date();
+ return d.getFullYear() === n.getFullYear() && d.getMonth() === n.getMonth() && d.getDate() === n.getDate();
+};
+const isOverdue = (iso) => {
+ if (!iso) return false;
+ const d = new Date(iso); const n = new Date();
+ return d < new Date(n.getFullYear(), n.getMonth(), n.getDate());
+};
+
+const STATUS_LABEL = {
+ idle: 'Idle', queued: 'Queued', running: 'Running',
+ review: 'Review', done: 'Done', error: 'Error',
+};
+
+const relTime = (iso) => {
+ if (!iso) return '';
+ const diff = Math.max(0, Date.now() - new Date(iso).getTime());
+ const s = Math.floor(diff / 1000);
+ if (s < 60) return s + 's ago';
+ const m = Math.floor(s / 60);
+ if (m < 60) return m + 'm ago';
+ const h = Math.floor(m / 60);
+ if (h < 24) return h + 'h ago';
+ return Math.floor(h / 24) + 'd ago';
+};
+
+const logTime = (iso) => {
+ const d = new Date(iso);
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
+};
+
+// ---------- Checkbox ----------
+const Checkbox = ({ done, onToggle, size }) => (
+
{ e.stopPropagation(); onToggle(); }}
+ role="checkbox"
+ aria-checked={done}
+ >
+
+
+);
+
+// ---------- Lists Island ----------
+const ListsIsland = ({ activeList, setActiveList, counts, search, setSearch }) => {
+ return (
+
+
+
+
+
+ setSearch(e.target.value)}
+ />
+ ⌘K
+
+
+
+
Smart lists
+ {SEED_LISTS.map((l) => (
+
setActiveList(l.id)}
+ >
+
+
{l.name}
+
{counts[l.id] ?? ''}
+
+ ))}
+
+
My lists
+ {SEED_USER_LISTS.map((l) => (
+
setActiveList(l.id)}
+ >
+
+
{l.name}
+
{counts[l.id] ?? ''}
+
+ ))}
+
+
+
+
+
+
AK
+
+
Aoife Kelly
+
rider.island / local
+
+
+
+
+ );
+};
+
+// ---------- Tasks Island ----------
+const TaskRow = ({ task, selected, onSelect, onToggle, onStar, leaving, entering }) => {
+ const [starPulse, setStarPulse] = useState(false);
+ const handleStar = (e) => {
+ e.stopPropagation();
+ setStarPulse(true);
+ setTimeout(() => setStarPulse(false), 400);
+ onStar();
+ };
+ const list = SEED_USER_LISTS.find((l) => l.id === task.list);
+ const overdue = isOverdue(task.due) && !task.done;
+ const today = isToday(task.due);
+
+ return (
+
+
+
+
{task.title}
+
+ {task.agent && (
+
+
+ {STATUS_LABEL[task.agent.status]}
+
+ )}
+ {list && (
+
+
+ {list.name}
+
+ )}
+ {task.agent?.branch && (
+
+ {task.agent.branch.replace('agent/', '')}
+
+ )}
+ {task.agent?.diff && task.agent.diff.files > 0 && (
+
+
+ +{task.agent.diff.additions}
+ −{task.agent.diff.deletions}
+
+
+ )}
+ {task.due && !task.agent && (
+
+ {fmtDate(task.due)}
+
+ )}
+ {task.subtasks && task.subtasks.length > 0 && (
+
+ {task.subtasks.filter((s) => s.done).length}/{task.subtasks.length} steps
+
+ )}
+ {task.tags && task.tags.map((t) => {t})}
+
+ {task.agent && task.agent.status === 'running' && task.agent.log && task.agent.log.length > 0 && (() => {
+ const last = task.agent.log[task.agent.log.length - 1];
+ return (
+
+ ›
+ {last.m}
+
+
+ );
+ })()}
+
+
+
+ );
+};
+
+const TasksIsland = ({
+ tasks, selectedId, setSelected,
+ onToggle, onStar, onAdd,
+ leavingIds, enteringIds,
+ activeList, showCompleted, setShowCompleted,
+}) => {
+ const [newTitle, setNewTitle] = useState('');
+ const inputRef = useRef(null);
+
+ const now = new Date();
+ const dateLine = now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
+
+ const activeTasks = tasks.filter((t) => !t.done);
+ const doneTasks = tasks.filter((t) => t.done);
+ const overdueTasks = activeTasks.filter((t) => isOverdue(t.due));
+ const todayTasks = activeTasks.filter((t) => !isOverdue(t.due));
+
+ const listMeta = SEED_LISTS.find((l) => l.id === activeList) || SEED_USER_LISTS.find((l) => l.id === activeList);
+ const title = activeList === 'myday' ? 'My Day' : (listMeta?.name || 'Tasks');
+ const eyebrow = activeList === 'myday' ? dateLine : `${activeTasks.length} open · ${doneTasks.length} done`;
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (newTitle.trim()) {
+ onAdd(newTitle.trim());
+ setNewTitle('');
+ }
+ };
+
+ return (
+
+
+
+
+
{activeList === 'myday' ? 'My Day' : 'List'}
+
{title}
+
+ {activeList === 'myday' ? dateLine : eyebrow}
+ ·
+ {activeTasks.length} open
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {overdueTasks.length > 0 && (
+ <>
+
Overdue
+ {overdueTasks.map((t) => (
+
setSelected(t.id)}
+ onToggle={() => onToggle(t.id)}
+ onStar={() => onStar(t.id)}
+ leaving={leavingIds.includes(t.id)}
+ entering={enteringIds.includes(t.id)}
+ />
+ ))}
+ >
+ )}
+
+ {todayTasks.length > 0 && (
+ <>
+ {overdueTasks.length > 0 && Tasks
}
+ {todayTasks.map((t) => (
+ setSelected(t.id)}
+ onToggle={() => onToggle(t.id)}
+ onStar={() => onStar(t.id)}
+ leaving={leavingIds.includes(t.id)}
+ entering={enteringIds.includes(t.id)}
+ />
+ ))}
+ >
+ )}
+
+ {activeTasks.length === 0 && (
+
+
+ All clear
+
+
The harbor is calm. Add a task above.
+
+ )}
+
+ {showCompleted && doneTasks.length > 0 && (
+ <>
+ Completed · {doneTasks.length}
+ {doneTasks.map((t) => (
+ setSelected(t.id)}
+ onToggle={() => onToggle(t.id)}
+ onStar={() => onStar(t.id)}
+ leaving={leavingIds.includes(t.id)}
+ entering={enteringIds.includes(t.id)}
+ />
+ ))}
+ >
+ )}
+
+
+ );
+};
+
+// ---------- Worktree + Terminal sub-components ----------
+const WorktreeCard = ({ agent, onOpenDiff, onOpenWorktree }) => {
+ if (!agent) return null;
+ return (
+
+
+ Worktree
+ {agent.worktree}
+
+
+
+ Branch
+
+ {agent.branch}
+ ← {agent.baseBranch}
+
+
+
+ Diff
+
+ {agent.diff.files > 0 ? (
+
+ {agent.diff.files} files
+ +{agent.diff.additions}
+ −{agent.diff.deletions}
+
+ {Array.from({ length: 5 }).map((_, i) => {
+ const total = agent.diff.additions + agent.diff.deletions || 1;
+ const addShare = Math.round((agent.diff.additions / total) * 5);
+ return ;
+ })}
+
+
+ ) : No changes yet}
+
+
+ {agent.commits > 0 && (
+
+ Commits
+ {agent.commits} on branch
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+const SessionTerminal = ({ agent, onInput }) => {
+ const bodyRef = useRef(null);
+ const [draft, setDraft] = useState('');
+
+ useEffect(() => {
+ if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
+ }, [agent?.log?.length]);
+
+ if (!agent) return null;
+ const running = agent.status === 'running';
+ const statusLabel = running ? 'LIVE' : STATUS_LABEL[agent.status];
+
+ return (
+
+
+
+
claude-session · {agent.branch}
+ {running
+ ?
LIVE
+ :
{statusLabel}}
+
+
+ {(agent.log || []).map((l, i) => (
+
+ {logTime(l.t)}
+ {l.k === 'msg' ? 'claude' : l.k === 'tool' ? 'tool' : l.k === 'sys' ? 'sys' : l.k === 'stdout' ? 'out' : l.k === 'stderr' ? 'err' : l.k}
+ {l.m}
+
+ ))}
+ {running && (
+
+ {logTime(new Date().toISOString())}
+ claude
+
+
+ )}
+
+
+ ›
+ setDraft(e.target.value)}
+ onKeyDown={(e) => { if (e.key === 'Enter' && draft.trim()) { onInput(draft); setDraft(''); } }}
+ disabled={!running}
+ style={{ flex: 1, fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--text)' }}
+ />
+
+
+
+ );
+};
+
+// ---------- Details Island ----------
+const DetailsIsland = ({ task, onUpdate, onDelete, onToggle, onStar, onAgentAction, onOpenDiff, onOpenWorktree, onAgentInput }) => {
+ if (!task) {
+ return (
+
+
+
+
+
No task selected
+
Pick a task from the middle
to see its details here.
+
+
+
+ );
+ }
+
+ const list = SEED_USER_LISTS.find((l) => l.id === task.list);
+ const created = task.created ? new Date(task.created).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '—';
+ const due = task.due ? new Date(task.due).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }) : 'None';
+
+ const toggleSub = (sid) => {
+ const next = task.subtasks.map((s) => s.id === sid ? { ...s, done: !s.done } : s);
+ onUpdate({ ...task, subtasks: next });
+ };
+ const addSub = (title) => {
+ if (!title.trim()) return;
+ const next = [...(task.subtasks || []), { id: 's' + Date.now(), title: title.trim(), done: false }];
+ onUpdate({ ...task, subtasks: next });
+ };
+ const [subDraft, setSubDraft] = useState('');
+
+ return (
+
+
+
+
+ Logbook
+ #{task.id}
+
+
+ {task.agent ? 'Agent task' : 'Task details'}
+
+
+
+ {task.agent && (
+
+
+
+ {STATUS_LABEL[task.agent.status]}
+
+
+ {task.agent.model}
+ ·
+ {task.agent.turns} turns
+ ·
+ {(task.agent.tokens / 1000).toFixed(1)}k tok
+ {task.agent.startedAt && <>·{relTime(task.agent.startedAt)}>}
+
+ {task.agent.status === 'running' ? (
+
+ ) : task.agent.status === 'idle' || task.agent.status === 'error' || task.agent.status === 'queued' ? (
+
+ ) : null}
+
+ )}
+
+
+
+ onToggle(task.id)} />
+
+
+ {task.agent && (
+
+
Worktree
+
onOpenDiff(task.id)}
+ onOpenWorktree={() => onOpenWorktree(task.id)}
+ />
+
+ )}
+
+ {task.agent && (
+
+
+ Session output
+
+ {(task.agent.log || []).length} lines
+
+
+
onAgentInput(task.id, msg)}
+ />
+
+ )}
+
+ {(task.subtasks || []).length > 0 && (
+
+
Steps · {task.subtasks.filter(s => s.done).length}/{task.subtasks.length}
+ {task.subtasks.map((s) => (
+
+
toggleSub(s.id)} />
+ {s.title}
+
+ ))}
+
+
+
setSubDraft(e.target.value)}
+ onKeyDown={(e) => { if (e.key === 'Enter') { addSub(subDraft); setSubDraft(''); } }}
+ />
+
+
+ )}
+
+
+
+ List
+
+ {list ? list.name : '—'}
+
+
+
+ Due
+ {due}
+
+ {!task.agent && (
+
+ Reminder
+ {task.reminder || 'None'}
+
+ )}
+
+ Important
+ {task.starred ? 'Starred' : 'No'}
+
+
+
+
+
+ {(task.tags || []).length > 0 && (
+
+
Tags
+
+ {task.tags.map((t) => {t})}
+ + add
+
+
+ )}
+
+
+
+
+
+ Created {created}
+
+
+
+
+ );
+};
+
+window.ListsIsland = ListsIsland;
+window.TasksIsland = TasksIsland;
+window.DetailsIsland = DetailsIsland;
diff --git a/docs/UI Rewrite/design_handoff_claudedo/modals.jsx b/docs/UI Rewrite/design_handoff_claudedo/modals.jsx
new file mode 100644
index 0000000..31a086a
--- /dev/null
+++ b/docs/UI Rewrite/design_handoff_claudedo/modals.jsx
@@ -0,0 +1,201 @@
+// Diff modal + Worktree modal
+const { useState: useStateM, useEffect: useEffectM } = window.React;
+
+// Fake diff hunks per task
+const DIFF_HUNKS = {
+ t1: [
+ { file: 'src/middleware/auth.ts', adds: 48, dels: 22, hunks: [
+ { header: '@@ -12,7 +12,9 @@ export function authMiddleware(', lines: [
+ { k: 'ctx', n1: 12, n2: 12, t: ' const session = await getSession(req);' },
+ { k: 'del', n1: 13, n2: null, t: ' if (!session) return unauthorized();' },
+ { k: 'del', n1: 14, n2: null, t: ' const user = await lookupUser(session.userId);' },
+ { k: 'add', n1: null, n2: 13, t: ' if (!session || session.expired) {' },
+ { k: 'add', n1: null, n2: 14, t: ' return unauthorized("expired_or_missing");' },
+ { k: 'add', n1: null, n2: 15, t: ' }' },
+ { k: 'add', n1: null, n2: 16, t: ' const user = await pool.withConnection(c => lookupUser(c, session.userId));' },
+ { k: 'ctx', n1: 15, n2: 17, t: ' req.user = user;' },
+ { k: 'ctx', n1: 16, n2: 18, t: ' return next();' },
+ ]},
+ { header: '@@ -42,4 +44,6 @@ export function guard(', lines: [
+ { k: 'ctx', n1: 42, n2: 44, t: ' return async (req, res, next) => {' },
+ { k: 'del', n1: 43, n2: null, t: ' const s = await redis.get(req.cookies.sid);' },
+ { k: 'add', n1: null, n2: 45, t: ' const s = await store.get(req.cookies.sid);' },
+ { k: 'add', n1: null, n2: 46, t: ' if (s) store.touch(req.cookies.sid);' },
+ { k: 'ctx', n1: 44, n2: 47, t: ' next();' },
+ ]},
+ ]},
+ { file: 'src/lib/session/index.ts', adds: 31, dels: 14, hunks: [
+ { header: '@@ -1,8 +1,14 @@', lines: [
+ { k: 'del', n1: 1, n2: null, t: 'import { createClient } from "redis";' },
+ { k: 'add', n1: null, n2: 1, t: 'import { SessionStore } from "./store";' },
+ { k: 'add', n1: null, n2: 2, t: 'import { Pool } from "./pool";' },
+ { k: 'ctx', n1: 2, n2: 3, t: '' },
+ { k: 'del', n1: 3, n2: null, t: 'export const redis = createClient({ url: process.env.REDIS_URL });' },
+ { k: 'add', n1: null, n2: 4, t: 'export const pool = new Pool({ size: 16 });' },
+ { k: 'add', n1: null, n2: 5, t: 'export const store = new SessionStore(pool);' },
+ ]},
+ ]},
+ { file: 'src/lib/session/ttl.ts', adds: 12, dels: 4, hunks: [] },
+ { file: 'src/lib/session/store.ts', adds: 38, dels: 0, hunks: [] },
+ ],
+ t2: [
+ { file: 'src/pages/settings.tsx', adds: 32, dels: 2, hunks: [
+ { header: '@@ -4,6 +4,8 @@ import { Section } from "../ui";', lines: [
+ { k: 'ctx', n1: 4, n2: 4, t: 'import { useTheme } from "../hooks/useTheme";' },
+ { k: 'add', n1: null, n2: 5, t: 'import { ThemeToggle } from "../ui/ThemeToggle";' },
+ { k: 'ctx', n1: 5, n2: 6, t: '' },
+ { k: 'ctx', n1: 6, n2: 7, t: 'export default function Settings() {' },
+ { k: 'add', n1: null, n2: 8, t: ' const [theme, setTheme] = useTheme();' },
+ ]},
+ ]},
+ { file: 'src/hooks/useTheme.ts', adds: 24, dels: 0, hunks: [] },
+ { file: 'src/theme/tokens.css', adds: 10, dels: 8, hunks: [] },
+ { file: 'src/ui/ThemeToggle.tsx', adds: 26, dels: 2, hunks: [] },
+ ],
+};
+
+const DiffModal = ({ task, onClose }) => {
+ useEffectM(() => {
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, [onClose]);
+ const files = DIFF_HUNKS[task.id] || [
+ { file: 'No diff available yet', adds: 0, dels: 0, hunks: [] }
+ ];
+ const [activeFile, setActiveFile] = useStateM(0);
+ const current = files[activeFile];
+
+ return (
+
+
e.stopPropagation()}>
+
+
+
+
+
Diff · {task.agent.branch}
+
+ {task.agent.worktree} · {files.length} files ·
+ +{task.agent.diff.additions}
+ −{task.agent.diff.deletions}
+
+
+
+
+
+
+
+
+
+
+
+ {files.map((f, i) => (
+
setActiveFile(i)}>
+
{f.file}
+
+ +{f.adds}
+ −{f.dels}
+
+
+ ))}
+
+
+
+
+ {current.file}
+
+ +{current.adds}
+ −{current.dels}
+
+
+ {current.hunks.length === 0 ? (
+
+ Select a hunk — no detail preview available for this file.
+
+ ) : current.hunks.map((h, hi) => (
+
+
{h.header}
+ {h.lines.map((ln, li) => (
+
+ {ln.n1 ?? ''}
+ {ln.n2 ?? ''}
+ {ln.k === 'add' ? '+' : ln.k === 'del' ? '−' : ' '}
+ {ln.t}
+
+ ))}
+
+ ))}
+
+
+
+
+ );
+};
+
+const WorktreeModal = ({ task, onClose }) => {
+ useEffectM(() => {
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, [onClose]);
+
+ const fakeTree = [
+ { kind: 'dir', path: 'src', children: [
+ { kind: 'dir', path: 'middleware', children: [
+ { kind: 'file', path: 'auth.ts', mod: true },
+ ]},
+ { kind: 'dir', path: 'lib/session', children: [
+ { kind: 'file', path: 'index.ts', mod: true },
+ { kind: 'file', path: 'ttl.ts', mod: true },
+ { kind: 'file', path: 'store.ts', added: true },
+ ]},
+ ]},
+ { kind: 'file', path: 'package.json' },
+ { kind: 'file', path: 'README.md' },
+ ];
+ const render = (nodes, depth = 0) => nodes.map((n) => (
+ n.kind === 'dir' ? (
+
+
+ {n.path}
+
+ {render(n.children, depth + 1)}
+
+ ) : (
+
+
+ {n.path}
+ {n.mod && M}
+ {n.added && A}
+
+ )
+ ));
+
+ return (
+
+
e.stopPropagation()}>
+
+
+
{task.agent.worktree}
+
{task.agent.branch} ← {task.agent.baseBranch}
+
+
+
+
+
+
+
+
+ Filesystem preview — modified files marked M, additions A
+
+
+ {render(fakeTree)}
+
+
+
+
+ );
+};
+
+window.DiffModal = DiffModal;
+window.WorktreeModal = WorktreeModal;
diff --git a/docs/UI Rewrite/design_handoff_claudedo/styles.css b/docs/UI Rewrite/design_handoff_claudedo/styles.css
new file mode 100644
index 0000000..42e31cb
--- /dev/null
+++ b/docs/UI Rewrite/design_handoff_claudedo/styles.css
@@ -0,0 +1,1383 @@
+/* ClaudeDo — Rider Island theme */
+/* Floating islands, logbook monospace, tactile dark UI */
+
+:root {
+ /* Base palette */
+ --void: #0a0e0c;
+ --deep: #0d1311;
+ --surface: #161d1a;
+ --surface-2: #1c2422;
+ --surface-3: #222b28;
+ --line: #2a3330;
+ --line-bright: #3a4542;
+
+ --text: #e4ebe4;
+ --text-dim: #9aa8a0;
+ --text-mute: #6b7973;
+ --text-faint: #4a5550;
+
+ /* Accents (moss / sage / peat) */
+ --moss: #4a6b4a;
+ --moss-bright: #6b8e6b;
+ --sage: #8b9d7a;
+ --peat: #d4a574;
+ --peat-soft: #b88d5e;
+ --blood: #c87060;
+
+ /* Tweakables (overridden by Tweaks panel) */
+ --accent-h: 88; /* 88 moss, 40 peat, 180 sea */
+ --island-gap: 14px;
+ --island-radius: 14px;
+ --grain-opacity: 0.035;
+ --density: 1; /* 1 comfy, 0.85 compact */
+ --sidebar-w: 260px;
+
+ /* Derived */
+ --accent: oklch(58% 0.08 var(--accent-h));
+ --accent-dim: oklch(48% 0.07 var(--accent-h));
+ --accent-soft: oklch(32% 0.05 var(--accent-h));
+ --accent-glow: oklch(65% 0.12 var(--accent-h) / 0.22);
+
+ /* Fonts */
+ --mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, Menlo, monospace;
+ --sans: 'Inter Tight', 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
+
+ color-scheme: dark;
+}
+
+* { box-sizing: border-box; }
+
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ background: #000;
+ color: var(--text);
+ font-family: var(--sans);
+ font-feature-settings: 'ss01', 'cv11';
+ -webkit-font-smoothing: antialiased;
+ overflow: hidden;
+}
+
+button { font-family: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
+input, textarea { font-family: inherit; color: inherit; background: none; border: 0; outline: 0; }
+input::placeholder, textarea::placeholder { color: var(--text-faint); }
+
+/* ==================== Windows desktop wallpaper ==================== */
+.desktop {
+ position: fixed;
+ inset: 0;
+ background:
+ radial-gradient(ellipse 120% 80% at 50% 110%, oklch(20% 0.02 var(--accent-h)) 0%, transparent 55%),
+ radial-gradient(ellipse 80% 60% at 20% 0%, oklch(15% 0.015 var(--accent-h)) 0%, transparent 50%),
+ linear-gradient(180deg, #05070a 0%, #0a0d10 50%, #060a08 100%);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.desktop::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background-image:
+ radial-gradient(circle at 1px 1px, rgba(255,255,255,0.03) 1px, transparent 0);
+ background-size: 3px 3px;
+ opacity: var(--grain-opacity);
+ pointer-events: none;
+}
+
+/* Taskbar (Windows 11 style, centered) */
+.taskbar {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 44px;
+ background: rgba(10, 14, 12, 0.72);
+ backdrop-filter: blur(32px) saturate(1.2);
+ -webkit-backdrop-filter: blur(32px) saturate(1.2);
+ border-top: 1px solid rgba(255,255,255,0.06);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ z-index: 10;
+}
+
+.taskbar-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 6px;
+ display: grid;
+ place-items: center;
+ color: var(--text-dim);
+ position: relative;
+ transition: background 0.15s;
+}
+.taskbar-icon:hover { background: rgba(255,255,255,0.06); }
+.taskbar-icon.active { background: rgba(255,255,255,0.04); }
+.taskbar-icon.active::after {
+ content: '';
+ position: absolute;
+ bottom: 2px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 14px;
+ height: 2px;
+ border-radius: 1px;
+ background: var(--accent);
+}
+
+.taskbar-clock {
+ position: absolute;
+ right: 14px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--text-dim);
+ text-align: right;
+ line-height: 1.25;
+}
+
+/* ==================== App window ==================== */
+.window {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, calc(-50% - 22px));
+ width: min(1320px, calc(100vw - 32px));
+ height: min(820px, calc(100vh - 76px));
+ min-width: 880px;
+ background: linear-gradient(180deg, #0b100e 0%, #080c0a 100%);
+ border-radius: 10px;
+ border: 1px solid rgba(255,255,255,0.06);
+ box-shadow:
+ 0 0 0 1px rgba(0,0,0,0.4),
+ 0 30px 80px rgba(0,0,0,0.6),
+ 0 60px 120px rgba(0,0,0,0.5);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+/* Title bar */
+.titlebar {
+ height: 32px;
+ display: flex;
+ align-items: center;
+ padding-left: 12px;
+ border-bottom: 1px solid rgba(255,255,255,0.04);
+ user-select: none;
+ -webkit-app-region: drag;
+ flex-shrink: 0;
+}
+.titlebar-title {
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--text-mute);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+.titlebar-title .bullet { color: var(--accent); margin: 0 8px; }
+
+.titlebar-controls {
+ display: flex;
+ margin-left: auto;
+ height: 100%;
+}
+.titlebar-btn {
+ width: 46px;
+ height: 100%;
+ display: grid;
+ place-items: center;
+ color: var(--text-dim);
+ transition: background 0.1s;
+}
+.titlebar-btn:hover { background: rgba(255,255,255,0.06); }
+.titlebar-btn.close:hover { background: #c42b1c; color: #fff; }
+
+/* Window body = the "sea" between islands */
+.window-body {
+ flex: 1;
+ display: grid;
+ grid-template-columns: minmax(200px, var(--sidebar-w)) minmax(340px, 1fr) minmax(260px, 320px);
+ gap: var(--island-gap);
+ padding: var(--island-gap);
+ min-height: 0;
+ background:
+ radial-gradient(ellipse 60% 50% at 30% 20%, oklch(14% 0.012 var(--accent-h)) 0%, transparent 60%),
+ radial-gradient(ellipse 50% 40% at 80% 90%, oklch(13% 0.01 var(--accent-h)) 0%, transparent 60%),
+ #070a09;
+ position: relative;
+}
+
+.window-body::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background-image:
+ radial-gradient(circle at 1px 1px, rgba(255,255,255,0.025) 1px, transparent 0);
+ background-size: 3px 3px;
+ opacity: var(--grain-opacity);
+ pointer-events: none;
+}
+
+/* ==================== Island ==================== */
+.island {
+ background: linear-gradient(180deg, var(--surface) 0%, #131917 100%);
+ border-radius: var(--island-radius);
+ border: 1px solid rgba(255,255,255,0.05);
+ box-shadow:
+ 0 1px 0 rgba(255,255,255,0.03) inset,
+ 0 20px 40px rgba(0,0,0,0.35),
+ 0 2px 4px rgba(0,0,0,0.3);
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
+ position: relative;
+}
+
+.island-header {
+ padding: calc(16px * var(--density)) calc(18px * var(--density)) calc(12px * var(--density));
+ border-bottom: 1px solid var(--line);
+ flex-shrink: 0;
+}
+
+.island-eyebrow {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.14em;
+ color: var(--text-faint);
+ text-transform: uppercase;
+ margin-bottom: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.island-eyebrow .dot {
+ width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
+ box-shadow: 0 0 8px var(--accent-glow);
+}
+
+.island-title {
+ font-size: 18px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ color: var(--text);
+ margin: 0;
+}
+
+.island-body {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: calc(8px * var(--density)) 0;
+}
+.island-body::-webkit-scrollbar { width: 6px; }
+.island-body::-webkit-scrollbar-track { background: transparent; }
+.island-body::-webkit-scrollbar-thumb { background: var(--line-bright); border-radius: 3px; }
+.island-body::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
+
+.island-footer {
+ padding: calc(10px * var(--density)) calc(14px * var(--density));
+ border-top: 1px solid var(--line);
+ flex-shrink: 0;
+}
+
+/* ==================== Lists island ==================== */
+.search-wrap {
+ margin: 10px 14px 4px;
+ background: var(--surface-2);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 7px 10px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ transition: border-color 0.15s;
+}
+.search-wrap:focus-within { border-color: var(--accent-dim); }
+.search-wrap input {
+ flex: 1;
+ font-family: var(--mono);
+ font-size: 12px;
+}
+.search-wrap .kbd {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-faint);
+ padding: 2px 5px;
+ border: 1px solid var(--line-bright);
+ border-radius: 3px;
+}
+
+.list-section-label {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.14em;
+ color: var(--text-faint);
+ text-transform: uppercase;
+ padding: 14px 18px 6px;
+}
+
+.list-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: calc(8px * var(--density)) 14px;
+ margin: 1px 8px;
+ border-radius: 7px;
+ cursor: pointer;
+ color: var(--text-dim);
+ position: relative;
+ transition: background 0.12s, color 0.12s;
+}
+.list-item:hover { background: var(--surface-2); color: var(--text); }
+.list-item.active {
+ background: var(--surface-3);
+ color: var(--text);
+}
+.list-item.active::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 8px;
+ bottom: 8px;
+ width: 2px;
+ border-radius: 0 2px 2px 0;
+ background: var(--accent);
+ box-shadow: 0 0 8px var(--accent-glow);
+}
+
+.list-item .icon { width: 16px; flex-shrink: 0; opacity: 0.85; }
+.list-item .label {
+ flex: 1;
+ font-size: 13px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.list-item .count {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-faint);
+ font-variant-numeric: tabular-nums;
+}
+.list-item .swatch {
+ width: 8px;
+ height: 8px;
+ border-radius: 2px;
+}
+
+.new-list-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ width: calc(100% - 16px);
+ margin: 4px 8px;
+ border-radius: 7px;
+ color: var(--text-mute);
+ font-size: 12px;
+ font-family: var(--mono);
+ letter-spacing: 0.04em;
+ transition: background 0.12s, color 0.12s;
+}
+.new-list-btn:hover { background: var(--surface-2); color: var(--text); }
+
+/* ==================== Tasks island ==================== */
+.tasks-head {
+ padding: 18px 20px 14px;
+ border-bottom: 1px solid var(--line);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ flex-wrap: nowrap;
+}
+.tasks-actions { flex-shrink: 0; }
+
+.details-col { display: contents; }
+.details-col > .island { height: 100%; }
+
+/* Collapse details island on narrow windows */
+@media (max-width: 1100px) {
+ .window-body {
+ grid-template-columns: minmax(200px, var(--sidebar-w)) minmax(340px, 1fr);
+ }
+ .details-col { display: none; }
+}
+@media (max-width: 780px) {
+ .window-body {
+ grid-template-columns: 1fr;
+ }
+ .island:first-child { display: none; }
+}
+
+.tasks-meta {
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ min-width: 0;
+ flex: 1;
+}
+.tasks-date {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.14em;
+ color: var(--text-faint);
+ text-transform: uppercase;
+}
+.tasks-title {
+ font-size: 24px;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ color: var(--text);
+ margin: 4px 0 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.tasks-subtitle {
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--text-mute);
+ margin-top: 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.tasks-subtitle .sep { color: var(--text-faint); margin: 0 6px; }
+
+.tasks-actions {
+ display: flex;
+ gap: 4px;
+}
+.icon-btn {
+ width: 30px;
+ height: 30px;
+ display: grid;
+ place-items: center;
+ border-radius: 7px;
+ color: var(--text-mute);
+ transition: background 0.12s, color 0.12s;
+}
+.icon-btn:hover { background: var(--surface-2); color: var(--text); }
+.icon-btn.active { background: var(--surface-3); color: var(--accent); }
+
+/* Add task row */
+.add-task {
+ margin: 14px 16px 8px;
+ background: var(--surface-2);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ padding: 12px 14px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ transition: border-color 0.15s;
+}
+.add-task:focus-within { border-color: var(--accent-dim); }
+.add-task .plus {
+ width: 20px; height: 20px; border-radius: 50%;
+ border: 1.5px dashed var(--text-faint);
+ display: grid; place-items: center;
+ color: var(--text-faint); font-size: 14px; line-height: 1;
+ flex-shrink: 0;
+}
+.add-task input {
+ flex: 1;
+ font-size: 14px;
+}
+.add-task .hint {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-faint);
+ letter-spacing: 0.08em;
+}
+
+.tasks-group-label {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.14em;
+ color: var(--text-faint);
+ text-transform: uppercase;
+ padding: 18px 24px 6px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.tasks-group-label::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ background: var(--line);
+}
+
+/* Task row */
+.task {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: calc(12px * var(--density)) 16px calc(12px * var(--density)) 18px;
+ margin: 2px 8px;
+ min-width: 0;
+ border-radius: 10px;
+ cursor: pointer;
+ position: relative;
+ transition: background 0.12s;
+}
+.task:hover { background: var(--surface-2); }
+.task.selected { background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line-bright); }
+.task.selected::before {
+ content: '';
+ position: absolute;
+ left: 6px;
+ top: 14px;
+ bottom: 14px;
+ width: 2px;
+ border-radius: 1px;
+ background: var(--accent);
+}
+
+/* Checkbox */
+.check {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ border: 1.5px solid var(--line-bright);
+ flex-shrink: 0;
+ margin-top: 1px;
+ position: relative;
+ transition: border-color 0.2s, background 0.2s, transform 0.15s;
+ cursor: pointer;
+ display: grid;
+ place-items: center;
+}
+.check:hover { border-color: var(--accent); transform: scale(1.05); }
+.check svg {
+ width: 12px;
+ height: 12px;
+ stroke: var(--deep);
+ stroke-width: 2.5;
+ fill: none;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-dasharray: 20;
+ stroke-dashoffset: 20;
+ transition: stroke-dashoffset 0.35s ease-out 0.05s;
+}
+.check.done {
+ background: var(--accent);
+ border-color: var(--accent);
+ animation: check-pop 0.35s ease-out;
+}
+.check.done svg { stroke-dashoffset: 0; }
+
+@keyframes check-pop {
+ 0% { transform: scale(1); }
+ 40% { transform: scale(1.2); box-shadow: 0 0 0 6px var(--accent-glow); }
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 transparent; }
+}
+
+.task-body { flex: 1; min-width: 0; }
+.task-title {
+ font-size: 14px;
+ line-height: 1.35;
+ color: var(--text);
+ transition: color 0.3s, text-decoration-color 0.3s;
+ text-decoration: line-through transparent;
+}
+.task.done .task-title {
+ color: var(--text-faint);
+ text-decoration-color: var(--text-mute);
+}
+.task-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 5px;
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.04em;
+ color: var(--text-mute);
+ flex-wrap: wrap;
+}
+.task-meta .chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 6px;
+ border-radius: 3px;
+ background: var(--surface-3);
+ color: var(--text-dim);
+}
+.task-meta .chip.due-today { color: var(--accent); background: color-mix(in oklab, var(--accent) 12%, transparent); }
+.task-meta .chip.overdue { color: var(--blood); background: color-mix(in oklab, var(--blood) 12%, transparent); }
+.task-meta .chip.starred { color: var(--peat); }
+.task-meta .tag { color: var(--text-faint); }
+.task-meta .tag::before { content: '#'; }
+.task-meta .subcount { color: var(--text-faint); }
+
+.star-btn {
+ width: 20px;
+ height: 20px;
+ display: grid;
+ place-items: center;
+ color: var(--text-faint);
+ flex-shrink: 0;
+ margin-top: 1px;
+ opacity: 0;
+ transition: opacity 0.15s, color 0.15s, transform 0.2s;
+}
+.task:hover .star-btn { opacity: 1; }
+.star-btn.on { opacity: 1; color: var(--peat); }
+.star-btn:hover { color: var(--peat); transform: scale(1.15); }
+.star-btn.pulse { animation: star-pulse 0.4s ease-out; }
+@keyframes star-pulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.35); filter: drop-shadow(0 0 6px var(--peat)); }
+ 100% { transform: scale(1); }
+}
+
+/* ==================== Details island ==================== */
+.details-empty {
+ height: 100%;
+ display: grid;
+ place-items: center;
+ text-align: center;
+ padding: 40px 24px;
+ color: var(--text-faint);
+}
+.details-empty .glyph {
+ width: 48px;
+ height: 48px;
+ border-radius: 10px;
+ border: 1px dashed var(--line-bright);
+ display: grid;
+ place-items: center;
+ margin: 0 auto 14px;
+ color: var(--text-mute);
+}
+.details-empty .label {
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--text-mute);
+}
+.details-empty .hint {
+ font-size: 12px;
+ margin-top: 8px;
+ color: var(--text-faint);
+ line-height: 1.5;
+}
+
+.details-title-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 18px 20px 14px;
+ border-bottom: 1px solid var(--line);
+}
+.details-title {
+ flex: 1;
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--text);
+ line-height: 1.4;
+ resize: none;
+ min-height: 22px;
+ max-height: 100px;
+}
+
+.details-section {
+ padding: 14px 20px;
+ border-bottom: 1px solid var(--line);
+}
+.details-section-label {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.14em;
+ color: var(--text-faint);
+ text-transform: uppercase;
+ margin-bottom: 10px;
+}
+
+.subtask-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 0;
+ font-size: 13px;
+}
+.subtask-row .check { width: 16px; height: 16px; }
+.subtask-row .check svg { width: 10px; height: 10px; }
+.subtask-row .label { flex: 1; color: var(--text-dim); }
+.subtask-row.done .label {
+ color: var(--text-faint);
+ text-decoration: line-through;
+ text-decoration-color: var(--text-mute);
+}
+.subtask-add {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 0;
+ color: var(--text-faint);
+ font-size: 13px;
+}
+.subtask-add input { flex: 1; font-size: 13px; }
+
+.meta-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 0;
+ font-family: var(--mono);
+ font-size: 11px;
+}
+.meta-row .key { color: var(--text-faint); letter-spacing: 0.06em; text-transform: uppercase; font-size: 10px; }
+.meta-row .val { color: var(--text); }
+.meta-row .val.muted { color: var(--text-mute); }
+.meta-row .val.accent { color: var(--accent); }
+.meta-row .val.peat { color: var(--peat); }
+
+.notes-area {
+ width: 100%;
+ min-height: 70px;
+ font-size: 13px;
+ line-height: 1.55;
+ color: var(--text);
+ resize: none;
+ font-family: var(--sans);
+}
+
+.tag-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 3px 8px;
+ border-radius: 4px;
+ background: var(--surface-3);
+ border: 1px solid var(--line);
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-dim);
+ margin-right: 4px;
+ margin-bottom: 4px;
+}
+.tag-chip::before { content: '#'; color: var(--text-faint); }
+
+.activity-log {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-faint);
+ line-height: 1.7;
+}
+.activity-log .time { color: var(--text-mute); margin-right: 8px; }
+
+/* ==================== Agent / Worktree / Terminal ==================== */
+.status-dot {
+ width: 8px; height: 8px; border-radius: 50%;
+ display: inline-block;
+ flex-shrink: 0;
+ position: relative;
+}
+.status-dot.running { background: var(--accent); box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 22%, transparent); }
+.status-dot.running::after {
+ content: ''; position: absolute; inset: -3px; border-radius: 50%;
+ border: 1.5px solid var(--accent); opacity: 0.6;
+ animation: pulse-ring 1.6s ease-out infinite;
+}
+.status-dot.review { background: var(--peat); }
+.status-dot.error { background: var(--blood); box-shadow: 0 0 0 3px color-mix(in oklab, var(--blood) 22%, transparent); }
+.status-dot.done { background: var(--moss-bright); opacity: 0.6; }
+.status-dot.queued { background: var(--text-mute); }
+.status-dot.idle { background: var(--text-faint); }
+
+@keyframes pulse-ring {
+ 0% { transform: scale(1); opacity: 0.7; }
+ 100% { transform: scale(2.4); opacity: 0; }
+}
+
+.status-chip {
+ display: inline-flex; align-items: center; gap: 6px;
+ padding: 3px 8px; border-radius: 4px;
+ background: var(--surface-3);
+ font-family: var(--mono);
+ font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
+}
+.status-chip.running { color: var(--accent); background: color-mix(in oklab, var(--accent) 10%, transparent); }
+.status-chip.review { color: var(--peat); background: color-mix(in oklab, var(--peat) 10%, transparent); }
+.status-chip.error { color: var(--blood); background: color-mix(in oklab, var(--blood) 10%, transparent); }
+.status-chip.done { color: var(--moss-bright); }
+.status-chip.queued { color: var(--text-mute); }
+.status-chip.idle { color: var(--text-faint); }
+
+/* Worktree card */
+.worktree-card {
+ background: var(--surface-2);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 10px 12px;
+ font-family: var(--mono);
+ font-size: 11px;
+ margin-bottom: 10px;
+}
+.worktree-card .row {
+ display: flex; align-items: center; gap: 8px;
+ padding: 3px 0;
+ color: var(--text-dim);
+}
+.worktree-card .row .k {
+ color: var(--text-faint);
+ letter-spacing: 0.06em; text-transform: uppercase;
+ font-size: 10px;
+ width: 54px;
+ flex-shrink: 0;
+}
+.worktree-card .row .v {
+ color: var(--text);
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.worktree-card .row .v.path { color: var(--text-dim); }
+.worktree-card .row .v .branch {
+ display: inline-flex; align-items: center; gap: 4px;
+ color: var(--accent);
+}
+.worktree-card .row .copy-btn {
+ color: var(--text-faint);
+ padding: 2px 4px; border-radius: 3px;
+ flex-shrink: 0;
+}
+.worktree-card .row .copy-btn:hover { color: var(--text); background: var(--surface-3); }
+
+.diff-stats {
+ display: inline-flex; align-items: center; gap: 6px;
+ font-family: var(--mono); font-size: 11px;
+}
+.diff-stats .add { color: var(--moss-bright); }
+.diff-stats .del { color: var(--blood); }
+.diff-stats .bars { display: inline-flex; gap: 1px; align-items: center; margin-left: 2px; }
+.diff-stats .bars span { width: 4px; height: 8px; background: var(--line-bright); border-radius: 1px; }
+.diff-stats .bars span.add { background: var(--moss-bright); }
+.diff-stats .bars span.del { background: var(--blood); }
+
+.action-row {
+ display: flex; gap: 6px; margin-top: 10px;
+}
+.btn {
+ display: inline-flex; align-items: center; gap: 6px;
+ padding: 7px 12px;
+ border-radius: 7px;
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: 0.04em;
+ color: var(--text);
+ background: var(--surface-3);
+ border: 1px solid var(--line-bright);
+ transition: background 0.12s, border-color 0.12s, transform 0.1s;
+ flex-shrink: 0;
+}
+.btn:hover { background: var(--line); border-color: var(--text-faint); }
+.btn:active { transform: translateY(1px); }
+.btn.primary {
+ background: color-mix(in oklab, var(--accent) 22%, var(--surface-3));
+ border-color: var(--accent-dim);
+ color: var(--text);
+}
+.btn.primary:hover { background: color-mix(in oklab, var(--accent) 32%, var(--surface-3)); }
+.btn.ghost { background: transparent; }
+.btn.danger { color: var(--blood); }
+.btn.danger:hover { background: color-mix(in oklab, var(--blood) 14%, var(--surface-3)); border-color: var(--blood); }
+.btn.icon-only { padding: 7px 8px; }
+.btn.grow { flex: 1; justify-content: center; }
+
+/* Terminal-style session log */
+.terminal {
+ background: #070a09;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ margin-top: 8px;
+ overflow: hidden;
+ display: flex; flex-direction: column;
+ max-height: 320px;
+}
+.terminal-head {
+ display: flex; align-items: center; gap: 8px;
+ padding: 7px 12px;
+ border-bottom: 1px solid var(--line);
+ background: var(--surface-2);
+}
+.terminal-head .dots { display: flex; gap: 4px; }
+.terminal-head .dots span {
+ width: 8px; height: 8px; border-radius: 50%;
+}
+.terminal-head .dots .r { background: #5a2a26; }
+.terminal-head .dots .y { background: #6a5a28; }
+.terminal-head .dots .g { background: #2f4d2f; }
+.terminal-head .lbl {
+ font-family: var(--mono); font-size: 10px;
+ color: var(--text-mute);
+ letter-spacing: 0.08em;
+ flex: 1; text-align: center;
+}
+.terminal-head .live {
+ font-family: var(--mono); font-size: 9px;
+ color: var(--accent);
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ display: flex; align-items: center; gap: 5px;
+}
+.terminal-head .live .d { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: blink 1.4s ease-in-out infinite; }
+@keyframes blink {
+ 0%, 100% { opacity: 1; } 50% { opacity: 0.3; }
+}
+
+.terminal-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px 12px 12px;
+ font-family: var(--mono);
+ font-size: 11px;
+ line-height: 1.55;
+ color: var(--text-dim);
+}
+.terminal-body::-webkit-scrollbar { width: 5px; }
+.terminal-body::-webkit-scrollbar-track { background: transparent; }
+.terminal-body::-webkit-scrollbar-thumb { background: var(--line-bright); border-radius: 3px; }
+
+.log-line {
+ display: flex; gap: 8px;
+ padding: 1px 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.log-line .ts {
+ color: var(--text-faint);
+ flex-shrink: 0;
+ font-variant-numeric: tabular-nums;
+ font-size: 10px;
+}
+.log-line .tag {
+ flex-shrink: 0;
+ font-size: 9px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ width: 44px;
+ text-align: right;
+ padding-top: 1px;
+}
+.log-line.sys .tag { color: var(--text-mute); }
+.log-line.tool .tag { color: var(--sage); }
+.log-line.msg .tag { color: var(--accent); }
+.log-line.msg .m { color: var(--text); }
+.log-line.stdout .tag { color: var(--text-faint); }
+.log-line.stderr .tag { color: #c08070; }
+.log-line.stderr .m { color: #e8a090; }
+.log-line.error .tag { color: var(--blood); }
+.log-line.error .m { color: #e8a090; }
+.log-line.done .tag { color: var(--moss-bright); }
+.log-line.done .m { color: var(--moss-bright); }
+.log-line .m { flex: 1; }
+
+.cursor-block {
+ display: inline-block;
+ width: 7px; height: 12px;
+ background: var(--accent);
+ vertical-align: middle;
+ margin-left: 4px;
+ animation: cursor 1s step-end infinite;
+}
+@keyframes cursor {
+ 50% { opacity: 0; }
+}
+
+/* Mini live-output preview on task row */
+.task-agent-line {
+ display: flex; align-items: center; gap: 6px;
+ margin-top: 6px;
+ padding: 4px 8px;
+ background: #080c0b;
+ border: 1px solid var(--line);
+ border-radius: 5px;
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-mute);
+ overflow: hidden;
+}
+.task-agent-line .prompt { color: var(--accent); }
+.task-agent-line .txt {
+ flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ color: var(--text-dim);
+}
+.task-agent-line .mini-cursor {
+ width: 5px; height: 10px; background: var(--accent);
+ animation: cursor 1s step-end infinite;
+}
+
+/* Agent header strip in details */
+.agent-strip {
+ display: flex; align-items: center; gap: 10px;
+ padding: 10px 20px;
+ border-bottom: 1px solid var(--line);
+ background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent);
+}
+.agent-strip .meta {
+ flex: 1; min-width: 0;
+ font-family: var(--mono); font-size: 10px; color: var(--text-mute);
+ letter-spacing: 0.04em;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+}
+.agent-strip .meta .sep { color: var(--text-faint); margin: 0 6px; }
+
+/* ==================== Modals ==================== */
+.modal-backdrop {
+ position: fixed; inset: 0;
+ background: rgba(3, 5, 4, 0.75);
+ backdrop-filter: blur(8px);
+ z-index: 100;
+ display: grid; place-items: center;
+ animation: fade-in 0.15s ease-out;
+}
+@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
+
+.modal {
+ background: linear-gradient(180deg, var(--surface) 0%, #131917 100%);
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 12px;
+ box-shadow: 0 40px 80px rgba(0,0,0,0.7);
+ display: flex; flex-direction: column;
+ overflow: hidden;
+ animation: modal-in 0.18s ease-out;
+}
+@keyframes modal-in {
+ from { opacity: 0; transform: translateY(8px) scale(0.98); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
+}
+.diff-modal { width: min(1100px, 90vw); height: min(720px, 88vh); }
+.worktree-modal { width: min(560px, 90vw); height: min(620px, 88vh); }
+
+.modal-head {
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--line);
+ display: flex; align-items: center; justify-content: space-between;
+ gap: 12px;
+ flex-shrink: 0;
+}
+.modal-title {
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--text);
+ letter-spacing: 0.04em;
+ display: flex; align-items: center; gap: 8px;
+}
+.modal-sub {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-faint);
+ margin-top: 3px;
+ letter-spacing: 0.04em;
+}
+.modal-sub .add { color: var(--moss-bright); }
+.modal-sub .del { color: var(--blood); }
+.modal-body { flex: 1; overflow: hidden; min-height: 0; }
+
+/* Diff modal */
+.diff-body { display: flex; }
+.diff-sidebar {
+ width: 260px;
+ border-right: 1px solid var(--line);
+ overflow-y: auto;
+ background: #0f1513;
+ flex-shrink: 0;
+}
+.diff-file-tab {
+ padding: 9px 14px;
+ cursor: pointer;
+ border-bottom: 1px solid rgba(255,255,255,0.02);
+ transition: background 0.1s;
+}
+.diff-file-tab:hover { background: var(--surface-2); }
+.diff-file-tab.active { background: var(--surface-3); }
+.diff-file-tab.active {
+ box-shadow: inset 2px 0 0 var(--accent);
+}
+.diff-file-name {
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--text);
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ direction: rtl; text-align: left;
+}
+.diff-file-stats {
+ font-family: var(--mono);
+ font-size: 10px;
+ margin-top: 2px;
+ display: flex; gap: 8px;
+}
+.diff-file-stats .add { color: var(--moss-bright); }
+.diff-file-stats .del { color: var(--blood); }
+
+.diff-view {
+ flex: 1;
+ overflow-y: auto;
+ background: #080c0b;
+}
+.diff-file-header {
+ position: sticky; top: 0;
+ display: flex; align-items: center; gap: 8px;
+ padding: 8px 16px;
+ background: var(--surface-2);
+ border-bottom: 1px solid var(--line);
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--text-dim);
+ z-index: 2;
+}
+.diff-hunk { font-family: var(--mono); font-size: 12px; line-height: 1.55; }
+.diff-hunk-header {
+ padding: 4px 16px;
+ color: var(--sage);
+ background: rgba(139, 157, 122, 0.05);
+ border-top: 1px solid var(--line);
+ border-bottom: 1px solid var(--line);
+ font-size: 11px;
+}
+.diff-line {
+ display: flex;
+ padding: 0 8px;
+ white-space: pre;
+}
+.diff-line .ln {
+ width: 40px;
+ text-align: right;
+ padding: 0 6px;
+ color: var(--text-faint);
+ font-size: 10px;
+ flex-shrink: 0;
+ font-variant-numeric: tabular-nums;
+ user-select: none;
+}
+.diff-line .sign {
+ width: 14px;
+ text-align: center;
+ flex-shrink: 0;
+ color: var(--text-faint);
+}
+.diff-line .t { color: var(--text-dim); flex: 1; }
+.diff-line.add { background: rgba(107, 142, 107, 0.08); }
+.diff-line.add .sign { color: var(--moss-bright); }
+.diff-line.add .t { color: #d0e4d0; }
+.diff-line.del { background: rgba(200, 112, 96, 0.08); }
+.diff-line.del .sign { color: var(--blood); }
+.diff-line.del .t { color: #e8bfb4; }
+
+/* Worktree modal tree */
+.tree-row {
+ display: flex; align-items: center; gap: 6px;
+ padding: 3px 8px;
+ color: var(--text-dim);
+ border-radius: 4px;
+ cursor: default;
+}
+.tree-row:hover { background: var(--surface-2); }
+.tree-row.mod { color: var(--peat); }
+.tree-row.added { color: var(--moss-bright); }
+.tree-badge {
+ margin-left: auto;
+ font-size: 9px;
+ font-family: var(--mono);
+ padding: 1px 5px;
+ border-radius: 3px;
+ letter-spacing: 0.06em;
+}
+.tree-badge.mod { background: color-mix(in oklab, var(--peat) 16%, transparent); color: var(--peat); }
+.tree-badge.add { background: color-mix(in oklab, var(--moss-bright) 16%, transparent); color: var(--moss-bright); }
+
+/* ==================== Tweaks panel ==================== */
+.tweaks-panel {
+ position: absolute;
+ bottom: 58px;
+ right: 18px;
+ width: 280px;
+ background: rgba(12, 16, 14, 0.92);
+ backdrop-filter: blur(24px) saturate(1.4);
+ -webkit-backdrop-filter: blur(24px) saturate(1.4);
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 12px;
+ padding: 14px 16px 16px;
+ box-shadow: 0 24px 48px rgba(0,0,0,0.5);
+ z-index: 50;
+ transform: translateY(6px);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.2s, transform 0.2s;
+}
+.tweaks-panel.open {
+ opacity: 1;
+ transform: translateY(0);
+ pointer-events: auto;
+}
+.tweaks-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+.tweaks-title {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.16em;
+ color: var(--text-dim);
+ text-transform: uppercase;
+}
+.tweaks-close {
+ color: var(--text-faint);
+ font-size: 14px;
+ width: 20px; height: 20px;
+ display: grid; place-items: center;
+ border-radius: 4px;
+}
+.tweaks-close:hover { background: var(--surface-2); color: var(--text); }
+
+.tweak-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 7px 0;
+}
+.tweak-row .label {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.1em;
+ color: var(--text-mute);
+ text-transform: uppercase;
+ flex-shrink: 0;
+}
+.tweak-row input[type="range"] {
+ flex: 1;
+ height: 2px;
+ -webkit-appearance: none;
+ appearance: none;
+ background: var(--line-bright);
+ border-radius: 1px;
+}
+.tweak-row input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 12px; height: 12px;
+ border-radius: 50%;
+ background: var(--accent);
+ box-shadow: 0 0 0 3px rgba(255,255,255,0.04);
+ cursor: pointer;
+}
+.tweak-row .val {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-dim);
+ min-width: 34px;
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+}
+
+.hue-swatches {
+ display: flex;
+ gap: 6px;
+ padding: 4px 0;
+}
+.hue-swatch {
+ width: 22px; height: 22px;
+ border-radius: 6px;
+ cursor: pointer;
+ border: 1.5px solid transparent;
+ transition: transform 0.1s, border-color 0.1s;
+}
+.hue-swatch:hover { transform: scale(1.1); }
+.hue-swatch.active { border-color: #fff4; transform: scale(1.1); }
+
+.density-toggle {
+ display: flex;
+ background: var(--surface-2);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ padding: 2px;
+}
+.density-toggle button {
+ padding: 4px 10px;
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-mute);
+ border-radius: 4px;
+}
+.density-toggle button.on {
+ background: var(--surface-3);
+ color: var(--text);
+}
+
+.tweaks-fab {
+ position: absolute;
+ bottom: 58px;
+ right: 18px;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: rgba(12, 16, 14, 0.88);
+ backdrop-filter: blur(24px);
+ border: 1px solid rgba(255,255,255,0.08);
+ color: var(--text-dim);
+ display: grid;
+ place-items: center;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
+ z-index: 20;
+ transition: transform 0.15s, color 0.15s;
+}
+.tweaks-fab:hover { color: var(--accent); transform: rotate(45deg); }
+.tweaks-fab.hidden { display: none; }
+
+/* Completion celebration */
+.complete-ripple {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ border-radius: 10px;
+ overflow: hidden;
+}
+.complete-ripple::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(circle at var(--rx, 30px) 50%, var(--accent-glow) 0%, transparent 60%);
+ animation: ripple 0.5s ease-out forwards;
+}
+@keyframes ripple {
+ 0% { opacity: 0.8; transform: scale(0.3); }
+ 100% { opacity: 0; transform: scale(1.2); }
+}
+
+/* Fade-in for removing done items */
+.task.leaving {
+ animation: task-leave 0.3s ease-out forwards;
+}
+@keyframes task-leave {
+ to { opacity: 0; transform: translateX(8px); }
+}
+
+/* Enter animation */
+.task.entering {
+ animation: task-enter 0.25s ease-out;
+}
+@keyframes task-enter {
+ from { opacity: 0; transform: translateY(-4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
diff --git a/docs/superpowers/plans/2026-04-17-logic-bug-fixes.md b/docs/superpowers/plans/2026-04-17-logic-bug-fixes.md
new file mode 100644
index 0000000..6c0637d
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-17-logic-bug-fixes.md
@@ -0,0 +1,705 @@
+# Logic Bug Fixes Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Fix confirmed logic bugs across Worker, App/Ui, and Installer found in the 2026-04-17 three-agent review.
+
+**Architecture:** Each bug is an isolated change to one or two files. Group by priority (Critical → High → Medium → Info). TDD where the bug is observable via xUnit integration test (Worker, Data); for UI/Installer bugs without test harness, do a focused manual repro and guard with a regression comment referencing the commit.
+
+**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm, EF Core + SQLite, SignalR, xUnit (Worker tests only).
+
+---
+
+## File Map
+
+**Worker:**
+- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — remove premature `RunCreated` broadcast
+- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — emit `RunCreated` after run row insert
+- Modify: `src/ClaudeDo.Worker/Services/QueueService.cs` — add slot-collision guard on `RunNow`/`ContinueTask`
+- Modify: `src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs` — extend quoting to cover whitespace/newline
+- Test: `tests/ClaudeDo.Worker.Tests/ClaudeArgsBuilderTests.cs` — regression for newline in system prompt
+- Test: `tests/ClaudeDo.Worker.Tests/QueueServiceSlotGuardTests.cs` — regression for RunNow-while-queued
+
+**Ui:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs` — guard nullability in `AddTask`, harden `OnTaskUpdated`
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` — defer `_taskId` assignment until after cancel check
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs` — init TCS before dialog shown
+
+**Installer:**
+- Modify: `src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs` — remove inline start; reject CurrentUser without password
+- Modify: `src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs` — rename-before-extract rollback
+- Modify: `src/ClaudeDo.Installer/Core/UninstallRunner.cs` — add `removeAppData` parameter
+- Modify: `src/ClaudeDo.Installer/Steps/WriteConfigStep.cs` — expand `~` in `UiDbPath`
+- Verify: `src/ClaudeDo.Installer/App.xaml.cs` — confirm Avalonia vs WPF usings
+
+---
+
+## Critical
+
+### Task 1: Worker — fix `RunCreated` broadcast ordering (W1)
+
+Bug: `WorkerHub.RunNow` fires `RunCreated` before the run row is inserted by `RunOnceAsync`. UI can receive an event for a row that does not yet exist.
+
+**Files:**
+- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs:35-50`
+- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs:234-256` (`RunOnceAsync`)
+
+- [ ] **Step 1: Remove premature broadcast from WorkerHub**
+
+In `src/ClaudeDo.Worker/Hub/WorkerHub.cs`, replace the body of `RunNow`:
+
+```csharp
+public async Task RunNow(string taskId)
+{
+ try
+ {
+ await _queue.RunNow(taskId);
+ }
+ catch (InvalidOperationException)
+ {
+ throw new HubException("override slot busy");
+ }
+ catch (KeyNotFoundException)
+ {
+ throw new HubException("task not found");
+ }
+}
+```
+
+- [ ] **Step 2: Emit `RunCreated` inside `RunOnceAsync` after row insert**
+
+In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, find `RunOnceAsync`. After the `runRepo.AddAsync(run, ct);` block (~line 256), add:
+
+```csharp
+await _broadcaster.RunCreated(taskId, runNumber, isRetry);
+```
+
+Then remove the existing `await _broadcaster.RunCreated(task.Id, 2, true);` on line 128 (inside the auto-retry block in `RunAsync`) and the `await _broadcaster.RunCreated(taskId, nextRunNumber, false);` on line 219 (in `ContinueAsync`), since `RunOnceAsync` now broadcasts unconditionally.
+
+- [ ] **Step 3: Build and run Worker tests**
+
+Run: `dotnet build ClaudeDo.slnx && dotnet test tests/ClaudeDo.Worker.Tests`
+Expected: all existing tests pass.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/Runner/TaskRunner.cs
+git commit -m "fix(worker): emit RunCreated after run row exists"
+```
+
+---
+
+### Task 2: Ui — harden `OnTaskUpdated` against async void crash (U2)
+
+Bug: `TaskListViewModel.OnTaskUpdated` is `async void` with no try/catch. A DB error escapes to `TaskScheduler.UnobservedTaskException` and can crash the process.
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs:328-332`
+
+- [ ] **Step 1: Wrap handler body in try/catch**
+
+Replace the existing method with:
+
+```csharp
+private async void OnTaskUpdated(string taskId)
+{
+ if (CurrentListId is null) return;
+ try
+ {
+ await RefreshSingleAsync(taskId);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[TaskListViewModel] OnTaskUpdated failed for {taskId}: {ex}");
+ }
+}
+```
+
+- [ ] **Step 2: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs
+git commit -m "fix(ui): swallow DB errors in TaskListViewModel.OnTaskUpdated"
+```
+
+---
+
+### Task 3: Installer — reject CurrentUser service registration without password (I1)
+
+Bug: `RegisterServiceStep` passes `obj=.\
` to `sc.exe create` with no `password=`. SCM rejects it with exit 5 / 1069 and the user gets an opaque error.
+
+**Files:**
+- Modify: `src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs`
+
+- [ ] **Step 1: Read the current file**
+
+Read `src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs` to confirm the exact shape of the `obj=` branch and the outer `StepResult` return.
+
+- [ ] **Step 2: Replace CurrentUser branch with early failure**
+
+Where the step builds `obj=".\\"` for the `CurrentUser` account option, replace it with:
+
+```csharp
+if (ctx.ServiceAccount == ServiceAccountType.CurrentUser)
+{
+ return StepResult.Fail(
+ "Service cannot run as Current User without a password. " +
+ "Select 'Local System' or extend ServicePage to capture a password.");
+}
+```
+
+Keep the `LocalSystem` branch (which passes `obj= LocalSystem` with no password requirement) unchanged.
+
+- [ ] **Step 3: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs
+git commit -m "fix(installer): reject CurrentUser service account without password"
+```
+
+---
+
+## High
+
+### Task 4: Worker — guard slot collision on `RunNow` and `ContinueTask` (W2)
+
+Bug: Queue slot and override slot have no guard against operating on the same `taskId`. `TaskRunner.MarkRunningAsync` can overwrite `started_at`.
+
+**Files:**
+- Modify: `src/ClaudeDo.Worker/Services/QueueService.cs:59-115`
+- Test: `tests/ClaudeDo.Worker.Tests/QueueServiceSlotGuardTests.cs` (new)
+
+- [ ] **Step 1: Write failing test**
+
+Create `tests/ClaudeDo.Worker.Tests/QueueServiceSlotGuardTests.cs`:
+
+```csharp
+using ClaudeDo.Data.Models;
+using ClaudeDo.Worker.Services;
+using Xunit;
+
+namespace ClaudeDo.Worker.Tests;
+
+public class QueueServiceSlotGuardTests : WorkerTestBase
+{
+ [Fact]
+ public async Task RunNow_rejects_task_already_active_in_queue_slot()
+ {
+ var queue = ServiceProvider.GetRequiredService();
+ var task = await SeedAgentTaskAsync(listId: await SeedListAsync(), title: "blocker");
+
+ // Prime queue slot by wake signal.
+ queue.WakeQueue();
+ await WaitForActiveSlotAsync("queue", task.Id);
+
+ // RunNow on the same id must throw InvalidOperationException.
+ await Assert.ThrowsAsync(() => queue.RunNow(task.Id));
+ }
+}
+```
+
+(Helpers `WorkerTestBase`, `SeedAgentTaskAsync`, `SeedListAsync`, `WaitForActiveSlotAsync` exist in the test project — follow the pattern from existing tests.)
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~QueueServiceSlotGuardTests"`
+Expected: FAIL — currently `RunNow` succeeds and creates a duplicate slot.
+
+- [ ] **Step 3: Add guard in `RunNow`**
+
+In `src/ClaudeDo.Worker/Services/QueueService.cs`, inside the `lock (_lock)` block in `RunNow` (~line 69), add before the existing override check:
+
+```csharp
+if (_queueSlot?.TaskId == taskId)
+ throw new InvalidOperationException("task is already running in queue slot");
+```
+
+- [ ] **Step 4: Add same guard in `ContinueTask`**
+
+In the `lock (_lock)` block in `ContinueTask` (~line 97), add:
+
+```csharp
+if (_queueSlot?.TaskId == taskId)
+ throw new InvalidOperationException("task is already running in queue slot");
+```
+
+- [ ] **Step 5: Run tests**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests`
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/ClaudeDo.Worker/Services/QueueService.cs tests/ClaudeDo.Worker.Tests/QueueServiceSlotGuardTests.cs
+git commit -m "fix(worker): guard against same task in queue and override slot"
+```
+
+---
+
+### Task 5: Worker — quote CLI args with tab/newline/carriage-return (W3)
+
+Bug: `ClaudeArgsBuilder.Escape` only quotes on space/quote. System prompts with newlines pass through unquoted and corrupt the argument list.
+
+**Files:**
+- Modify: `src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs:56-64`
+- Test: `tests/ClaudeDo.Worker.Tests/ClaudeArgsBuilderTests.cs` (new or existing)
+
+- [ ] **Step 1: Write failing test**
+
+If `ClaudeArgsBuilderTests.cs` does not exist, create it:
+
+```csharp
+using ClaudeDo.Worker.Runner;
+using Xunit;
+
+namespace ClaudeDo.Worker.Tests;
+
+public class ClaudeArgsBuilderTests
+{
+ [Fact]
+ public void Build_quotes_system_prompt_with_newline()
+ {
+ var builder = new ClaudeArgsBuilder();
+ var args = builder.Build(new ClaudeRunConfig(
+ Model: null,
+ SystemPrompt: "line1\nline2",
+ AgentPath: null,
+ ResumeSessionId: null));
+
+ Assert.Contains("--append-system-prompt \"line1\\nline2\"", args);
+ }
+
+ [Fact]
+ public void Build_quotes_system_prompt_with_tab()
+ {
+ var builder = new ClaudeArgsBuilder();
+ var args = builder.Build(new ClaudeRunConfig(
+ Model: null,
+ SystemPrompt: "col1\tcol2",
+ AgentPath: null,
+ ResumeSessionId: null));
+
+ Assert.Contains("\"col1", args);
+ }
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~ClaudeArgsBuilderTests"`
+Expected: FAIL — newline is passed through unquoted.
+
+- [ ] **Step 3: Extend `Escape` condition and escape newline/tab**
+
+In `src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs`, replace `Escape`:
+
+```csharp
+private static string Escape(string value)
+{
+ if (value.Contains(' ') || value.Contains('"') || value.Contains('\'')
+ || value.Contains('\t') || value.Contains('\n') || value.Contains('\r'))
+ {
+ var escaped = value
+ .Replace("\\", "\\\\")
+ .Replace("\"", "\\\"")
+ .Replace("\n", "\\n")
+ .Replace("\r", "\\r")
+ .Replace("\t", "\\t");
+ return $"\"{escaped}\"";
+ }
+ return value;
+}
+```
+
+- [ ] **Step 4: Run tests**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~ClaudeArgsBuilderTests"`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs tests/ClaudeDo.Worker.Tests/ClaudeArgsBuilderTests.cs
+git commit -m "fix(worker): escape newline/tab in CLI args"
+```
+
+---
+
+### Task 6: Installer — remove inline service start from `RegisterServiceStep` (I2)
+
+Bug: `RegisterServiceStep` calls `sc.exe start` inline. `StartServiceStep` exists separately. If the update path ever wires both, the service is started twice.
+
+**Files:**
+- Modify: `src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs`
+- Modify: `src/ClaudeDo.Installer/App.xaml.cs` (pipeline) — ensure `StartServiceStep` is in the fresh-install pipeline
+
+- [ ] **Step 1: Read current pipeline wiring in App.xaml.cs**
+
+Read `src/ClaudeDo.Installer/App.xaml.cs` around line 112 to confirm the list of steps passed into `InstallerService`.
+
+- [ ] **Step 2: Remove inline `sc.exe start` from RegisterServiceStep**
+
+Delete the block (~lines 72-77) that runs `sc.exe start ` when `ctx.AutoStart == true`.
+
+- [ ] **Step 3: Add `StartServiceStep` to the fresh-install pipeline if missing**
+
+In `App.xaml.cs`, append `new StartServiceStep(...)` after `RegisterServiceStep` in the step list. Gate its execution internally on `ctx.AutoStart` (it already handles exit code 1056).
+
+- [ ] **Step 4: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs src/ClaudeDo.Installer/App.xaml.cs
+git commit -m "fix(installer): move service start out of RegisterServiceStep"
+```
+
+---
+
+### Task 7: Installer — rollback-safe extract in `DownloadAndExtractStep` (I3)
+
+Bug: Old `app/` and `worker/` are deleted before extraction. If extraction throws, user is left with no binaries and no recovery path.
+
+**Files:**
+- Modify: `src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs:70-95`
+
+- [ ] **Step 1: Read the current delete/extract sequence**
+
+Read `src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs` around lines 70-95 to identify the exact `Directory.Delete` and `ZipFile.ExtractToDirectory` calls and which `ctx` paths they reference.
+
+- [ ] **Step 2: Replace delete-before-extract with rename-then-commit**
+
+Wrap the delete+extract block:
+
+```csharp
+var appDir = Path.Combine(ctx.InstallRoot, "app");
+var workDir = Path.Combine(ctx.InstallRoot, "worker");
+var appBak = appDir + ".bak";
+var workBak = workDir + ".bak";
+
+// Stash existing dirs.
+if (Directory.Exists(appBak)) Directory.Delete(appBak, recursive: true);
+if (Directory.Exists(workBak)) Directory.Delete(workBak, recursive: true);
+if (Directory.Exists(appDir)) Directory.Move(appDir, appBak);
+if (Directory.Exists(workDir)) Directory.Move(workDir, workBak);
+
+try
+{
+ ZipFile.ExtractToDirectory(zipPath, ctx.InstallRoot, overwriteFiles: true);
+}
+catch
+{
+ // Roll back to previous binaries.
+ if (Directory.Exists(appDir)) Directory.Delete(appDir, recursive: true);
+ if (Directory.Exists(workDir)) Directory.Delete(workDir, recursive: true);
+ if (Directory.Exists(appBak)) Directory.Move(appBak, appDir);
+ if (Directory.Exists(workBak)) Directory.Move(workBak, workDir);
+ throw;
+}
+
+// Success — drop stash.
+if (Directory.Exists(appBak)) Directory.Delete(appBak, recursive: true);
+if (Directory.Exists(workBak)) Directory.Delete(workBak, recursive: true);
+```
+
+- [ ] **Step 3: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs
+git commit -m "fix(installer): rollback-safe extract with .bak stash"
+```
+
+---
+
+### Task 8: Installer — gate `~/.todo-app` deletion behind explicit consent (I4)
+
+Bug: Uninstaller always deletes user data (db, logs, configs). Reinstalling a different version silently destroys all tasks.
+
+**Files:**
+- Modify: `src/ClaudeDo.Installer/Core/UninstallRunner.cs:60-80`
+- Modify: `src/ClaudeDo.Installer/Views/UninstallPage.xaml(.cs)` (or equivalent) — add a checkbox
+
+- [ ] **Step 1: Read `UninstallRunner.RunAsync` signature**
+
+Read `src/ClaudeDo.Installer/Core/UninstallRunner.cs` around lines 1-90 to get current signature.
+
+- [ ] **Step 2: Add `removeAppData` parameter to `RunAsync`**
+
+Change signature to:
+
+```csharp
+public async Task RunAsync(bool removeAppData, CancellationToken ct = default)
+```
+
+Guard the deletion:
+
+```csharp
+if (removeAppData)
+{
+ var appData = Paths.Expand("~/.todo-app");
+ if (Directory.Exists(appData))
+ Directory.Delete(appData, recursive: true);
+}
+```
+
+- [ ] **Step 3: Wire a "Remove user data" checkbox on the uninstall page**
+
+In the uninstall view/VM, add `[ObservableProperty] private bool _removeAppData;` (default `false`) and pass it into `RunAsync(RemoveAppData)`.
+
+- [ ] **Step 4: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Installer/Core/UninstallRunner.cs src/ClaudeDo.Installer/Views/UninstallPage.xaml src/ClaudeDo.Installer/Views/UninstallPage.xaml.cs
+git commit -m "fix(installer): make user-data deletion on uninstall opt-in"
+```
+
+---
+
+## Medium
+
+### Task 9: Ui — guard `AddTask` against null `CurrentListId` after await (U1)
+
+Bug: `AddTask` awaits `editor.LoadAgentsAsync`. Between `CanAddTask` and `listRepo.GetByIdAsync(CurrentListId)` on line 164, a concurrent `LoadAsync(null)` could null the id. Compiler warns CS8604.
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs:157-170`
+
+- [ ] **Step 1: Capture `CurrentListId` before the first `await`**
+
+Replace the start of `AddTask`:
+
+```csharp
+[RelayCommand(CanExecute = nameof(CanAddTask))]
+private async Task AddTask()
+{
+ var listId = CurrentListId;
+ if (listId is null) return;
+
+ string defaultCommitType;
+ using (var context = _dbFactory.CreateDbContext())
+ {
+ var listRepo = new ListRepository(context);
+ var list = await listRepo.GetByIdAsync(listId);
+ defaultCommitType = list?.DefaultCommitType ?? "chore";
+ }
+
+ var editor = _editorFactory();
+ await editor.LoadAgentsAsync(_worker);
+ editor.InitForCreate(listId, defaultCommitType);
+ // …rest unchanged, but use `listId` consistently where CurrentListId was read
+```
+
+Audit the rest of the method: replace every subsequent read of `CurrentListId` with `listId`.
+
+- [ ] **Step 2: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds with no CS8604 on this method.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs
+git commit -m "fix(ui): capture CurrentListId before await in AddTask"
+```
+
+---
+
+### Task 10: Ui — defer `_taskId` assignment in `TaskDetailViewModel.LoadAsync` (U3)
+
+Bug: `_taskId = taskId` is set at line 87, before the previous `_loadCts` is cancelled. If load is cancelled, `_taskId` has been clobbered but `HasWorktree` / `CanWorktreeAction` still reflect the previous task.
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs:76-90`
+
+- [ ] **Step 1: Reset stale worktree state when starting a new load**
+
+Replace the start of `LoadAsync`:
+
+```csharp
+public async Task LoadAsync(string taskId)
+{
+ var oldCts = _loadCts;
+ var cts = new CancellationTokenSource();
+ _loadCts = cts;
+ oldCts?.Cancel();
+ oldCts?.Dispose();
+ var ct = cts.Token;
+
+ _taskId = taskId;
+
+ // Clear stale worktree state so buttons don't act on the previous task.
+ HasWorktree = false;
+ WorktreeState = "";
+ BranchName = null;
+ DiffStat = null;
+ WorktreePath = null;
+ OnPropertyChanged(nameof(CanWorktreeAction));
+
+ LiveText = "";
+ _formatter = new StreamLineFormatter();
+ // …rest unchanged
+```
+
+- [ ] **Step 2: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs
+git commit -m "fix(ui): reset stale worktree state on TaskDetail reload"
+```
+
+---
+
+### Task 11: Ui — initialize TCS before dialog shown in `TaskEditorViewModel` (U4)
+
+Bug: `ShowAndWaitAsync` creates a fresh `_tcs` only when called. If `Save` fires before `ShowAndWaitAsync` (possible if `ShowDialogAsync` is ever awaited), the result is dropped.
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs:72-80, 260-264`
+- Modify: `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs` (same pattern — apply identically)
+
+- [ ] **Step 1: Reset `_tcs` at the start of `InitForCreate` and `InitForEditAsync`**
+
+In `TaskEditorViewModel.cs`, at the top of `InitForCreate`:
+
+```csharp
+public void InitForCreate(string listId, string defaultCommitType = "chore")
+{
+ _tcs = new TaskCompletionSource();
+ _editId = null;
+ // …rest unchanged
+```
+
+Same first line at the top of `InitForEditAsync` and `InitForEdit`.
+
+- [ ] **Step 2: Remove re-assignment in `ShowAndWaitAsync`**
+
+```csharp
+public Task ShowAndWaitAsync() => _tcs.Task;
+```
+
+- [ ] **Step 3: Apply the same pattern to `ListEditorViewModel`**
+
+Mirror the same three edits in `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs` (reset TCS in its `InitForCreate` / `InitForEdit`, strip the creation in `ShowAndWaitAsync`).
+
+- [ ] **Step 4: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs
+git commit -m "fix(ui): init editor TCS before dialog can complete"
+```
+
+---
+
+### Task 12: Installer — expand `~` in `UiDbPath` (I5)
+
+Bug: `workerCfg.DbPath = Paths.Expand(ctx.DbPath)` but `uiCfg.DbPath = ctx.UiDbPath` is stored as-is. If UI cannot expand `~` at runtime on Windows, DB path is unresolvable.
+
+**Files:**
+- Modify: `src/ClaudeDo.Installer/Steps/WriteConfigStep.cs:31-34`
+
+- [ ] **Step 1: Expand UiDbPath symmetrically**
+
+Change the assignment:
+
+```csharp
+uiCfg.DbPath = Paths.Expand(ctx.UiDbPath);
+```
+
+- [ ] **Step 2: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Installer/Steps/WriteConfigStep.cs
+git commit -m "fix(installer): expand ~ in UiDbPath"
+```
+
+---
+
+## Info
+
+### Task 13: Installer — verify App.xaml.cs WPF-vs-Avalonia usings (I6)
+
+Suspected bug: `src/ClaudeDo.Installer/App.xaml.cs` uses `System.Windows` (WPF). If the project is Avalonia, wrong base class is inherited.
+
+**Files:**
+- Read: `src/ClaudeDo.Installer/App.xaml.cs`
+- Read: `src/ClaudeDo.Installer/ClaudeDo.Installer.csproj`
+
+- [ ] **Step 1: Inspect the csproj for the UI framework SDK**
+
+Read `src/ClaudeDo.Installer/ClaudeDo.Installer.csproj` and look for `` or ``.
+
+- [ ] **Step 2: Decision fork**
+
+- If WPF (`true`): `System.Windows` is correct. Stop. No fix needed.
+- If Avalonia: replace `using System.Windows;` with `using Avalonia;` and change `Application` / `StartupEventArgs` / `ExitEventArgs` to Avalonia equivalents (`Avalonia.Application`, lifetime `OnFrameworkInitializationCompleted`).
+
+- [ ] **Step 3: Build**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: build succeeds.
+
+- [ ] **Step 4: Commit only if changed**
+
+```bash
+git add src/ClaudeDo.Installer/App.xaml.cs
+git commit -m "fix(installer): use Avalonia application base class"
+```
+
+---
+
+## Out of Scope
+
+Deferred / not fixed in this plan:
+- `TryScheduleTrampolineDelete` PID-less delay (I6 in review, low severity) — `ping -n 3` is flaky but rarely hit
+- `AvailableAgents` being `List` instead of `ObservableCollection` (U5/info) — current `OnPropertyChanged` pattern works; revisit only if a bug manifests
+
+---
+
+## Self-Review Notes
+
+- Every Worker bug (W1–W3) has a regression test or tested path.
+- Every UI fix names the exact file:line and shows the replacement snippet.
+- Installer Task 3 (I1) does not guess a password-capture UI — it deliberately returns `StepResult.Fail`, leaving the UX change for a later plan.
+- Task 13 (I6) is a conditional task with a decision fork; no speculative rewrite.
+- Types are consistent: `RunCreated(taskId, runNumber, isRetry)` in Task 1 matches the existing `HubBroadcaster.RunCreated` signature used at `TaskRunner.cs:128,219`.
diff --git a/docs/superpowers/plans/2026-04-20-ui-polish-design-parity.md b/docs/superpowers/plans/2026-04-20-ui-polish-design-parity.md
new file mode 100644
index 0000000..f1e64b8
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-20-ui-polish-design-parity.md
@@ -0,0 +1,209 @@
+# UI Polish — Design Parity Follow-up
+
+> Follow-up to the islands rewrite. Closes visible gaps between the current state and the handoff mock. Execute with subagent-driven development; phases B/C/D can run in parallel.
+
+**Goal:** Bring the rewrite to pixel-level parity with `docs/UI Rewrite/design_handoff_claudedo/ClaudeDo-standalone.html`.
+
+**Tech stack:** Avalonia 12, CommunityToolkit.Mvvm. No new dependencies.
+
+**Reference files:**
+- Source of truth: `docs/UI Rewrite/design_handoff_claudedo/ClaudeDo-standalone.html`
+- CSS measurements: `docs/UI Rewrite/design_handoff_claudedo/styles.css`
+- JSX component structure: `docs/UI Rewrite/design_handoff_claudedo/islands.jsx`, `app.jsx`
+- Tokens: `src/ClaudeDo.Ui/Design/Tokens.axaml`
+- Styles: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
+
+**Rules for all phases:**
+- Use existing token brushes (`MossBrush`, `PeatBrush`, `AccentSoftBrush`, etc.) — do NOT hard-code hex.
+- Use `Classes="foo"` + selectors in `IslandStyles.axaml` for reusable styling; inline AXAML setters for one-off values only.
+- Icons: use `Projektion.Avalonia` `PathIcon` with `Data="{StaticResource IconKey}"`. Define new `StreamGeometry` resources in `IslandStyles.axaml` under an `` section when needed. Pull the SVG paths from the JSX reference.
+- Read the relevant JSX + CSS file in the handoff before implementing each component — those are the source of truth for exact measurements/paddings/colors.
+- Do not touch the data layer, Worker, SignalR, or command wiring. This is a view/style-only pass.
+
+---
+
+## Phase A — Shell + title bar (sequential, run first)
+
+One subagent. Small blast radius; prerequisite for the visual "feel."
+
+### Task A1 — Custom title bar
+
+**Files:**
+- `src/ClaudeDo.Ui/Views/MainWindow.axaml` + `.axaml.cs`
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (add title-bar styles + window-control icon-button style)
+
+- [ ] Replace the current title bar Grid with a 3-section layout:
+ - Left: brand block — checkbox-style green glyph + `CLAUDEDO` (mono, uppercase, tracking 1.4, 11px) + separator dot + current-list name eyebrow-style (mono uppercase, `TextDim`). Bind the list name to `Shell.Lists.SelectedList.Name.ToUpperInvariant()`.
+ - Middle: draggable strip (`PointerPressed → BeginMoveDrag`).
+ - Right: three frameless icon buttons (minimize / maximize-restore / close). Close button hover turns `BloodBrush`. Use `PathIcon` with inline `StreamGeometry` for the Lucide-style icons: `Minus`, `Square`, `X` — the exact SVG `d` strings are in `icons.jsx`.
+- [ ] Title bar height: 36px, background `DeepBrush`, bottom border 1px `LineBrush`.
+- [ ] Remove the character glyphs currently used for the window controls (`—`, `▢`, `✕`) — use PathIcons instead.
+- [ ] Commit: `style(ui): custom title bar with brand and window controls`
+
+### Task A2 — Background + island shadow
+
+**Files:**
+- `src/ClaudeDo.Ui/Views/MainWindow.axaml` (background layer)
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (island shadow adjust)
+
+- [ ] Under the three-island Grid, add a `Border` filling the whole row with a subtle radial gradient from `DeepBrush` (center) to `VoidBrush` (edges). Use a `RadialGradientBrush` with 2 stops; keep opacity light.
+- [ ] In `IslandStyles.axaml`, bump the `Border.island` `BoxShadow` to match the token `IslandShadow` value exactly (`0 20 40 #59000000, 0 2 4 #4D000000`). Verify by inspecting the current style — if it's already set, no-op.
+- [ ] Commit: `style(ui): background gradient and stronger island shadow`
+
+---
+
+## Phase B — Lists island polish (parallel with C, D)
+
+### Task B1 — Icon geometries + eyebrow rename + sections
+
+**Files:**
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (add `StreamGeometry` icon resources at the top)
+- `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs` (map `IconKey` strings → resource keys, add section grouping)
+- `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml`
+
+- [ ] Extract the SVG path `d` strings from `icons.jsx` for: `Sun`, `Activity` (pulse), `Star`, `Calendar`, `Eye`, `Inbox`, `Folder`, `Search`, `Plus`, `MoreHorizontal`. Define each as an `x:Key="Icon.Sun"` `StreamGeometry` in `IslandStyles.axaml`.
+- [ ] Change Lists eyebrow from `WORKSPACE` to `NAVIGATOR`.
+- [ ] Add two section-header rows in the ItemsControl: `SMART LISTS` (above items of `Kind=Smart` + `Virtual`) and `MY LISTS` (above items of `Kind=User`). Simplest approach: two separate `ItemsControl`s bound to filtered subsets; or wrap items in a `CollectionViewSource` grouping. Pick the simplest working approach.
+- [ ] Per-item icon: bind `PathIcon Data="{DynamicResource Icon.{IconKey}}"` via a tiny `IconGeometryConverter` (takes `IconKey` string → looks up resource). Icon color: `TextMute` default; `AccentBrush` (moss) when `IsActive`.
+- [ ] User-list items: use a 6px circle with `MossBrush` / `PeatBrush` / `SageBrush` dot instead of folder icon (map per list index mod colors, or single color if simpler).
+- [ ] Active state: remove solid fill. Use `AccentSoftBrush` (~10% moss) + left 2px accent bar + `AccentBrush` icon + `TextBrush` text.
+- [ ] Commit: `style(ui): lists icons, section headers, active state`
+
+### Task B2 — Search bar + keyboard hint + footer buttons
+
+**Files:**
+- `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml`
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (search style + kbd chip style)
+
+- [ ] Search `TextBox`: wrap in a `Grid ColumnDefinitions="Auto,*,Auto"` — left `PathIcon Data="{Icon.Search}"` (14px, `TextFaint`), middle TextBox, right `Border Classes="kbd"` with `⌘K` (or `Ctrl K` on Win). The `kbd` chip: mono 10px, `Surface2` bg, `LineBrush` border, padding `6,2`, radius 4.
+- [ ] Under the items list, add:
+ - `+ New list` button — plain icon+text row, `PathIcon Data="{Icon.Plus}"`, hover tint.
+ - User profile row — avatar circle (initials fallback, seed from `Environment.UserName`), name (`Environment.UserName`), subtitle `{MachineName} / local` mono dim, right `PathIcon Data="{Icon.MoreHorizontal}"`.
+- [ ] Commit: `style(ui): lists search icon, kbd hint, footer actions`
+
+---
+
+## Phase C — Tasks island polish (parallel with B, D)
+
+### Task C1 — Header + add-task row styling
+
+**Files:**
+- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (subtitle format, header toolbar properties)
+- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (kbd-enter, add-task row)
+
+- [ ] Subtitle format: change from `{open} open · {running} running · {review} in review` to `{Weekday}, {Month} {Day} · {open} open` to match the mock. Keep the running/review counts visible but move them into a right-aligned mono pill row next to the title (or drop if cleaner).
+- [ ] Eyebrow: keep current `MONTAG · APR. 20` pattern. Title remains list name.
+- [ ] Right-side icon toolbar: three `Button Classes="icon-btn"` — `Sort` icon, `Eye` icon (toggle completed), `MoreHorizontal`. Icons: pull paths from `icons.jsx`. Wire `Eye` to an `IsShowingCompleted` observable (persist in a private field for now; no DB change).
+- [ ] Add-task row: wrap the `TextBox` in a `Border` with `Surface2` bg, rounded 8px, 14px padding. Prepend a circular `PathIcon Data="{Icon.Plus}"` (20px circle, `Surface3` bg). Append a `Border Classes="kbd"` with `ENTER` text (only visible when `NewTaskTitle` has focus — bind visibility to `TextBox.IsFocused`).
+- [ ] Commit: `style(ui): tasks header toolbar and add-task row`
+
+### Task C2 — Task row chips + states
+
+**Files:**
+- `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` (expose a few more flags: `IsOverdue`, `Tags`, `StepsCount`, `StepsCompleted`)
+- `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (chip variants, selected accent, done state, live-tail meter)
+
+- [ ] Chip set per row (ItemsControl or StackPanel):
+ - Status chip (already present) — ensure color maps per Status → token brush (idle/queued/running/review/error).
+ - List chip — small colored bullet (6px circle in `MossBrush` or similar) + list name.
+ - Branch chip — `PathIcon Data="{Icon.GitBranch}"` (12px) + branch name (mono 10px).
+ - Diff chip — `+N` moss + ` ` + `−M` blood.
+ - Tags — one chip per tag (`#refactor` style, `Surface2` bg, mono 10px, `TextDim`).
+- [ ] Selected state: add 2px `AccentBrush` left border on the row Border when `IsSelected=true` (style selector `Border.task-row.selected`). Background shifts to `AccentSoftBrush`.
+- [ ] Done state: strike-through title + fade opacity to 0.5. Add `Border.task-row:has(.done)` equivalent via the existing `Done` binding — simpler: a `TextBlock` style selector that flips `TextDecorations`.
+- [ ] Live-tail row (only visible when `Status == Running` and `LiveTail != null`): a `Border` under the chip row with mono 11px ellipsized text + a slim 3px progress `Rectangle` with `MossBrush`. For now the progress is static 30% — wire it to a future `ProgressFraction` property (leave as 0.3 fallback).
+- [ ] Ensure `task-row` Border has `Transitions` for `Background` + `Margin` (smooth hover + select).
+- [ ] Commit: `style(ui): task row chip set, selected/done states, live tail`
+
+### Task C3 — Section dividers (OVERDUE / TASKS / COMPLETED)
+
+**Files:**
+- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (group the ObservableCollection into sections)
+- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml` (group headers)
+
+- [ ] Add grouping: transform `Items` into three sub-collections:
+ - `OverdueItems` — tasks with `ScheduledFor < Today` and not Done.
+ - `OpenItems` — remaining not-Done tasks.
+ - `CompletedItems` — tasks with `Done=true`.
+- [ ] Expose as three `ObservableCollection` on the VM. Recompute inside `LoadForList`.
+- [ ] View: three `ItemsControl`s stacked in a `StackPanel`, each preceded by a section header `TextBlock` — `OVERDUE` (only if non-empty), `TASKS`, `COMPLETED · {N}`. Eyebrow style, `TextFaint`.
+- [ ] Commit: `style(ui): task section dividers overdue/tasks/completed`
+
+---
+
+## Phase D — Details island polish (parallel with B, C)
+
+### Task D1 — Header + task row restyle
+
+**Files:**
+- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` (expose `TaskIdBadge` like `#T1`, computed from task id prefix)
+- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
+
+- [ ] Top header block:
+ - Eyebrow `LOGBOOK` + right-aligned `#T{shortId}` badge (first 3 hex chars of `Task.Id`, mono `TextFaint`).
+ - Title: keep editable title `TextBox` but reduce size and match mock.
+- [ ] Under header, a new "task strip" row: `Ellipse` checkbox (bound to `Task.Done` toggle) + title + right-aligned star button. This is separate from the editable title (mock shows both title as editable heading AND a task-row-style strip with check/star).
+- [ ] Commit: `style(ui): details header with logbook eyebrow and task-id badge`
+
+### Task D2 — Agent strip v2
+
+**Files:**
+- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` (add `Turns`, `TokensFormatted`, `ElapsedFormatted`, `DiffAdditions`, `DiffDeletions`, `CommitsOnBranch` if not present — most exist)
+- `src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml`
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (diff meter bar style)
+
+- [ ] Layout (three rows):
+ - Row 1: pulsing status dot + status label (`RUNNING` etc.) + mono model name + right-aligned stop button (only visible when Running).
+ - Row 2: `WORKTREE` section label + worktree path mono, with a copy-to-clipboard `PathIcon Data="{Icon.Copy}"` button at the end.
+ - Row 3: Branch line — `PathIcon Data="{Icon.GitBranch}"` + branch mono + arrow `←` + `main` + commits count chip.
+ - Row 4: `DIFF` label + `+{additions}` (moss) + `−{deletions}` (blood) + a slim 4px progress-meter `Rectangle` showing additions vs deletions ratio (moss-filled portion).
+- [ ] Action buttons row: `Open diff`, `Worktree`, external-link `→` (opens file:// to worktree path in OS explorer).
+- [ ] Agent strip should use `AgentStripStyle.Classes` bound to the running status so colors shift.
+- [ ] Commit: `style(ui): agent strip with worktree panel and diff meter`
+
+### Task D3 — Session terminal styling
+
+**Files:**
+- `src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml`
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (terminal header, log-line columns, `LIVE` chip)
+- `src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs` (add `TimestampFormatted` property)
+
+- [ ] Top bar of the terminal `Border`: three colored dots (red/yellow/green, 8px `Ellipse`) + `claude-session · {branch}` mono text + right-aligned `LIVE` chip (moss bg, white text, pulsing animation when a task is actively running).
+- [ ] Log lines: two-column layout — timestamp (mono 10px, `TextFaint`, fixed 70px width) + kind marker (e.g. `TOOL`, `CLAUDE`, `OUT`) + text. Kind marker uses attribute selector `[Tag=log-tool]`, color-mapped.
+- [ ] Line number/timestamp: add `TimestampFormatted` to `LogLineViewModel` populated as `DateTime.Now.ToString("HH:mm:ss")` on construction. (If real timestamps arrive via SignalR later, swap source.)
+- [ ] Ensure auto-scroll still works (existing logic).
+- [ ] Commit: `style(ui): session terminal header, line columns, LIVE chip`
+
+### Task D4 — Subtasks, notes, metadata footer
+
+**Files:**
+- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
+- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (subtask row style)
+- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` (delete-task command, close-detail command)
+
+- [ ] Subtasks: each row is a compact `Border` with rounded 6px, hover background. Check is an `Ellipse` matching the task-row style (not default WinForms-style CheckBox). Completed items get strike-through + fade.
+- [ ] Notes `TextBox`: `Surface2` bg, 12px padding, watermark `Notes...`, auto-saves on `LostFocus` (call repository `Update`).
+- [ ] Bottom metadata bar (sticky at the bottom of the Details island — anchor via `DockPanel.Dock="Bottom"`):
+ - Left: `PathIcon Data="{Icon.Trash}"` delete button (prompts confirmation before calling `TaskRepository.DeleteAsync`).
+ - Middle: `Created {Month Day}` mono `TextFaint`.
+ - Right: close-details `PathIcon Data="{Icon.X}"` (clears `SelectedTask` on `TasksIslandViewModel`).
+- [ ] Commit: `style(ui): subtasks, notes, details metadata footer`
+
+---
+
+## Execution order
+
+```
+Phase A (A1 → A2) [sequential, 1 subagent]
+ ↓
+Phase B, C, D [parallel, 3 subagents, one per phase]
+ ↓
+Final build + smoke
+```
+
+Phase A is sequential because it touches `MainWindow.axaml` and `IslandStyles.axaml` root setup.
+Phases B, C, D each own a distinct island. Only potential conflict: all three add icon geometries to `IslandStyles.axaml`. Mitigation: Phase B is responsible for adding the `StreamGeometry` icon resources (it needs the most). Phases C and D reference those keys without redefining.
+
+Final pass: run the app, eyeball against the mock, note remaining gaps.
diff --git a/docs/superpowers/plans/2026-04-20-ui-rewrite-islands.md b/docs/superpowers/plans/2026-04-20-ui-rewrite-islands.md
new file mode 100644
index 0000000..262ec1f
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-20-ui-rewrite-islands.md
@@ -0,0 +1,1636 @@
+# UI Rewrite — Islands Layout Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the current `ClaudeDo.Ui` with a high-fidelity three-island Avalonia interface per the design handoff at `docs/UI Rewrite/design_handoff_claudedo/`.
+
+**Architecture:** Data-layer additions (`IsStarred`, `IsMyDay`, `Notes` columns; default-list seeding); a new `Design/` resource folder (`Tokens.axaml`, `IslandStyles.axaml`); embedded Inter Tight + JetBrains Mono fonts; a chromeless `MainWindow` containing a three-column `Grid` of island `Border`s — Lists / Tasks / Details — backed by new `IslandsShellViewModel`, `ListsIslandViewModel`, `TasksIslandViewModel`, `DetailsIslandViewModel`. Existing `WorkerClient`, `Repositories` and SignalR plumbing are preserved.
+
+**Tech Stack:** .NET 8.0, Avalonia 12.0.0 (Fluent theme), CommunityToolkit.Mvvm, Entity Framework Core (SQLite), xUnit.
+
+**Reference files:**
+- `docs/UI Rewrite/design_handoff_claudedo/README.md` — full handoff
+- `docs/UI Rewrite/design_handoff_claudedo/Tokens.axaml` — design tokens
+- `docs/UI Rewrite/design_handoff_claudedo/IslandStyles.axaml` — control styles
+- `docs/UI Rewrite/design_handoff_claudedo/styles.css` — measurement source of truth
+- `docs/UI Rewrite/design_handoff_claudedo/ClaudeDo-standalone.html` — interactive reference
+
+**Confirmed decisions (from brainstorm):**
+- Full rewrite of `ClaudeDo.Ui` (Views + ViewModels). `WorkerClient`, repositories, SignalR untouched.
+- Schema: add `IsStarred`, `IsMyDay`, `Notes` to `TaskEntity`. Seed `My Day`, `Important`, `Planned` Lists on install.
+- `Running` and `Review` lists are **virtual filters** (status-based), not seeded list rows.
+- Window: chromeless (`SystemDecorations="None"` + `ExtendClientAreaToDecorationsHint="True"`).
+- Fonts: embed `Inter Tight` and `JetBrains Mono`.
+- Resource folder: `src/ClaudeDo.Ui/Design/`.
+
+---
+
+## File Structure
+
+**Data layer (new / modified):**
+- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs` — add 3 properties
+- Modify: `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs` — column mappings
+- Create: `src/ClaudeDo.Data/Migrations/_AddTaskFlagsAndNotes.cs` — EF migration
+- Modify: `src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs` — generated update
+- Modify: `src/ClaudeDo.Installer/...` (seed call site — discover during Phase 1)
+
+**UI design assets (new):**
+- Create: `src/ClaudeDo.Ui/Design/Tokens.axaml`
+- Create: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
+- Create: `src/ClaudeDo.Ui/Assets/Fonts/InterTight-*.ttf` (Regular, Medium, SemiBold)
+- Create: `src/ClaudeDo.Ui/Assets/Fonts/JetBrainsMono-Regular.ttf`
+- Modify: `src/ClaudeDo.Ui/App.axaml` — merge Tokens + Styles
+- Modify: `src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` — embed font assets
+
+**Shell + Islands (new — replaces existing Views/ViewModels):**
+- Create: `src/ClaudeDo.Ui/Views/MainWindow.axaml` (replace) — chromeless, 3-column Grid
+- Create: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs` (replace `MainWindowViewModel`)
+- Create: `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml`
+- Create: `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs`
+- Create: `src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs`
+- Create: `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
+- Create: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
+- Create: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` (UserControl)
+- Create: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`
+- Create: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
+- Create: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
+- Create: `src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml`
+- Create: `src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml`
+- Create: `src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs`
+
+**Modals (new):**
+- Create: `src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml`
+- Create: `src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs`
+- Create: `src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml`
+- Create: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs`
+
+**To delete (after rewrite verified working):**
+- `src/ClaudeDo.Ui/Views/StatusBarView.axaml(.cs)`, `TaskListView.axaml(.cs)`, `TaskDetailView.axaml(.cs)`, `TaskEditorView.axaml(.cs)`, `ListEditorView.axaml(.cs)`
+- `src/ClaudeDo.Ui/ViewModels/StatusBarViewModel.cs`, `TaskListViewModel.cs`, `TaskDetailViewModel.cs`, `TaskItemViewModel.cs`, `MainWindowViewModel.cs`, `TaskEditorViewModel.cs`, `ListEditorViewModel.cs`, `ListItemViewModel.cs`, `SubtaskItemViewModel.cs`
+
+**Tests (new):**
+- Create: `tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs`
+- Create: `tests/ClaudeDo.Worker.Tests/UiSchema/DefaultListSeedTests.cs`
+
+---
+
+## Phase 1 — Schema and seed
+
+### Task 1: Add `IsStarred`, `IsMyDay`, `Notes` to `TaskEntity`
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs`
+- Modify: `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`
+- Test: `tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs` (new)
+
+- [ ] **Step 1: Write failing test** — `tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs`
+
+```csharp
+using ClaudeDo.Data;
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Xunit;
+using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
+
+namespace ClaudeDo.Worker.Tests.UiSchema;
+
+public class TaskEntityFlagsTests : IDisposable
+{
+ private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo-flags-{Guid.NewGuid():N}.db");
+
+ private ClaudeDoDbContext NewContext()
+ {
+ var opts = new DbContextOptionsBuilder()
+ .UseSqlite($"Data Source={_dbPath}")
+ .Options;
+ var ctx = new ClaudeDoDbContext(opts);
+ ctx.Database.EnsureCreated();
+ return ctx;
+ }
+
+ [Fact]
+ public async Task Persists_IsStarred_IsMyDay_And_Notes()
+ {
+ await using var ctx = NewContext();
+ var list = new ListEntity { Id = "l1", Name = "L", CreatedAt = DateTime.UtcNow };
+ ctx.Lists.Add(list);
+ ctx.Tasks.Add(new TaskEntity
+ {
+ Id = "t1", ListId = "l1", Title = "T", CreatedAt = DateTime.UtcNow,
+ IsStarred = true, IsMyDay = true, Notes = "hello"
+ });
+ await ctx.SaveChangesAsync();
+
+ await using var ctx2 = NewContext();
+ var loaded = await ctx2.Tasks.SingleAsync();
+ Assert.True(loaded.IsStarred);
+ Assert.True(loaded.IsMyDay);
+ Assert.Equal("hello", loaded.Notes);
+ }
+
+ public void Dispose()
+ {
+ if (File.Exists(_dbPath)) File.Delete(_dbPath);
+ }
+}
+```
+
+- [ ] **Step 2: Run test — verify fail**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~TaskEntityFlagsTests`
+Expected: build error — `TaskEntity` has no `IsStarred`/`IsMyDay`/`Notes`.
+
+- [ ] **Step 3: Add properties to `TaskEntity`** — append before navigation block in `src/ClaudeDo.Data/Models/TaskEntity.cs`:
+
+```csharp
+public bool IsStarred { get; set; }
+public bool IsMyDay { get; set; }
+public string? Notes { get; set; }
+```
+
+- [ ] **Step 4: Update `TaskEntityConfiguration`** — add column mappings inside `Configure(EntityTypeBuilder b)`:
+
+```csharp
+b.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
+b.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
+b.Property(t => t.Notes).HasColumnName("notes");
+```
+
+(Match existing snake_case style — verify by reading the file first.)
+
+- [ ] **Step 5: Run test — verify pass**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~TaskEntityFlagsTests`
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Models/TaskEntity.cs src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs
+git commit -m "feat(data): add IsStarred, IsMyDay, Notes to TaskEntity"
+```
+
+---
+
+### Task 2: Generate EF Core migration for new columns
+
+**Files:**
+- Create: `src/ClaudeDo.Data/Migrations/_AddTaskFlagsAndNotes.cs`
+- Modify: `src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs` (generated)
+
+- [ ] **Step 1: Generate migration**
+
+Run from repo root:
+```bash
+dotnet ef migrations add AddTaskFlagsAndNotes --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --startup-project src/ClaudeDo.Data/ClaudeDo.Data.csproj
+```
+
+If `dotnet-ef` is missing: `dotnet tool install --global dotnet-ef --version 8.*`.
+
+- [ ] **Step 2: Inspect the generated `Up`** — confirm three `AddColumn<>` calls for `is_starred`, `is_my_day`, `notes`. If column names mismatch, edit them by hand.
+
+- [ ] **Step 3: Apply migration in test (already covered by `EnsureCreated` in tests)**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~TaskEntityFlagsTests`
+Expected: still PASS.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Migrations/
+git commit -m "feat(data): migration for IsStarred/IsMyDay/Notes columns"
+```
+
+---
+
+### Task 3: Seed default Lists ("My Day", "Important", "Planned") on install
+
+**Files:**
+- Discover seed call site — search `src/ClaudeDo.Installer/` and `src/ClaudeDo.App/` for `EnsureCreated`, `Migrate`, or existing tag seeding ("agent" tag).
+- Modify: the discovered seeder OR create `src/ClaudeDo.Data/Seeding/DefaultListsSeeder.cs` if no central seeder exists.
+- Test: `tests/ClaudeDo.Worker.Tests/UiSchema/DefaultListSeedTests.cs` (new)
+
+- [ ] **Step 1: Locate seed site**
+
+Run:
+```bash
+grep -rn "agent.*manual\|GetOrCreateAsync\|EnsureCreated\|Migrate(" src/ClaudeDo.Installer src/ClaudeDo.App
+```
+
+If a central seeder exists, add list-seeding there. Otherwise create `DefaultListsSeeder`.
+
+- [ ] **Step 2: Write failing test** — `tests/ClaudeDo.Worker.Tests/UiSchema/DefaultListSeedTests.cs`
+
+```csharp
+using ClaudeDo.Data;
+using ClaudeDo.Data.Seeding; // adjust namespace if seeded elsewhere
+using Microsoft.EntityFrameworkCore;
+using Xunit;
+
+namespace ClaudeDo.Worker.Tests.UiSchema;
+
+public class DefaultListSeedTests : IDisposable
+{
+ private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo-seed-{Guid.NewGuid():N}.db");
+
+ private ClaudeDoDbContext NewContext()
+ {
+ var opts = new DbContextOptionsBuilder()
+ .UseSqlite($"Data Source={_dbPath}").Options;
+ var ctx = new ClaudeDoDbContext(opts);
+ ctx.Database.EnsureCreated();
+ return ctx;
+ }
+
+ [Fact]
+ public async Task Seeds_MyDay_Important_Planned_Lists_Idempotently()
+ {
+ await using (var ctx = NewContext())
+ {
+ await DefaultListsSeeder.SeedAsync(ctx);
+ await DefaultListsSeeder.SeedAsync(ctx); // idempotent
+ }
+
+ await using var verify = NewContext();
+ var names = verify.Lists.Select(l => l.Name).OrderBy(n => n).ToList();
+ Assert.Equal(new[] { "Important", "My Day", "Planned" }, names);
+ }
+
+ public void Dispose() { if (File.Exists(_dbPath)) File.Delete(_dbPath); }
+}
+```
+
+- [ ] **Step 3: Run test — verify fail**
+
+Run: `dotnet test ... --filter FullyQualifiedName~DefaultListSeedTests`
+Expected: build error.
+
+- [ ] **Step 4: Implement seeder** — create `src/ClaudeDo.Data/Seeding/DefaultListsSeeder.cs`:
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ClaudeDo.Data.Seeding;
+
+public static class DefaultListsSeeder
+{
+ private static readonly string[] Defaults = { "My Day", "Important", "Planned" };
+
+ public static async Task SeedAsync(ClaudeDoDbContext ctx, CancellationToken ct = default)
+ {
+ var existing = await ctx.Lists.Select(l => l.Name).ToListAsync(ct);
+ var now = DateTime.UtcNow;
+ foreach (var name in Defaults.Where(n => !existing.Contains(n)))
+ {
+ ctx.Lists.Add(new ListEntity
+ {
+ Id = Guid.NewGuid().ToString("N"),
+ Name = name,
+ CreatedAt = now,
+ });
+ }
+ await ctx.SaveChangesAsync(ct);
+ }
+}
+```
+
+- [ ] **Step 5: Wire seeder into install path** — call `DefaultListsSeeder.SeedAsync(ctx)` from the same code path that seeds the "agent"/"manual" tags. If none exists, call it once on app startup after `Database.Migrate()` in `ClaudeDo.App`.
+
+- [ ] **Step 6: Run test — verify pass**
+
+Run: `dotnet test ... --filter FullyQualifiedName~DefaultListSeedTests`
+Expected: PASS.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Seeding/ tests/ClaudeDo.Worker.Tests/UiSchema/DefaultListSeedTests.cs
+git commit -m "feat(data): seed default Lists (My Day, Important, Planned)"
+```
+
+---
+
+## Phase 2 — Design tokens, fonts, and shell wiring
+
+### Task 4: Embed Inter Tight + JetBrains Mono fonts
+
+**Files:**
+- Create: `src/ClaudeDo.Ui/Assets/Fonts/InterTight-Regular.ttf`, `InterTight-Medium.ttf`, `InterTight-SemiBold.ttf`
+- Create: `src/ClaudeDo.Ui/Assets/Fonts/JetBrainsMono-Regular.ttf`
+- Modify: `src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+
+- [ ] **Step 1: Download font files** — fetch from Google Fonts (`https://fonts.google.com/specimen/Inter+Tight`, `https://fonts.google.com/specimen/JetBrains+Mono`). Place TTFs in `src/ClaudeDo.Ui/Assets/Fonts/`. SIL OFL license — include `OFL.txt` next to each family.
+
+- [ ] **Step 2: Mark as Avalonia resources** — in `src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` add (or extend the existing `` for `AvaloniaResource`):
+
+```xml
+
+
+
+
+```
+
+- [ ] **Step 3: Build to confirm assets resolve**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: build succeeds.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Assets/Fonts/ src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
+git commit -m "feat(ui): embed Inter Tight and JetBrains Mono fonts"
+```
+
+---
+
+### Task 5: Port `Tokens.axaml` into the Ui project
+
+**Files:**
+- Create: `src/ClaudeDo.Ui/Design/Tokens.axaml`
+
+- [ ] **Step 1: Copy and adapt** — open `docs/UI Rewrite/design_handoff_claudedo/Tokens.axaml` and copy its `` content into `src/ClaudeDo.Ui/Design/Tokens.axaml`.
+
+- [ ] **Step 2: Replace placeholder font URIs** — anywhere the file references `avares://YourApp/...`, replace with `avares://ClaudeDo.Ui/Assets/Fonts/#Inter Tight` and `avares://ClaudeDo.Ui/Assets/Fonts/#JetBrains Mono`.
+
+- [ ] **Step 3: Verify required keys present** — open the file and confirm these brushes/values exist (per README §"Design tokens"):
+ - `VoidBrush #0A0E0C`, `DeepBrush #0D1311`, `SurfaceBrush #161D1A`, `Surface2Brush #1C2422`, `Surface3Brush #222B28`, `LineBrush #2A3330`
+ - `TextBrush #E4EBE4`, `TextDimBrush #9AA8A0`, `TextMuteBrush #6B7973`, `TextFaintBrush #4A5550`
+ - `MossBrush #7C9166`, `SageBrush #8B9D7A`, `PeatBrush #D4A574`, `BloodBrush #C87060`
+ - `IslandRadius 14`, `ModalRadius 12`, `ChipRadius 10`, `RowRadius 8`, `ButtonRadius 6`
+ - `MotionFast 0:0:0.12`, `MotionBase 0:0:0.18`, `MotionSlow 0:0:0.30`
+ - `IslandShadow`, `ModalShadow` BoxShadow values
+ - `SansFamily`, `MonoFamily` `FontFamily` values
+
+ If any are missing, add them inline using the values from the README §"Design tokens (reference)".
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Design/Tokens.axaml
+git commit -m "feat(ui): add design Tokens resource dictionary"
+```
+
+---
+
+### Task 6: Port `IslandStyles.axaml` into the Ui project
+
+**Files:**
+- Create: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
+
+- [ ] **Step 1: Copy** the full `` content from `docs/UI Rewrite/design_handoff_claudedo/IslandStyles.axaml` into `src/ClaudeDo.Ui/Design/IslandStyles.axaml`.
+
+- [ ] **Step 2: Confirm classed selectors present** — file must define styles for at least:
+ - `Border.island`, `Border.island-header`
+ - `Border.list-item`, `Border.list-item.active`
+ - `TextBox.search`
+ - `Border.task-row`, `Border.task-row.selected`
+ - `Ellipse.task-check`, `Ellipse.task-check.done`
+ - `Border.chip` and status variants `chip.running`, `chip.review`, `chip.error`, `chip.queued`, `chip.idle`
+ - `Border.agent-strip` and status variants
+ - `Border.terminal`, `TextBlock.log-sys`, `log-tool`, `log-claude`, `log-stdout`, `log-stderr`, `log-done`, `log-msg`
+ - `Button.icon-btn`
+
+ If any classed selector is missing, add a minimal one referencing the relevant token brush.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Design/IslandStyles.axaml
+git commit -m "feat(ui): add island control styles"
+```
+
+---
+
+### Task 7: Wire tokens and styles into `App.axaml`
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/App.axaml`
+
+- [ ] **Step 1: Read current App.axaml** to preserve any DI/region wiring:
+
+Run: open `src/ClaudeDo.Ui/App.axaml` and note its existing structure.
+
+- [ ] **Step 2: Edit** — ensure the `` root contains:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+(Preserve any existing `RequestedThemeVariant`, converter resources, etc. — only add the includes if not already present.)
+
+- [ ] **Step 3: Build**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: success.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/App.axaml
+git commit -m "feat(ui): merge Tokens and IslandStyles into App"
+```
+
+---
+
+## Phase 3 — Chromeless shell + three-island layout
+
+### Task 8: Replace `MainWindowViewModel` with `IslandsShellViewModel`
+
+**Files:**
+- Create: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
+
+- [ ] **Step 1: Write the VM** with three child VMs and a width-driven boolean for collapsing the Details column:
+
+```csharp
+using CommunityToolkit.Mvvm.ComponentModel;
+using ClaudeDo.Ui.ViewModels.Islands;
+
+namespace ClaudeDo.Ui.ViewModels;
+
+public sealed partial class IslandsShellViewModel : ViewModelBase
+{
+ public ListsIslandViewModel Lists { get; }
+ public TasksIslandViewModel Tasks { get; }
+ public DetailsIslandViewModel Details { get; }
+
+ [ObservableProperty]
+ private double _windowWidth = 1280;
+
+ public bool ShowDetails => WindowWidth >= 1100;
+ public bool ShowLists => WindowWidth >= 780;
+
+ partial void OnWindowWidthChanged(double value)
+ {
+ OnPropertyChanged(nameof(ShowDetails));
+ OnPropertyChanged(nameof(ShowLists));
+ }
+
+ public IslandsShellViewModel(
+ ListsIslandViewModel lists,
+ TasksIslandViewModel tasks,
+ DetailsIslandViewModel details)
+ {
+ Lists = lists; Tasks = tasks; Details = details;
+ Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
+ Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask);
+ }
+}
+```
+
+- [ ] **Step 2: Register in DI** — modify `src/ClaudeDo.App/Program.cs` (or wherever `MainWindowViewModel` is registered) to register the new VMs:
+
+```csharp
+services.AddSingleton();
+services.AddSingleton();
+services.AddSingleton();
+services.AddSingleton();
+```
+
+(Adjust to scoped/transient if existing patterns demand.)
+
+- [ ] **Step 3: Build** — `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` — fails until child VM stubs exist; that's fine, next task creates them.
+
+- [ ] **Step 4: Defer commit** until child VMs compile.
+
+---
+
+### Task 9: Stub child island VMs (compile-only skeletons)
+
+**Files (all new):**
+- `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
+
+- [ ] **Step 1: Write minimal stubs** so `IslandsShellViewModel` compiles:
+
+```csharp
+// ListsIslandViewModel.cs
+using CommunityToolkit.Mvvm.ComponentModel;
+namespace ClaudeDo.Ui.ViewModels.Islands;
+public sealed partial class ListsIslandViewModel : ViewModelBase
+{
+ public event EventHandler? SelectionChanged;
+ [ObservableProperty] private ListNavItemViewModel? _selectedList;
+ partial void OnSelectedListChanged(ListNavItemViewModel? value) =>
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+}
+
+// TasksIslandViewModel.cs
+using CommunityToolkit.Mvvm.ComponentModel;
+namespace ClaudeDo.Ui.ViewModels.Islands;
+public sealed partial class TasksIslandViewModel : ViewModelBase
+{
+ public event EventHandler? SelectionChanged;
+ [ObservableProperty] private TaskRowViewModel? _selectedTask;
+ public void LoadForList(ListNavItemViewModel? list) { /* Phase 5 */ }
+ partial void OnSelectedTaskChanged(TaskRowViewModel? value) =>
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+}
+
+// DetailsIslandViewModel.cs
+using CommunityToolkit.Mvvm.ComponentModel;
+namespace ClaudeDo.Ui.ViewModels.Islands;
+public sealed partial class DetailsIslandViewModel : ViewModelBase
+{
+ [ObservableProperty] private TaskRowViewModel? _task;
+ public void Bind(TaskRowViewModel? task) => Task = task;
+}
+```
+
+- [ ] **Step 2: Add minimal `ListNavItemViewModel` and `TaskRowViewModel`** so references resolve:
+
+```csharp
+// ListNavItemViewModel.cs
+using CommunityToolkit.Mvvm.ComponentModel;
+namespace ClaudeDo.Ui.ViewModels.Islands;
+public sealed partial class ListNavItemViewModel : ViewModelBase
+{
+ public required string Id { get; init; }
+ public required string Name { get; init; }
+ [ObservableProperty] private int _count;
+ [ObservableProperty] private bool _isActive;
+ public string? IconKey { get; init; }
+}
+
+// TaskRowViewModel.cs (placeholder — fleshed out in Phase 5)
+using CommunityToolkit.Mvvm.ComponentModel;
+namespace ClaudeDo.Ui.ViewModels.Islands;
+public sealed partial class TaskRowViewModel : ViewModelBase
+{
+ public required string Id { get; init; }
+ [ObservableProperty] private string _title = "";
+ [ObservableProperty] private bool _done;
+}
+```
+
+- [ ] **Step 3: Build** — `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` should succeed.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ src/ClaudeDo.App
+git commit -m "feat(ui): scaffold islands shell and child VMs"
+```
+
+---
+
+### Task 10: Replace `MainWindow.axaml` with chromeless three-column shell
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml`
+- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml.cs`
+
+- [ ] **Step 1: Rewrite `MainWindow.axaml`**:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Code-behind handlers** — `MainWindow.axaml.cs`:
+
+```csharp
+using Avalonia.Controls;
+using Avalonia.Input;
+using ClaudeDo.Ui.ViewModels;
+
+namespace ClaudeDo.Ui.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow() { InitializeComponent(); }
+
+ private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+ BeginMoveDrag(e);
+ }
+ private void OnMinimize(object? s, Avalonia.Interactivity.RoutedEventArgs e) =>
+ WindowState = WindowState.Minimized;
+ private void OnToggleMax(object? s, Avalonia.Interactivity.RoutedEventArgs e) =>
+ WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+ private void OnClose(object? s, Avalonia.Interactivity.RoutedEventArgs e) => Close();
+
+ protected override void OnSizeChanged(SizeChangedEventArgs e)
+ {
+ base.OnSizeChanged(e);
+ if (DataContext is IslandsShellViewModel vm) vm.WindowWidth = Bounds.Width;
+ }
+}
+```
+
+- [ ] **Step 3: Stub island views** so the AXAML compiles — create three minimal UserControls:
+
+```xml
+
+
+
+
+```
+
+(Identical pattern for `TasksIslandView.axaml` and `DetailsIslandView.axaml`. Each needs an empty `.axaml.cs` with `InitializeComponent()`.)
+
+- [ ] **Step 4: Run the app**
+
+Run: `dotnet run --project src/ClaudeDo.App/ClaudeDo.App.csproj`
+Expected: chromeless dark window, three islands with correct widths and 14px gaps; resize below 1100px collapses Details.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Views/MainWindow.axaml src/ClaudeDo.Ui/Views/MainWindow.axaml.cs src/ClaudeDo.Ui/Views/Islands/
+git commit -m "feat(ui): chromeless three-island shell"
+```
+
+---
+
+## Phase 4 — Lists Island
+
+### Task 11: Build `ListsIslandViewModel` (real)
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs`
+
+The Lists island shows: a search box, then nav items in this fixed order — `My Day`, `Important`, `Planned`, `Running` (virtual filter — all tasks with status Running), `Review` (virtual: status Done with worktree state Active), then one entry per real list (excluding the three seeded "smart" lists already shown above). Counts live-update from the Tasks repository.
+
+- [ ] **Step 1: Define `ListKind` enum and replace stub**:
+
+```csharp
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using ClaudeDo.Data.Repositories;
+
+namespace ClaudeDo.Ui.ViewModels.Islands;
+
+public enum ListKind { Smart, Virtual, User }
+
+public sealed partial class ListsIslandViewModel : ViewModelBase
+{
+ private readonly TaskRepository _tasks;
+ private readonly ListRepository _lists;
+
+ public event EventHandler? SelectionChanged;
+
+ public ObservableCollection Items { get; } = new();
+
+ [ObservableProperty] private string _searchText = "";
+ [ObservableProperty] private ListNavItemViewModel? _selectedList;
+
+ public ListsIslandViewModel(TaskRepository tasks, ListRepository lists)
+ {
+ _tasks = tasks; _lists = lists;
+ }
+
+ public async Task LoadAsync(CancellationToken ct = default)
+ {
+ Items.Clear();
+ Items.Add(new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" });
+ Items.Add(new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" });
+ Items.Add(new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" });
+ Items.Add(new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Pulse" });
+ Items.Add(new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" });
+
+ var seedNames = new HashSet(new[] { "My Day", "Important", "Planned" });
+ foreach (var l in await _lists.GetAllAsync(ct))
+ if (!seedNames.Contains(l.Name))
+ Items.Add(new ListNavItemViewModel { Id = $"user:{l.Id}", Name = l.Name, Kind = ListKind.User, IconKey = "Folder" });
+
+ await RefreshCountsAsync(ct);
+ SelectedList = Items.FirstOrDefault();
+ }
+
+ public async Task RefreshCountsAsync(CancellationToken ct = default)
+ {
+ // Implementation note: extend TaskRepository with count-by-kind helpers if missing.
+ // Leave counts at 0 for now; populate as Phase 5 wires task loads.
+ foreach (var i in Items) i.Count = 0;
+ await Task.CompletedTask;
+ }
+
+ partial void OnSelectedListChanged(ListNavItemViewModel? value)
+ {
+ foreach (var i in Items) i.IsActive = ReferenceEquals(i, value);
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
+```
+
+- [ ] **Step 2: Extend `ListNavItemViewModel`** — add `Kind` property:
+
+```csharp
+public required ListKind Kind { get; init; }
+```
+
+- [ ] **Step 3: Trigger `LoadAsync`** — in `IslandsShellViewModel` constructor, after wiring events:
+
+```csharp
+_ = Lists.LoadAsync();
+```
+
+- [ ] **Step 4: Build**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: success (assuming `ListRepository.GetAllAsync` exists — if not, use the existing query method or extend the repository).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
+git commit -m "feat(ui): ListsIslandViewModel with smart/virtual/user lists"
+```
+
+---
+
+### Task 12: Build `ListsIslandView`
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml`
+
+- [ ] **Step 1: Replace placeholder** with header + search + nav `ItemsControl`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Selection behavior** — wrap each row in a `Button` or attach a `Tapped` handler that sets `((ListsIslandViewModel)DataContext).SelectedList = item`. Quick path: replace `Border` with a `Button Classes="list-item-btn"` styled flat, `Command="{Binding $parent[ItemsControl].DataContext.SelectCommand}"` with `[RelayCommand] private void Select(ListNavItemViewModel item) => SelectedList = item;` added to the VM.
+
+- [ ] **Step 3: Run the app** — verify list items render, search box shows, click selection toggles `active` class.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
+git commit -m "feat(ui): Lists island view with search and nav items"
+```
+
+---
+
+## Phase 5 — Tasks Island
+
+### Task 13: `TaskRowViewModel` — full
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`
+- Test: `tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelTests.cs` (new)
+
+- [ ] **Step 1: Write the VM** based on README §"Task model (MVVM)":
+
+```csharp
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using ClaudeDo.Data.Models;
+using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
+
+namespace ClaudeDo.Ui.ViewModels.Islands;
+
+public sealed partial class TaskRowViewModel : ViewModelBase
+{
+ public required string Id { get; init; }
+ [ObservableProperty] private string _title = "";
+ [ObservableProperty] private string _listName = "";
+ [ObservableProperty] private bool _done;
+ [ObservableProperty] private bool _isStarred;
+ [ObservableProperty] private bool _isMyDay;
+ [ObservableProperty] private bool _isSelected;
+ [ObservableProperty] private TaskStatus _status;
+ [ObservableProperty] private string? _branch;
+ [ObservableProperty] private string? _diffStat;
+ [ObservableProperty] private string? _liveTail;
+
+ public string StatusChipClass => Status switch
+ {
+ TaskStatus.Running => "running",
+ TaskStatus.Failed => "error",
+ TaskStatus.Done => "review",
+ TaskStatus.Queued => "queued",
+ _ => "idle",
+ };
+
+ public static TaskRowViewModel FromEntity(TaskEntity t) => new()
+ {
+ Id = t.Id, Title = t.Title, ListName = t.List?.Name ?? "",
+ Done = t.Status == TaskStatus.Done,
+ IsStarred = t.IsStarred, IsMyDay = t.IsMyDay,
+ Status = t.Status,
+ Branch = t.Worktree?.BranchName,
+ DiffStat = t.Worktree?.DiffStat,
+ };
+}
+```
+
+- [ ] **Step 2: Write VM test** — `tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelTests.cs`:
+
+```csharp
+using ClaudeDo.Data.Models;
+using ClaudeDo.Ui.ViewModels.Islands;
+using Xunit;
+using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
+
+namespace ClaudeDo.Worker.Tests.UiVm;
+
+public class TaskRowViewModelTests
+{
+ [Theory]
+ [InlineData(TaskStatus.Running, "running")]
+ [InlineData(TaskStatus.Failed, "error")]
+ [InlineData(TaskStatus.Done, "review")]
+ [InlineData(TaskStatus.Queued, "queued")]
+ [InlineData(TaskStatus.Manual, "idle")]
+ public void StatusChipClass_Maps_Correctly(TaskStatus s, string expected)
+ {
+ var vm = new TaskRowViewModel { Id = "t" };
+ vm.Status = s;
+ Assert.Equal(expected, vm.StatusChipClass);
+ }
+}
+```
+
+- [ ] **Step 3: Run tests**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~TaskRowViewModelTests`
+Expected: PASS.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs tests/ClaudeDo.Worker.Tests/UiVm/
+git commit -m "feat(ui): TaskRowViewModel with status chip mapping"
+```
+
+---
+
+### Task 14: `TasksIslandViewModel` — load + add + filter
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
+
+- [ ] **Step 1: Implement** — load on list selection; add task on Enter; expose header counts:
+
+```csharp
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using ClaudeDo.Data.Models;
+using ClaudeDo.Data.Repositories;
+using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
+
+namespace ClaudeDo.Ui.ViewModels.Islands;
+
+public sealed partial class TasksIslandViewModel : ViewModelBase
+{
+ private readonly TaskRepository _tasks;
+ private ListNavItemViewModel? _currentList;
+
+ public event EventHandler? SelectionChanged;
+
+ public ObservableCollection Items { get; } = new();
+
+ [ObservableProperty] private string _newTaskTitle = "";
+ [ObservableProperty] private TaskRowViewModel? _selectedTask;
+ [ObservableProperty] private string _headerTitle = "";
+ [ObservableProperty] private string _headerEyebrow = "";
+ [ObservableProperty] private string _subtitle = "";
+
+ public TasksIslandViewModel(TaskRepository tasks) { _tasks = tasks; }
+
+ public async void LoadForList(ListNavItemViewModel? list)
+ {
+ _currentList = list;
+ Items.Clear();
+ if (list is null) return;
+
+ HeaderTitle = list.Name;
+ HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd").ToUpperInvariant();
+
+ var all = await _tasks.GetAllAsync();
+ IEnumerable filtered = list.Kind switch
+ {
+ ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay),
+ ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred),
+ ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null),
+ ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running),
+ ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active),
+ ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
+ _ => Enumerable.Empty(),
+ };
+
+ foreach (var t in filtered) Items.Add(TaskRowViewModel.FromEntity(t));
+ UpdateSubtitle();
+ }
+
+ private void UpdateSubtitle()
+ {
+ var open = Items.Count(i => !i.Done);
+ var running = Items.Count(i => i.Status == TaskStatus.Running);
+ var review = Items.Count(i => i.Status == TaskStatus.Done && !i.Done /* TODO refine */);
+ Subtitle = $"{open} open · {running} running · {review} in review";
+ }
+
+ [RelayCommand]
+ private async Task AddAsync()
+ {
+ if (string.IsNullOrWhiteSpace(NewTaskTitle) || _currentList?.Kind != ListKind.User) return;
+ var listId = _currentList.Id["user:".Length..];
+ var entity = new TaskEntity
+ {
+ Id = Guid.NewGuid().ToString("N"),
+ ListId = listId,
+ Title = NewTaskTitle.Trim(),
+ CreatedAt = DateTime.UtcNow,
+ };
+ await _tasks.CreateAsync(entity);
+ Items.Insert(0, TaskRowViewModel.FromEntity(entity));
+ NewTaskTitle = "";
+ UpdateSubtitle();
+ }
+
+ partial void OnSelectedTaskChanged(TaskRowViewModel? value)
+ {
+ foreach (var i in Items) i.IsSelected = ReferenceEquals(i, value);
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
+```
+
+- [ ] **Step 2: Verify repository methods exist** — `TaskRepository.GetAllAsync` and `CreateAsync`. If names differ, adjust.
+
+- [ ] **Step 3: Build**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: success.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
+git commit -m "feat(ui): TasksIslandViewModel with smart/virtual/user filtering"
+```
+
+---
+
+### Task 15: `TasksIslandView` + `TaskRowView`
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
+- Create: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml(.cs)`
+
+- [ ] **Step 1: `TaskRowView.axaml`** — extracted UserControl per README §"Task list":
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Add converters** — `NotNullToBool`, `StrikeIfTrue`, `EqStatusRunning` (and one per status). Place under `src/ClaudeDo.Ui/Converters/`. Register them as resources in `App.axaml`. (Each converter is ~10 lines; copy the pattern from `StatusColorConverter.cs`.)
+
+- [ ] **Step 3: Add `[RelayCommand]` ToggleDone / ToggleStar** to `TasksIslandViewModel`:
+
+```csharp
+[RelayCommand] private async Task ToggleDoneAsync(TaskRowViewModel row)
+{
+ row.Done = !row.Done;
+ var entity = await _tasks.GetByIdAsync(row.Id);
+ if (entity != null)
+ {
+ entity.Status = row.Done ? TaskStatus.Done : TaskStatus.Manual;
+ await _tasks.UpdateAsync(entity);
+ }
+ UpdateSubtitle();
+}
+[RelayCommand] private async Task ToggleStarAsync(TaskRowViewModel row)
+{
+ row.IsStarred = !row.IsStarred;
+ var entity = await _tasks.GetByIdAsync(row.Id);
+ if (entity != null) { entity.IsStarred = row.IsStarred; await _tasks.UpdateAsync(entity); }
+}
+```
+
+- [ ] **Step 4: `TasksIslandView.axaml`** — header, add-task box, scrollable list:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 5: Selection handler** — add `Tapped` on `TaskRowView` setting `((TasksIslandViewModel)DataContext.parent).SelectedTask = vm`. Or use a `[RelayCommand] private void Select(TaskRowViewModel row) => SelectedTask = row;` and bind via `InputElement.Tapped` behavior.
+
+- [ ] **Step 6: Run app** — verify rows render with chips, add-task on Enter prepends a row, selection updates Details.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Views/Islands/ src/ClaudeDo.Ui/Converters/ src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs src/ClaudeDo.Ui/App.axaml
+git commit -m "feat(ui): tasks island with rows, chips, add-task, selection"
+```
+
+---
+
+## Phase 6 — Details Island
+
+### Task 16: `DetailsIslandViewModel` — bind selected task + agent state
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
+- Create: `src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs`
+
+- [ ] **Step 1: `LogLineViewModel`**:
+
+```csharp
+namespace ClaudeDo.Ui.ViewModels.Islands;
+
+public enum LogKind { Sys, Tool, Claude, Stdout, Stderr, Done, Msg }
+
+public sealed class LogLineViewModel
+{
+ public required LogKind Kind { get; init; }
+ public required string Text { get; init; }
+ public string ClassName => Kind switch
+ {
+ LogKind.Sys => "log-sys", LogKind.Tool => "log-tool", LogKind.Claude => "log-claude",
+ LogKind.Stdout => "log-stdout", LogKind.Stderr => "log-stderr",
+ LogKind.Done => "log-done", LogKind.Msg => "log-msg",
+ };
+}
+```
+
+- [ ] **Step 2: `DetailsIslandViewModel`** — bind everything the AgentStrip + Terminal + Subtasks + Notes need:
+
+```csharp
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using ClaudeDo.Data.Repositories;
+using ClaudeDo.Ui.Services;
+
+namespace ClaudeDo.Ui.ViewModels.Islands;
+
+public sealed partial class DetailsIslandViewModel : ViewModelBase
+{
+ private readonly TaskRepository _tasks;
+ private readonly SubtaskRepository _subtasks;
+ private readonly WorkerClient _worker;
+
+ [ObservableProperty] private TaskRowViewModel? _task;
+ [ObservableProperty] private string _editableTitle = "";
+ [ObservableProperty] private string _notes = "";
+ [ObservableProperty] private string _promptInput = "";
+ [ObservableProperty] private string _agentStatusLabel = "Idle";
+ [ObservableProperty] private string? _model;
+ [ObservableProperty] private string? _worktreePath;
+ [ObservableProperty] private string? _branchLine;
+ [ObservableProperty] private int _turns;
+ [ObservableProperty] private int _tokens;
+ [ObservableProperty] private TimeSpan _elapsed;
+
+ public ObservableCollection Log { get; } = new();
+ public ObservableCollection Subtasks { get; } = new();
+
+ public DetailsIslandViewModel(TaskRepository tasks, SubtaskRepository subtasks, WorkerClient worker)
+ {
+ _tasks = tasks; _subtasks = subtasks; _worker = worker;
+ }
+
+ public async void Bind(TaskRowViewModel? row)
+ {
+ Task = row;
+ Log.Clear(); Subtasks.Clear();
+ if (row == null) return;
+ var entity = await _tasks.GetByIdAsync(row.Id);
+ if (entity == null) return;
+ EditableTitle = entity.Title;
+ Notes = entity.Notes ?? "";
+ Model = entity.Model;
+ WorktreePath = entity.Worktree?.Path;
+ BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
+ AgentStatusLabel = entity.Status.ToString();
+ foreach (var s in await _subtasks.GetForTaskAsync(row.Id))
+ Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed });
+ }
+
+ [RelayCommand] private async Task SendPromptAsync()
+ {
+ if (string.IsNullOrWhiteSpace(PromptInput) || Task == null) return;
+ Log.Add(new LogLineViewModel { Kind = LogKind.Msg, Text = $"[you] {PromptInput}" });
+ await _worker.SendPromptAsync(Task.Id, PromptInput); // adjust to actual API
+ PromptInput = "";
+ }
+
+ [RelayCommand] private async Task ApproveMergeAsync() { /* call worker merge */ await Task.CompletedTask; }
+ [RelayCommand] private async Task StopAsync() { /* call worker stop */ await Task.CompletedTask; }
+}
+
+public sealed partial class SubtaskRowViewModel : ViewModelBase
+{
+ public required string Id { get; init; }
+ [ObservableProperty] private string _title = "";
+ [ObservableProperty] private bool _done;
+}
+```
+
+- [ ] **Step 3: Wire SignalR live log** — subscribe to `WorkerClient` log events in `Bind` and append to `Log`. Use the existing event pattern; verify the API surface in `WorkerClient.cs` first.
+
+- [ ] **Step 4: Build + commit**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs
+git commit -m "feat(ui): DetailsIslandViewModel with agent state and log"
+```
+
+---
+
+### Task 17: Build `DetailsIslandView` + `AgentStripView` + `SessionTerminalView`
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
+- Create: `src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml(.cs)`
+- Create: `src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml(.cs)`
+
+- [ ] **Step 1: `AgentStripView.axaml`** — three rows per README §"Agent strip":
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: `SessionTerminalView.axaml`** — log + prompt input:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+In code-behind: subscribe to `Log.CollectionChanged` and call `LogScroll.ScrollToEnd()`.
+
+- [ ] **Step 3: `DetailsIslandView.axaml`** — assemble sections per README §"Details island":
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 4: Run + visually verify**
+
+Run: `dotnet run --project src/ClaudeDo.App/ClaudeDo.App.csproj`
+Expected: clicking a task populates Details with title, agent strip, terminal area, notes.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml.cs src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml.cs
+git commit -m "feat(ui): details island with agent strip, terminal, subtasks, notes"
+```
+
+---
+
+## Phase 7 — Modals
+
+### Task 18: Diff modal
+
+**Files:**
+- Create: `src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml(.cs)`
+- Create: `src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs`
+
+- [ ] **Step 1: ViewModel** — exposes `Files: ObservableCollection`, `SelectedFile`, each file has `Hunks: List` with `Kind: Add|Del|Ctx`, `OldNo`, `NewNo`, `Text`. Populate from `git diff` output (extend `GitService` with a parser if missing — initial scope: stub data).
+
+- [ ] **Step 2: View** — borderless `Window` (`SystemDecorations="None"`, `WindowStartupLocation="CenterOwner"`, `Background="Transparent"`). Inner `Border Classes="modal"` with `Grid ColumnDefinitions="240,*"`: left `ListBox` of files (each row showing name + `+N −N` chips), right `ItemsControl` of hunk lines styled by kind (`del` red-tinted, `add` green-tinted, `ctx` neutral) — per README §"Diff modal".
+
+- [ ] **Step 3: Wire button** — `AgentStripView` "Open diff" `Button.Click` opens the window with the current task's diff.
+
+- [ ] **Step 4: Esc closes** — `KeyBindings` in modal: ``.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml.cs src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
+git commit -m "feat(ui): diff modal with file sidebar and tinted hunks"
+```
+
+---
+
+### Task 19: Worktree modal
+
+**Files:**
+- Create: `src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml(.cs)`
+- Create: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs`
+
+- [ ] **Step 1: ViewModel** — `TreeNodes: ObservableCollection` (recursive, with `Name`, `Status: 'M'|'A'|null`, `Children`).
+
+- [ ] **Step 2: View** — modal `Window` (same chrome pattern as diff modal). Body: `TreeView` bound to `TreeNodes`, each node `StackPanel Horizontal` with name + status badge (`M` peat-tinted, `A` moss-tinted).
+
+- [ ] **Step 3: Populate** — parse `git status --porcelain` from worktree path; build tree by splitting paths on `/`.
+
+- [ ] **Step 4: Wire button + Esc** as in Task 18.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml.cs src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs
+git commit -m "feat(ui): worktree modal with tree view and M/A badges"
+```
+
+---
+
+## Phase 8 — Animations and keyboard shortcuts
+
+### Task 20: Animations
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
+
+- [ ] **Step 1: Task-row hover** — add `Transitions` on `Border.task-row`:
+
+```xml
+
+```
+
+- [ ] **Step 2: Running pulse** — add to `Ellipse.status-pulse`:
+
+```xml
+
+```
+
+- [ ] **Step 3: Task-row add** — animate inserted row opacity+Y. In `TaskRowView.axaml.cs`, on `AttachedToVisualTree` start a 0.3s `Animation` on opacity (0→1) and `TranslateTransform.Y` (8→0).
+
+- [ ] **Step 4: Modal scale-in** — modal root `Border Classes="modal"` gets `RenderTransform` with `ScaleTransform`; on open animate 0.18s from `ScaleX/Y=0.98, Opacity=0` to `1.0, 1.0`.
+
+- [ ] **Step 5: Visual verify** — run app, observe pulse, hover, modal animation.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs src/ClaudeDo.Ui/Views/Modals/
+git commit -m "feat(ui): pulse, hover, modal, and row-add animations"
+```
+
+---
+
+### Task 21: Keyboard shortcuts
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml(.cs)`
+
+- [ ] **Step 1: Window-level KeyBindings**:
+
+```xml
+
+
+
+
+
+```
+
+(`/` is the `OemQuestion` gesture without shift on US layouts; on DE layout you may need `KeyDown` handler instead — verify on the target machine.)
+
+- [ ] **Step 2: Implement commands on `IslandsShellViewModel`**:
+
+```csharp
+[RelayCommand] private void FocusSearch() { /* raise event consumed by ListsIslandView code-behind to call Focus() on the search box */ }
+[RelayCommand] private void FocusAddTask() { /* same pattern for tasks add box */ }
+[RelayCommand] private async Task ToggleSelectedDoneAsync()
+{
+ if (Tasks.SelectedTask is { } row)
+ await Tasks.ToggleDoneCommand.ExecuteAsync(row);
+}
+```
+
+- [ ] **Step 3: Esc closes modals** — already in modal `KeyBindings` from Tasks 18–19.
+
+- [ ] **Step 4: Visual verify**
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Views/MainWindow.axaml src/ClaudeDo.Ui/Views/MainWindow.axaml.cs src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
+git commit -m "feat(ui): keyboard shortcuts (/ Ctrl+N Space Esc)"
+```
+
+---
+
+## Phase 9 — Cleanup
+
+### Task 22: Remove obsolete views and viewmodels
+
+**Files (delete):**
+- `src/ClaudeDo.Ui/Views/StatusBarView.axaml(.cs)`
+- `src/ClaudeDo.Ui/Views/TaskListView.axaml(.cs)`
+- `src/ClaudeDo.Ui/Views/TaskDetailView.axaml(.cs)`
+- `src/ClaudeDo.Ui/Views/TaskEditorView.axaml(.cs)`
+- `src/ClaudeDo.Ui/Views/ListEditorView.axaml(.cs)`
+- `src/ClaudeDo.Ui/ViewModels/StatusBarViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs` (only if unused)
+- `src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/SubtaskItemViewModel.cs`
+- `src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs`
+
+- [ ] **Step 1: Verify nothing references them**
+
+Run: `grep -rn "MainWindowViewModel\|TaskListViewModel\|TaskDetailViewModel\|StatusBarViewModel\|ListEditorViewModel\|TaskEditorViewModel\|TaskItemViewModel\|ListItemViewModel\|SubtaskItemViewModel" src/`
+
+Expected: no hits outside the files being deleted (and DI registration sites you've already migrated in Task 8).
+
+- [ ] **Step 2: Delete** with `git rm` on every listed file.
+
+- [ ] **Step 3: Build + run smoke test**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj && dotnet run --project src/ClaudeDo.App/ClaudeDo.App.csproj`
+Expected: build succeeds, app launches with new shell, all interactions still work.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git commit -m "chore(ui): remove obsolete pre-rewrite views and viewmodels"
+```
+
+---
+
+## Acceptance — final pass
+
+Once Task 22 is committed, walk the README's Acceptance checklist (lines 236–249) interactively:
+
+- [ ] Three-island layout, correct spacing, collapse <1100px
+- [ ] Lists sidebar with icons, counts, search, active state
+- [ ] Task rows with checkbox, title, meta chips, star
+- [ ] Selection updates Details
+- [ ] Agent strip shows status, model, turns, tokens, elapsed, worktree, branch
+- [ ] Session terminal renders all log kinds with distinct colors, auto-scrolls, accepts prompt input
+- [ ] Diff modal with file sidebar and tinted lines
+- [ ] Worktree modal with M/A badges
+- [ ] Status chip tints match
+- [ ] Fonts: Inter Tight + JetBrains Mono applied
+- [ ] Motion: row add/toggle, pulse, modal open, hover transitions
+- [ ] Keyboard shortcuts wired
+
+If any item is missing or visually off, file a follow-up task — do not silently skip.
diff --git a/docs/superpowers/plans/2026-04-21-stream-formatter-rewrite.md b/docs/superpowers/plans/2026-04-21-stream-formatter-rewrite.md
new file mode 100644
index 0000000..c0c9e7e
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-21-stream-formatter-rewrite.md
@@ -0,0 +1,614 @@
+# Stream Formatter Rewrite — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Rewrite `StreamLineFormatter` so Claude CLI stream-json messages (system/init, assistant text, assistant tool_use, user tool_result, result) render as compact readable lines in the Details pane.
+
+**Architecture:** Single-file rewrite of `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs`. Public API (`FormatLine(string)` / `FormatFile(string)` / `Trim`) and constants unchanged. Internal dispatch switches on top-level `type`; per-type helpers return one or more `\n`-terminated display lines, concatenated into the return string.
+
+**Tech Stack:** C# 12, .NET 8, `System.Text.Json` (already in use).
+
+**Spec:** `docs/superpowers/specs/2026-04-21-stream-formatter-rewrite-design.md`
+
+**Testing:** Skipped per user decision; verification is a manual build after each task and a final end-to-end run of a real task.
+
+**Build command (repo uses csproj builds, not slnx, on .NET 8):**
+```bash
+dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
+```
+
+---
+
+## File Structure
+
+- **Modify:** `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` — complete rewrite of parsing logic; keeps public class surface.
+
+No other files change. `DetailsIslandViewModel` and the Worker pipeline are unaffected.
+
+---
+
+## Task 1: Replace the dispatch skeleton
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs`
+
+Swap the old top-level `switch` for one that names every supported message type. Every branch returns `null` for now except `result` and `api_retry`, which keep their existing behavior. This gives us a clean compile before we fill in each branch.
+
+- [ ] **Step 1: Overwrite the file with the new skeleton**
+
+Replace the entire contents of `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` with:
+
+```csharp
+using System.Text;
+using System.Text.Json;
+
+namespace ClaudeDo.Ui.Helpers;
+
+public class StreamLineFormatter
+{
+ private const int MaxLength = 50_000;
+ private const int MaxArgChars = 120;
+
+ public string? FormatLine(string line)
+ {
+ JsonDocument doc;
+ try
+ {
+ doc = JsonDocument.Parse(line);
+ }
+ catch (JsonException)
+ {
+ return line;
+ }
+
+ using (doc)
+ {
+ var root = doc.RootElement;
+ if (root.ValueKind != JsonValueKind.Object)
+ return null;
+ if (!root.TryGetProperty("type", out var typeProp))
+ return null;
+
+ return typeProp.GetString() switch
+ {
+ "system" => FormatSystem(root),
+ "assistant" => FormatAssistant(root),
+ "user" => FormatUser(root),
+ "result" => FormatResult(root),
+ _ => null,
+ };
+ }
+ }
+
+ private static string? FormatSystem(JsonElement root)
+ {
+ if (!root.TryGetProperty("subtype", out var subtypeProp))
+ return null;
+ return subtypeProp.GetString() switch
+ {
+ "api_retry" => "[Retrying API call...]\n",
+ _ => null,
+ };
+ }
+
+ private static string? FormatAssistant(JsonElement root) => null;
+
+ private static string? FormatUser(JsonElement root) => null;
+
+ private static string? FormatResult(JsonElement root)
+ {
+ if (root.TryGetProperty("result", out var resultProp))
+ return $"\n--- Result ---\n{resultProp.GetString()}\n";
+ return null;
+ }
+
+ public string FormatFile(string filePath)
+ {
+ var sb = new StringBuilder();
+ foreach (var line in File.ReadLines(filePath))
+ {
+ var formatted = FormatLine(line);
+ if (formatted is not null)
+ sb.Append(formatted);
+ }
+ return Trim(sb.ToString());
+ }
+
+ public static string Trim(string text)
+ {
+ if (text.Length <= MaxLength) return text;
+ var trimStart = text.Length - MaxLength;
+ var newlineAfter = text.IndexOf('\n', trimStart);
+ if (newlineAfter >= 0 && newlineAfter < trimStart + 200)
+ trimStart = newlineAfter + 1;
+ return text[trimStart..];
+ }
+}
+```
+
+- [ ] **Step 2: Build**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: build succeeds, 0 errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs
+git commit -m "refactor(ui): skeleton dispatch for StreamLineFormatter rewrite"
+```
+
+---
+
+## Task 2: Add system/init formatting
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs`
+
+Emit `[session · ]` when the CLI announces the session at startup.
+
+- [ ] **Step 1: Replace the `FormatSystem` method**
+
+Find:
+
+```csharp
+ private static string? FormatSystem(JsonElement root)
+ {
+ if (!root.TryGetProperty("subtype", out var subtypeProp))
+ return null;
+ return subtypeProp.GetString() switch
+ {
+ "api_retry" => "[Retrying API call...]\n",
+ _ => null,
+ };
+ }
+```
+
+Replace with:
+
+```csharp
+ private static string? FormatSystem(JsonElement root)
+ {
+ if (!root.TryGetProperty("subtype", out var subtypeProp))
+ return null;
+
+ var subtype = subtypeProp.GetString();
+ switch (subtype)
+ {
+ case "api_retry":
+ return "[Retrying API call...]\n";
+
+ case "init":
+ {
+ var sessionId = root.TryGetProperty("session_id", out var sid)
+ ? sid.GetString() : null;
+ var model = root.TryGetProperty("model", out var m)
+ ? m.GetString() : null;
+
+ var shortId = sessionId is { Length: >= 8 }
+ ? sessionId[..8]
+ : sessionId ?? "?";
+ var modelPart = string.IsNullOrEmpty(model) ? "" : $" · {model}";
+ return $"[session {shortId}{modelPart}]\n";
+ }
+
+ default:
+ return null;
+ }
+ }
+```
+
+- [ ] **Step 2: Build**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: build succeeds, 0 errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs
+git commit -m "feat(ui): format system init message in StreamLineFormatter"
+```
+
+---
+
+## Task 3: Add assistant text + thinking filter
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs`
+
+Iterate `message.content[]`. Emit each `text` block verbatim with a trailing `\n`; skip `thinking`. Leave `tool_use` for the next task (still returns nothing for now).
+
+- [ ] **Step 1: Replace the `FormatAssistant` method**
+
+Find:
+
+```csharp
+ private static string? FormatAssistant(JsonElement root) => null;
+```
+
+Replace with:
+
+```csharp
+ private static string? FormatAssistant(JsonElement root)
+ {
+ if (!TryGetContentArray(root, out var content))
+ return null;
+
+ var sb = new StringBuilder();
+ foreach (var block in content.EnumerateArray())
+ {
+ if (block.ValueKind != JsonValueKind.Object) continue;
+ if (!block.TryGetProperty("type", out var blockTypeProp)) continue;
+
+ switch (blockTypeProp.GetString())
+ {
+ case "text":
+ if (block.TryGetProperty("text", out var textProp))
+ {
+ var text = textProp.GetString();
+ if (!string.IsNullOrEmpty(text))
+ {
+ sb.Append(text);
+ if (!text.EndsWith('\n')) sb.Append('\n');
+ }
+ }
+ break;
+
+ case "tool_use":
+ // Filled in by a later task.
+ break;
+
+ case "thinking":
+ default:
+ // Filtered.
+ break;
+ }
+ }
+
+ return sb.Length == 0 ? null : sb.ToString();
+ }
+
+ private static bool TryGetContentArray(JsonElement root, out JsonElement content)
+ {
+ content = default;
+ if (!root.TryGetProperty("message", out var message)) return false;
+ if (message.ValueKind != JsonValueKind.Object) return false;
+ if (!message.TryGetProperty("content", out var c)) return false;
+ if (c.ValueKind != JsonValueKind.Array) return false;
+ content = c;
+ return true;
+ }
+```
+
+- [ ] **Step 2: Build**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: build succeeds, 0 errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs
+git commit -m "feat(ui): render assistant text blocks, skip thinking"
+```
+
+---
+
+## Task 4: Add tool_use block formatting
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs`
+
+Fill in the `tool_use` case inside `FormatAssistant`. Per-tool label/arg logic lives in a dedicated helper.
+
+- [ ] **Step 1: Replace the `tool_use` case body**
+
+Find:
+
+```csharp
+ case "tool_use":
+ // Filled in by a later task.
+ break;
+```
+
+Replace with:
+
+```csharp
+ case "tool_use":
+ sb.Append(FormatToolUse(block));
+ sb.Append('\n');
+ break;
+```
+
+- [ ] **Step 2: Add helper methods at the end of the class (before `FormatFile`)**
+
+Insert just above the `public string FormatFile(string filePath)` method:
+
+```csharp
+ private static string FormatToolUse(JsonElement block)
+ {
+ var name = block.TryGetProperty("name", out var nameProp)
+ ? nameProp.GetString() ?? "?"
+ : "?";
+
+ JsonElement input = default;
+ var hasInput = block.TryGetProperty("input", out input)
+ && input.ValueKind == JsonValueKind.Object;
+
+ var label = name;
+ if (hasInput && (name == "Task" || name == "Agent"))
+ {
+ var sub = GetStr(input, "subagent_type");
+ if (!string.IsNullOrEmpty(sub))
+ label = $"{name}: {sub}";
+ }
+
+ string? arg = hasInput ? BuildToolArg(name, input) : null;
+
+ return string.IsNullOrEmpty(arg)
+ ? $"[{label}]"
+ : $"[{label}] {arg}";
+ }
+
+ private static string? BuildToolArg(string toolName, JsonElement input)
+ {
+ switch (toolName)
+ {
+ case "Read":
+ case "Write":
+ case "Edit":
+ case "NotebookEdit":
+ return Basename(GetStr(input, "file_path"));
+
+ case "Bash":
+ case "PowerShell":
+ {
+ var cmd = GetStr(input, "command");
+ return string.IsNullOrEmpty(cmd) ? null : "$ " + Truncate(cmd, MaxArgChars);
+ }
+
+ case "Grep":
+ {
+ var p = GetStr(input, "pattern");
+ return string.IsNullOrEmpty(p) ? null : $"\"{Truncate(p, MaxArgChars)}\"";
+ }
+
+ case "Glob":
+ return Truncate(GetStr(input, "pattern"), MaxArgChars);
+
+ case "Task":
+ case "Agent":
+ return Truncate(GetStr(input, "description"), MaxArgChars);
+
+ case "WebFetch":
+ return GetStr(input, "url");
+
+ case "WebSearch":
+ {
+ var q = GetStr(input, "query");
+ return string.IsNullOrEmpty(q) ? null : $"\"{Truncate(q, MaxArgChars)}\"";
+ }
+
+ case "TodoWrite":
+ return null;
+
+ default:
+ return null;
+ }
+ }
+
+ private static string? GetStr(JsonElement obj, string name)
+ => obj.TryGetProperty(name, out var p) && p.ValueKind == JsonValueKind.String
+ ? p.GetString()
+ : null;
+
+ private static string Basename(string? path)
+ {
+ if (string.IsNullOrEmpty(path)) return "";
+ var i = path.LastIndexOfAny(new[] { '/', '\\' });
+ return i < 0 ? path : path[(i + 1)..];
+ }
+
+ private static string Truncate(string? s, int max)
+ {
+ if (string.IsNullOrEmpty(s)) return "";
+ return s.Length <= max ? s : s[..max] + "…";
+ }
+```
+
+- [ ] **Step 3: Build**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: build succeeds, 0 errors.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs
+git commit -m "feat(ui): render assistant tool_use blocks with per-tool args"
+```
+
+---
+
+## Task 5: Add user tool_result formatting
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs`
+
+Iterate `message.content[]` for `tool_result` blocks and emit `→ ` lines per the spec rules.
+
+- [ ] **Step 1: Replace the `FormatUser` method**
+
+Find:
+
+```csharp
+ private static string? FormatUser(JsonElement root) => null;
+```
+
+Replace with:
+
+```csharp
+ private static string? FormatUser(JsonElement root)
+ {
+ if (!TryGetContentArray(root, out var content))
+ return null;
+
+ var sb = new StringBuilder();
+ foreach (var block in content.EnumerateArray())
+ {
+ if (block.ValueKind != JsonValueKind.Object) continue;
+ if (!block.TryGetProperty("type", out var blockTypeProp)) continue;
+ if (blockTypeProp.GetString() != "tool_result") continue;
+
+ var summary = BuildToolResultSummary(root, block);
+ if (!string.IsNullOrEmpty(summary))
+ {
+ sb.Append("→ ");
+ sb.Append(summary);
+ sb.Append('\n');
+ }
+ }
+
+ return sb.Length == 0 ? null : sb.ToString();
+ }
+
+ private static string BuildToolResultSummary(JsonElement root, JsonElement block)
+ {
+ var isError = block.TryGetProperty("is_error", out var errProp)
+ && errProp.ValueKind == JsonValueKind.True;
+
+ var contentText = ResolveContentText(block);
+
+ if (isError)
+ {
+ var msg = FirstNonEmptyLine(contentText);
+ return string.IsNullOrEmpty(msg) ? "error" : $"error: {Truncate(msg, MaxArgChars)}";
+ }
+
+ // tool_use_result.file.numLines shortcut for Read-style results
+ if (root.TryGetProperty("tool_use_result", out var tur)
+ && tur.ValueKind == JsonValueKind.Object
+ && tur.TryGetProperty("file", out var file)
+ && file.ValueKind == JsonValueKind.Object
+ && file.TryGetProperty("numLines", out var nl)
+ && nl.ValueKind == JsonValueKind.Number
+ && nl.TryGetInt32(out var lines))
+ {
+ return $"{lines} lines";
+ }
+
+ if (string.IsNullOrWhiteSpace(contentText))
+ return "ok";
+
+ var first = FirstNonEmptyLine(contentText);
+ return Truncate(first, MaxArgChars);
+ }
+
+ private static string ResolveContentText(JsonElement block)
+ {
+ if (!block.TryGetProperty("content", out var c))
+ return "";
+
+ if (c.ValueKind == JsonValueKind.String)
+ return c.GetString() ?? "";
+
+ if (c.ValueKind == JsonValueKind.Array)
+ {
+ var sb = new StringBuilder();
+ foreach (var part in c.EnumerateArray())
+ {
+ if (part.ValueKind != JsonValueKind.Object) continue;
+ if (!part.TryGetProperty("type", out var pt)) continue;
+ if (pt.GetString() != "text") continue;
+ if (part.TryGetProperty("text", out var t) && t.ValueKind == JsonValueKind.String)
+ {
+ if (sb.Length > 0) sb.Append('\n');
+ sb.Append(t.GetString());
+ }
+ }
+ return sb.ToString();
+ }
+
+ return "";
+ }
+
+ private static string FirstNonEmptyLine(string s)
+ {
+ if (string.IsNullOrEmpty(s)) return "";
+ foreach (var raw in s.Split('\n'))
+ {
+ var line = raw.TrimEnd('\r').Trim();
+ if (line.Length > 0) return line;
+ }
+ return "";
+ }
+```
+
+- [ ] **Step 2: Build**
+
+Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
+Expected: build succeeds, 0 errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs
+git commit -m "feat(ui): render user tool_result blocks as one-line summaries"
+```
+
+---
+
+## Task 6: Manual end-to-end verification
+
+**Files:** none (verification only).
+
+- [ ] **Step 1: Build everything the app needs**
+
+Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` and `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
+Expected: both succeed, 0 errors.
+
+- [ ] **Step 2: Start the Worker in one terminal**
+
+Run: `dotnet run --project src/ClaudeDo.Worker`
+Expected: SignalR hub bound to `127.0.0.1:47821`, no crash.
+
+- [ ] **Step 3: Start the App in another terminal**
+
+Run: `dotnet run --project src/ClaudeDo.App`
+Expected: UI opens, status bar shows online.
+
+- [ ] **Step 4: Run any task tagged "agent" (e.g. "create a README")**
+
+In the Details pane, verify the log shows:
+- A `[session …]` line at the top
+- Plain prose lines for assistant text
+- `[Read] `, `[Bash] $ …`, `[Write] ` etc. for tool calls
+- `→ lines` / `→ ok` / `→ error: …` lines after each tool call
+- A final `--- Result ---` block
+- **No raw JSON anywhere**
+
+- [ ] **Step 5: Spot-check the raw log file**
+
+Open `~/.todo-app/logs/.log` (or equivalent) and confirm the full JSON is still there for debugging — the formatter must not have altered persisted logs.
+
+- [ ] **Step 6: If any issues surface, fix inline and re-verify**
+
+Common gotchas to check for if you see blank lines or missing output:
+- `message.content` sometimes absent → already guarded by `TryGetContentArray`
+- Unknown tool name → should render `[]` with no arg
+- `tool_result.content` array form → covered by `ResolveContentText`
+
+No further commit unless a fix was needed.
+
+---
+
+## Post-Implementation Self-Review
+
+After the tasks above are done, verify:
+
+1. Every message type listed in the spec's "Output format" table is implemented (`system/init`, `system/api_retry`, `system/other`, `assistant text`, `assistant tool_use`, `assistant thinking`, `user tool_result`, `result`, parse failure).
+2. No `TODO` / `TBD` / commented-out stubs remain in `StreamLineFormatter.cs`.
+3. Tool labels match the spec table exactly (`[Read]`, `[Bash] $ …`, `[Task: ] `, etc.).
+4. Public API surface (`FormatLine`, `FormatFile`, `Trim`, `MaxLength` behavior) is unchanged.
+5. No edits outside `StreamLineFormatter.cs` (per the spec's non-goals).
diff --git a/docs/superpowers/specs/2026-04-21-stream-formatter-rewrite-design.md b/docs/superpowers/specs/2026-04-21-stream-formatter-rewrite-design.md
new file mode 100644
index 0000000..925822a
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-21-stream-formatter-rewrite-design.md
@@ -0,0 +1,141 @@
+# Stream Formatter Rewrite — Design
+
+**Date:** 2026-04-21
+**Scope:** `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs`
+
+## Problem
+
+`StreamLineFormatter` converts Claude CLI stream-json lines into human-readable
+text for the Details pane. The current implementation only recognizes:
+
+- `type=stream_event` — dead code (requires `--include-partial-messages`, which
+ the Worker does not pass)
+- `type=result` — shown as `--- Result ---` block
+- `type=system` with `subtype=api_retry`
+
+Everything else — notably `assistant` and `user` messages that carry the actual
+conversation and tool activity — falls through to `default: return null` and is
+silently dropped. The Details pane is therefore mostly empty during a run,
+while the raw `.log` file retains the full JSON.
+
+## Goal
+
+Rewrite the formatter so every meaningful message type is rendered as one or
+more compact text lines suitable for the live log in the Details pane. The
+public API (`FormatLine(string)` / `FormatFile(string)`) and the existing
+buffer/trim behavior in `DetailsIslandViewModel` stay the same.
+
+## Input format
+
+The Worker invokes the Claude CLI with:
+
+```
+claude -p --output-format stream-json --verbose --dangerously-skip-permissions ...
+```
+
+Each stdout line is one complete SDK message. Top-level shapes relevant to the
+formatter:
+
+```jsonc
+// Session start
+{"type":"system","subtype":"init","session_id":"…","model":"claude-…", …}
+
+// API retry notification
+{"type":"system","subtype":"api_retry", …}
+
+// Assistant reply (text + tool calls)
+{"type":"assistant","message":{"role":"assistant","content":[
+ {"type":"text","text":"…"},
+ {"type":"tool_use","id":"toolu_…","name":"Read","input":{"file_path":"…"}}
+]}, …}
+
+// Tool result fed back to the model
+{"type":"user","message":{"role":"user","content":[
+ {"tool_use_id":"toolu_…","type":"tool_result","content":"… or [ {type,text} ] …","is_error":false}
+]}, "tool_use_result":{…optional rich payload…}, …}
+
+// Final result
+{"type":"result","result":"…", …}
+```
+
+Notes on quirks already observed in captured output:
+
+- `tool_result.content` is sometimes a plain string, sometimes an array of
+ `{type:"text", text:"…"}` blocks. Handle both.
+- The envelope may include `tool_use_result.file.numLines` / `file.filePath`
+ for Read-style results.
+- Assistant messages may contain `thinking` blocks (filtered, not displayed).
+
+## Output format
+
+One line per logical event. A trailing `\n` ends each line so the
+`DetailsIslandViewModel` buffer splits cleanly.
+
+| Input | Output |
+|---|---|
+| `system` / `init` | `[session · ]\n` |
+| `system` / `api_retry` | `[Retrying API call...]\n` |
+| `system` / other | `null` (filtered) |
+| `assistant` text block | `\n` (raw) |
+| `assistant` tool_use block | `[] \n` (see below) |
+| `assistant` thinking block | `null` (filtered) |
+| `user` tool_result block | `→ \n` (see below) |
+| `result` | `\n--- Result ---\n\n` |
+| unrecognized / parse failure | raw line (existing behavior for non-JSON) |
+
+A single `assistant` message with N content blocks produces N output lines,
+concatenated into one return string.
+
+### Tool label + arg
+
+Pick the most identifying input field per tool:
+
+| Tool name | Display |
+|---|---|
+| `Read`, `Write`, `Edit`, `NotebookEdit` | `[] ` |
+| `Bash`, `PowerShell` | `[Bash] $ ` — truncate command at 120 chars, append `…` |
+| `Grep` | `[Grep] ""` |
+| `Glob` | `[Glob] ` |
+| `Task`, `Agent` | `[Task: ] ` (description truncated to 120) |
+| `WebFetch` | `[WebFetch] ` |
+| `WebSearch` | `[WebSearch] ""` |
+| `TodoWrite` | `[TodoWrite]` (no arg) |
+| fallback | `[]` |
+
+Missing or empty input fields → emit the label only, no trailing text.
+
+### tool_result summary
+
+For each `tool_result` block in a `user` message, in priority order:
+
+1. `is_error == true` → `→ error: `
+2. Envelope has `tool_use_result.file.numLines` → `→ lines`
+3. Content resolves to empty/whitespace string → `→ ok`
+4. Otherwise → `→ ` (append `…` if truncated)
+
+Content resolution: if `content` is a string, use it; if it's an array, join
+the `text` fields of `{type:"text"}` entries.
+
+## Non-goals
+
+- No changes to `DetailsIslandViewModel` or the Worker pipeline.
+- No collapsible/rich rendering — tool results stay one-liners.
+- No persistence changes — the raw `.log` file still contains full JSON for
+ debugging.
+- No unit tests in this change (separate workload).
+
+## Out of scope
+
+- Partial-token streaming (`--include-partial-messages`). The existing
+ `stream_event` branch is removed as dead code.
+- Structured output / `--json-schema` rendering beyond the final `result`.
+
+## Risks / edge cases
+
+- **Unknown tool names** — fallback label `[]` keeps output readable.
+- **Malformed JSON inside a valid envelope** (e.g. missing `message.content`)
+ — skip the broken block, emit what we can; never throw.
+- **Very long Bash commands or search queries** — 120-char truncation with `…`
+ keeps lines reasonable while preserving the prefix.
+- **Binary or huge tool_result content** — summary rules 2–4 cap output at a
+ single line; full content stays in the raw log.