diff --git a/docs/UI Rewrite/design_handoff_claudedo/ClaudeDo-standalone.html b/docs/UI Rewrite/design_handoff_claudedo/ClaudeDo-standalone.html new file mode 100644 index 0000000..f2808cb --- /dev/null +++ b/docs/UI Rewrite/design_handoff_claudedo/ClaudeDo-standalone.html @@ -0,0 +1,184 @@ + + + + + ClaudeDo — Rider Island + + + + +
+ + + + + + + + + +
+
Unpacking...
+ + + + + + + + + + \ No newline at end of file diff --git a/docs/UI Rewrite/design_handoff_claudedo/ClaudeDo.html b/docs/UI Rewrite/design_handoff_claudedo/ClaudeDo.html new file mode 100644 index 0000000..5ef0bf5 --- /dev/null +++ b/docs/UI Rewrite/design_handoff_claudedo/ClaudeDo.html @@ -0,0 +1,36 @@ + + + + +ClaudeDo — Rider Island + + + + + + + + +
+ + + + + + + + + + + + diff --git a/docs/UI Rewrite/design_handoff_claudedo/IslandStyles.axaml b/docs/UI Rewrite/design_handoff_claudedo/IslandStyles.axaml new file mode 100644 index 0000000..afcb965 --- /dev/null +++ b/docs/UI Rewrite/design_handoff_claudedo/IslandStyles.axaml @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/UI Rewrite/design_handoff_claudedo/README.md b/docs/UI Rewrite/design_handoff_claudedo/README.md new file mode 100644 index 0000000..91a08da --- /dev/null +++ b/docs/UI Rewrite/design_handoff_claudedo/README.md @@ -0,0 +1,253 @@ +# ClaudeDo — Avalonia Handoff + +## Overview + +ClaudeDo is an agent dispatcher for Claude Code: a Windows desktop app that presents background coding agents as tasks. Each task has a title, a list, a git worktree/branch, an agent status (idle / queued / running / review / error), a live session log, and a diff. The UI is organised as **three floating islands** (Lists / Tasks / Details) over a dark "sea" background, Windows-11 style. + +The bundled HTML file is a **design reference**, not production code. Your job is to recreate it as a native Avalonia app — match the look, feel, and interaction model, but use idiomatic AXAML, Avalonia controls, and whatever MVVM / ReactiveUI / CommunityToolkit patterns your codebase already uses. + +## Fidelity + +**High-fidelity.** All colors, typography, spacing, corner radii, shadows, and interaction states are final. Recreate pixel-perfectly using Avalonia primitives. The one exception: motion — CSS animations translate approximately to Avalonia `Transitions` / `Animation`; the durations and easings in `Tokens.axaml` are the intent. + +## What's in this package + +| File | Purpose | +|---|---| +| `Tokens.axaml` | `ResourceDictionary` — colors, brushes, spacing, corner radii, typography, shadows, motion durations. **Merge this first in `App.axaml`.** | +| `IslandStyles.axaml` | `Styles` — classed styles for Island, Chip, Button, TextBox, TaskRow, AgentStrip, Terminal, ListItem. Depends on `Tokens.axaml`. | +| `ClaudeDo.html` | The live design reference — open it in a browser to see behavior, hover states, animations, modals. | +| `ClaudeDo-standalone.html` | Fully offline single-file version (no network). Ship this with the handoff. | +| `app.jsx`, `islands.jsx`, `modals.jsx`, `icons.jsx`, `data.jsx`, `styles.css` | Source of the reference. Read `styles.css` for any measurement you need to verify; read the JSX for component structure and state transitions. | +| `ComponentSpec.md` | This file section below — maps every visual element to the AXAML control you should use. | + +## How to wire the tokens + +In `App.axaml`: + +```xml + + + + + + + + + + + + +``` + +Pack **Inter Tight** (sans) and **JetBrains Mono** (mono) as embedded resources and reference them via `avares://YourApp/Assets/Fonts/#Inter Tight` in `Tokens.axaml` if the system-font fallback isn't good enough. + +--- + +## Window chrome + +The reference shows a Windows-11-style app in a chromeless window with a custom title bar and taskbar. For a native Avalonia app, use `SystemDecorations="None"` + `ExtendClientAreaToDecorationsHint="True"` and draw your own title bar, OR use the platform chrome — the islands-over-sea metaphor works either way. The taskbar strip at the bottom of the reference is decorative; drop it. + +## Layout + +Root window: +``` +┌─────────────────────────────────────────────────────────┐ +│ TitleBar │ +├─────────────┬──────────────────────────┬────────────────┤ +│ Lists │ Tasks │ Details │ +│ (260px) │ (1fr, min 340px) │ (320px) │ +│ │ │ hides <1100px │ +└─────────────┴──────────────────────────┴────────────────┘ +``` + +Use a `Grid` with 3 columns: `260,*,320`. Collapse the Details column when `ActualWidth < 1100` via a bound `ColumnDefinition.Width`. Between columns and around the grid, add 14px gap — put each island in a `Border Classes="island"` with `Margin="7"` so you get the island-to-island gap naturally. + +Background of the grid cell: apply `DesktopBackgroundBrush` from tokens, plus a subtle radial-gradient overlay via a `Border` with an opacity mask if desired. + +--- + +## Components + +### Island (base container) + +- `Border Classes="island"` +- Contents: header section + scrollable body +- Header: inner `Border Classes="island-header"` with an eyebrow label (mono, uppercase, tracking 1.4) and a title +- Body: `ScrollViewer` → `StackPanel` or `ItemsControl` +- All three columns are islands. + +### Lists island (left) + +- Search box: `TextBox Classes="search"` with left-aligned search icon (PathIcon) +- Nav items: `ItemsControl` bound to a `Lists` collection + - Each item is `Border Classes="list-item"` (toggle `active` class when selected) containing + - `PathIcon` (16px) + - `TextBlock` (list name, 13px) + - `TextBlock` (count, mono 10px, right-aligned, TextFaintBrush) +- Lists shown: My Day, Important, Planned, Running, Review, Tasks (by project name) + +### Tasks island (middle) + +Header row: +- Eyebrow: weekday date ("MONDAY · APR 28") +- Title: "My Day" (or current list name) — 24px semibold +- Subtitle: "{N} open · {N} running · {N} in review" — mono 11px TextMute +- Right side: icon buttons (sort, filter, show-completed toggle) + +Add-task row: +- `TextBox` with placeholder "Add a task…" +- On Enter: dispatch new task (see ViewModel spec) + +Task list: +- `ItemsControl` → `Border Classes="task-row"` per task +- Row content: `Grid` with columns `Auto,*,Auto` + - Left: `Ellipse Classes="task-check"` (toggles `done` class on completion) — use a `Button` with a templated Ellipse for keyboard support + - Middle: `StackPanel` vertical + - Title: `TextBlock` 14px, strike-through when done + - Meta row: `StackPanel` horizontal with 8px gap, children: + - `Border Classes="chip {status}"` (status chip — Running / Review / Error / Queued / Idle) + - `Border Classes="chip"` with list name + - `Border Classes="chip"` with mono branch name (e.g. `agent/auth-pool`) + - `Border Classes="chip"` with diff stats (`+142 −86`) + - Live tail of latest agent output when running — use `TextTrimming="CharacterEllipsis"` in a fixed-width container + - Right: `Button Classes="icon-btn"` (star) +- Selection: toggle `selected` class; add a `Rectangle` with `Width=2` as the left accent bar (child of the task-row Border) + +### Details island (right) + +Shown when a task is selected. Sections, top to bottom: + +1. **Header**: task title (editable — `TextBox` with no visible border, `FontSize=18`), list chip, delete icon button +2. **Agent strip** — `Border Classes="agent-strip {status}"`: + - Row 1: status indicator dot + status label ("Running" / "Review" / etc.) + model name ("claude-sonnet-4.5") + turns + tokens + elapsed + - Row 2: Worktree path (mono, truncating) + - Row 3: Branch → Base ("agent/auth-pool ← main") + commit count + diff stats + - Buttons: "Open diff" / "Worktree" / "Stop" (when running) / "Approve & merge" (when review) +3. **Session output** — `Border Classes="terminal"` with a `ScrollViewer` auto-scrolled to bottom: + - Each line is a `TextBlock Classes="log-{kind}"` — kinds: sys, tool, claude, stdout, stderr, done, msg + - Below it, a prompt input: `[you]` prefix + `TextBox` to send messages to the agent +4. **Subtasks** — `ItemsControl` of checkbox + text rows +5. **Notes** — multi-line `TextBox`, `AcceptsReturn="True"` +6. **Metadata** — created date, last activity, tags (readonly chips) + +### Modals + +Two modals in the reference: **Diff** and **Worktree**. Use `Window` with `WindowStartupLocation="CenterOwner"` and a scrim (`Border` over the main window with `Background="#BF030504"` + blur via `OpacityMask` or a child `Grid`). Or use `Dialog` if your shell has one. + +**Diff modal**: left sidebar of files (each with `+N −N` stats), right pane with syntax-colored hunks. Use two `ListBox`-style panels side-by-side. For lines: `del` = red tinted, `add` = green tinted, `ctx` = neutral. Left gutter columns: old line number, new line number, sign (`+` / `−` / space). + +**Worktree modal**: folder tree with `M` (modified) / `A` (added) badges. `TreeView` fits naturally. + +### Status mapping + +| Status | Chip color | Icon | When | +|---|---|---|---| +| idle | TextMute | circle | Task created, agent not dispatched | +| queued | Sage | dots | Agent queued behind others | +| running | Accent (moss) | pulse dot | Agent actively working | +| review | Peat | eye | Agent finished; awaiting approval | +| error | Blood | exclamation | Agent failed | + +--- + +## State & interactions + +### Task model (MVVM) + +```csharp +public class TaskItem : ReactiveObject { + string Id, Title, List; + bool Done, Starred, MyDay; + DateTime? Due, Created; + string Notes; + List Tags; + List Subtasks; + AgentState Agent; // null if not dispatched +} + +public class AgentState : ReactiveObject { + AgentStatus Status; // Idle | Queued | Running | Review | Error + string Model, Worktree, Branch, BaseBranch; + int Commits, Turns, Tokens; + DiffStats Diff; // Files, Additions, Deletions + DateTime? StartedAt, FinishedAt; + ObservableCollection Log; +} +``` + +### Key interactions + +- **Toggle done**: click checkbox → flip `Done`, animate strike-through (0.2s ease-out) +- **Select task**: click row → set `SelectedTask`; details island rebinds +- **Add task**: Enter in the add-task textbox → prepend new task; scroll list to top; 0.3s fade-in animation on the new row (use `Animation` with opacity + `TranslateTransform.Y`) +- **Dispatch agent**: "Start agent" button in Details → sets `Agent.Status = Running`, appends sys log "Agent dispatched." +- **Stop agent**: → `Status = Review` (or `Error` on failure), appends sys log +- **Send prompt**: Enter in prompt input → append `[you] {msg}` to log +- **Open diff / worktree**: opens modal; Esc closes + +### Keyboard + +- `/` focuses the search box in the Lists island +- `Cmd/Ctrl+N` focuses add-task +- `Space` toggles done on selected row +- `Esc` closes any open modal + +### Animations + +- Task-row hover: background transition 0.1s +- Task-row add: 0.3s opacity + slight Y-slide +- Task-row complete: 0.25s strike-through + fade to `done` styling +- Running status dot: infinite pulse (opacity 0.4 → 1.0, 1.2s) +- Modal open: 0.18s opacity + scale (0.98 → 1.0) +- Backdrop: 0.15s opacity fade + +### Responsive + +- `< 1100px`: hide Details island; details open as a transient panel or modal on task select +- `< 780px`: hide Lists island; use a hamburger drawer + +--- + +## Design tokens (reference) + +All final values live in `Tokens.axaml`. Reproduced here for reading: + +**Surfaces**: `#0A0E0C` void · `#0D1311` deep · `#161D1A` surface · `#1C2422` surface-2 · `#222B28` surface-3 · `#2A3330` line + +**Text**: `#E4EBE4` primary · `#9AA8A0` dim · `#6B7973` mute · `#4A5550` faint + +**Accents**: `#7C9166` moss (primary) · `#8B9D7A` sage · `#D4A574` peat · `#C87060` blood + +**Spacing**: 4, 8, 12, 14 (island gap), 18, 24 + +**Corner radii**: 14 (island) · 12 (modal) · 10 (chip) · 8 (task row, input) · 6 (button) · 999 (pill) + +**Typography**: Inter Tight (sans), JetBrains Mono (mono). Scale: 10 (eyebrow) / 11 (mono, micro) / 13 (body) / 14 (task title) / 18 (h3) / 24 (h2) / 32 (h1) + +**Shadows**: +- Island: `0 20 40 #59000000, 0 2 4 #4D000000` +- Modal: `0 40 80 #B2000000` + +**Motion**: 120ms (fast) / 180ms (base) / 300ms (slow). Easing: cubic-bezier(0.4, 0, 0.2, 1) — use Avalonia's `CubicEaseOut` or a custom `SplineEasing`. + +--- + +## Acceptance checklist + +- [ ] Three-island layout with correct spacing and grid collapse at <1100px +- [ ] Lists sidebar with icons, counts, search, active state +- [ ] Task rows with checkbox, title, meta chips (status/list/branch/diff), star +- [ ] Task selection updates Details island +- [ ] 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 add/del lines +- [ ] Worktree modal with M/A badges +- [ ] Status chip tints match the spec +- [ ] Fonts: Inter Tight + JetBrains Mono packed and applied +- [ ] Motion: task add/toggle, running pulse, modal open, hover transitions +- [ ] Keyboard shortcuts wired + +## Questions / contact + +The HTML reference is the source of truth for any visual ambiguity. Open `ClaudeDo-standalone.html` and inspect directly. diff --git a/docs/UI Rewrite/design_handoff_claudedo/Tokens.axaml b/docs/UI Rewrite/design_handoff_claudedo/Tokens.axaml new file mode 100644 index 0000000..b6814f4 --- /dev/null +++ b/docs/UI Rewrite/design_handoff_claudedo/Tokens.axaml @@ -0,0 +1,188 @@ + + + + + + + + + #FF0A0E0C + #FF0D1311 + #FF161D1A + #FF1C2422 + #FF222B28 + #FF2A3330 + #FF3A4542 + + + #FFE4EBE4 + #FF9AA8A0 + #FF6B7973 + #FF4A5550 + + + #FF4A6B4A + #FF6B8E6B + #FF8B9D7A + #FFD4A574 + #FFB88D5E + #FFC87060 + + + #FF7C9166 + #FF64785A + #FF3E4B39 + #387C9166 + + + #FF7C9166 + #FFD4A574 + #FFC87060 + #FF8B9D7A + #FF6B7973 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + 8 + 12 + 14 + 18 + 24 + + 7 + 18,16,18,12 + 14 + 14 + + + + + + 14 + 6 + 10 + 999 + 8 + 12 + + 1 + + + + + + + Inter Tight, Inter, Segoe UI, -apple-system, sans-serif + JetBrains Mono, IBM Plex Mono, Cascadia Mono, Consolas, monospace + + + 10 + 11 + 11 + 13 + 14 + 18 + 24 + 32 + + + + + + + + + + + + + 0 20 40 0 #59000000, 0 2 4 0 #4D000000 + 0 40 80 0 #B2000000 + 0 2 4 0 #33000000 + + + + + + 0:0:0.12 + 0:0:0.18 + 0:0:0.30 + + + + diff --git a/docs/UI Rewrite/design_handoff_claudedo/app.jsx b/docs/UI Rewrite/design_handoff_claudedo/app.jsx new file mode 100644 index 0000000..7f68db2 --- /dev/null +++ b/docs/UI Rewrite/design_handoff_claudedo/app.jsx @@ -0,0 +1,374 @@ +// App shell + Tweaks panel + Windows chrome +const { useState, useEffect, useRef, useMemo } = window.React; + +const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ + "accentHue": 88, + "islandGap": 14, + "islandRadius": 14, + "grainOpacity": 0.035, + "density": "comfy", + "sidebarWidth": 260 +}/*EDITMODE-END*/; + +const HUE_PRESETS = [ + { name: 'Moss', h: 88 }, + { name: 'Sea', h: 200 }, + { name: 'Peat', h: 60 }, + { name: 'Heather', h: 310 }, + { name: 'Rust', h: 30 }, +]; + +const TweaksPanel = ({ open, onClose, tweaks, setTweaks }) => { + const update = (k, v) => { + const next = { ...tweaks, [k]: v }; + setTweaks(next); + try { + window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [k]: v } }, '*'); + } catch (e) {} + }; + return ( +
+
+
Tweaks
+ +
+ +
+
+ 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 ( +
+
+
Navigator
+

Lists

+
+ +
+ + 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 +
+
+
+
+ + + +
+
+ +
+
+
+ setNewTitle(e.target.value)} + /> + ENTER +
+ +
+ {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)} /> +