From 84cf9fa404a3c0096d235007e5e40c2b8eae251d Mon Sep 17 00:00:00 2001 From: dimakis Date: Mon, 4 May 2026 00:02:46 +0100 Subject: [PATCH 1/2] feat(ui): cc-deck-inspired attention feed, ATB redesign, and Telos sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Homepage "What's Next" attention feed that aggregates Telos focus items, ATB blocked/review tasks, and waiting sessions into a tiered feed — always visible, not just when sessions are active. ATB redesign: 2-line task cards with state-colored left borders, T1 background tint for review/blocked/failed, attention-tier sorting, done-task fade logic (5min→30min opacity decay), sort toggle. Telos redesign: 3-line card layout (summary-first), urgency-as-border- color (red/amber/purple intensity), section grouping (Focus/Active/ Seen/Done with collapsible headers), status-colored icons. Telos: 76732123c7c5, ae3ead922dc8 Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/AttentionFeed.tsx | 96 ++++++++ frontend/src/components/TaskNode.tsx | 41 +++- frontend/src/components/TodoCard.tsx | 100 +++++--- .../hooks/__tests__/useAttentionFeed.test.ts | 196 ++++++++++++++++ frontend/src/hooks/useAttentionFeed.ts | 216 ++++++++++++++++++ frontend/src/pages/SessionList.tsx | 3 + frontend/src/pages/TaskBoard.tsx | 47 +++- frontend/src/pages/TodoView.tsx | 130 +++++++++-- frontend/src/styles/global.css | 195 ++++++++++++++-- 9 files changed, 949 insertions(+), 75 deletions(-) create mode 100644 frontend/src/components/AttentionFeed.tsx create mode 100644 frontend/src/hooks/__tests__/useAttentionFeed.test.ts create mode 100644 frontend/src/hooks/useAttentionFeed.ts diff --git a/frontend/src/components/AttentionFeed.tsx b/frontend/src/components/AttentionFeed.tsx new file mode 100644 index 00000000..aed65ea0 --- /dev/null +++ b/frontend/src/components/AttentionFeed.tsx @@ -0,0 +1,96 @@ +import { useState, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAttentionFeed, type AttentionItem } from '../hooks/useAttentionFeed'; +import { selectionChanged } from '../lib/haptics'; + +// ─── Source labels ───────────────────────────────────────────────────────── + +const SOURCE_LABEL: Record = { + telos: 'telos', + atb: 'task', + session: 'session', +}; + +// ─── Card ────────────────────────────────────────────────────────────────── + +function AttentionCard({ + item, + onTap, +}: { + item: AttentionItem; + onTap: (item: AttentionItem) => void; +}) { + return ( + + ); +} + +// ─── Main component ──────────────────────────────────────────────────────── + +export function AttentionFeed() { + const { items, tier1Count, loading } = useAttentionFeed(); + const navigate = useNavigate(); + + const hasUrgent = tier1Count > 0; + const [manualOpen, setManualOpen] = useState(null); + const isOpen = manualOpen ?? true; // always open by default + + const toggleOpen = useCallback(() => { + setManualOpen((prev) => !(prev ?? true)); + }, []); + + const handleTap = useCallback( + (item: AttentionItem) => { + selectionChanged(); + navigate(item.navigateTo); + }, + [navigate], + ); + + // Show section even when empty — gives "all clear" signal + const summaryParts: string[] = []; + if (tier1Count > 0) summaryParts.push(`${tier1Count} needs you`); + const t2Count = items.filter((i) => i.tier === 2).length; + if (t2Count > 0) summaryParts.push(`${t2Count} in focus`); + + return ( +
+ + {isOpen && ( +
+ {!loading && items.length === 0 && ( +
Nothing needs your attention right now.
+ )} + {items.map((item) => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/TaskNode.tsx b/frontend/src/components/TaskNode.tsx index d4e62857..845712bf 100644 --- a/frontend/src/components/TaskNode.tsx +++ b/frontend/src/components/TaskNode.tsx @@ -38,6 +38,45 @@ const NEXT_STATUS: Record = { failed: 'pending', }; +const T1_STATUSES: TaskStatus[] = ['pending_review', 'blocked', 'failed']; + +function statusMeta(task: Task): string { + switch (task.status) { + case 'active': + return task.sessionId ? `session ${task.sessionId.slice(-6)}` : 'in progress'; + case 'pending_review': + return 'awaiting approval'; + case 'blocked': + return task.annotations?.[0] || 'blocked'; + case 'failed': + return `failed${task.retryCount > 0 ? ` \u00B7 retry ${task.retryCount}/${task.maxRetries}` : ''}`; + case 'done': + return task.completedAt ? formatElapsed(Date.now() - task.completedAt) : 'done'; + default: + return task.status; + } +} + +function formatElapsed(ms: number): string { + const sec = Math.floor(ms / 1000); + if (sec < 60) return 'just now'; + const min = Math.floor(sec / 60); + if (min < 60) return `${min}min ago`; + const hr = Math.floor(min / 60); + return `${hr}h ago`; +} + +/** Opacity for done tasks: fade after 5min, near-invisible after 30min */ +function doneOpacity(task: Task): number { + if (task.status !== 'done' || !task.completedAt) return 1; + const elapsed = Date.now() - task.completedAt; + const FIVE_MIN = 5 * 60 * 1000; + const THIRTY_MIN = 30 * 60 * 1000; + if (elapsed < FIVE_MIN) return 1; + if (elapsed > THIRTY_MIN) return 0.3; + return 1 - 0.7 * ((elapsed - FIVE_MIN) / (THIRTY_MIN - FIVE_MIN)); +} + interface TaskNodeProps { task: Task; depth: number; @@ -150,7 +189,7 @@ export function TaskNode({ {'\u21BB'} {task.retryCount} - )} +
{task.status === 'pending_review' && onApprove && (
-
{item.summary}
- {source && ( -
- {source.author} -
- )} -
+ + {/* Line 2: source + meta */} +
+ {source ? ( + {sourceIcon(source.type)} + ) : ( + + + )} + {source?.author && ( + <>{source.author}{' \u00B7 '} + )} + {ageLabel} + {' \u00B7 '} + {item.profile} + {hasChildren && ( + <> + {' \u00B7 '} + + {item.completedChildCount ?? 0}/{item.childCount ?? children.length} + + + )} +
+ + {/* Line 3: actions */} +
+ ); +} + +// ─── Main view ───────────────────────────────────────────────────────────── + export function TodoView() { const navigate = useNavigate(); const location = useLocation(); @@ -84,6 +156,11 @@ export function TodoView() { const [creating, setCreating] = useState<{ parentId?: string } | null>(null); const setPendingSession = useMitzoStore((s) => s.setPendingSession); const scrollRef = useRef(null); + const [collapsedSections, setCollapsedSections] = useState>({ + done: true, + }); + + const sections = useMemo(() => groupIntoSections(items), [items]); // Restore scroll position when returning from detail view useEffect(() => { @@ -97,6 +174,10 @@ export function TodoView() { return scrollRef.current?.scrollTop ?? 0; }, []); + function toggleSection(key: string) { + setCollapsedSections((prev) => ({ ...prev, [key]: !prev[key] })); + } + function handleStartSession(item: TodoItem) { setPendingSession({ prompt: buildPrompt(item), @@ -175,24 +256,35 @@ export function TodoView() { /> )} -
- {items.length > 0 && Tap to start working. Swipe right = seen, left = done.} -
- -
- {items.map((item) => ( - - ))} -
+ {sections.map((section) => { + const isCollapsed = collapsedSections[section.key] ?? section.defaultCollapsed; + return ( +
+ toggleSection(section.key)} + /> + {!isCollapsed && ( +
+ {section.items.map((item) => ( + + ))} +
+ )} +
+ ); + })}
); diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index d20f066a..412c8b0a 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -753,6 +753,80 @@ textarea:focus { margin-top: 2px; } +/* ─── Attention Feed ────────────────────────────────────────────────────── */ + +.attention-section { + margin-bottom: var(--space-4); +} + +.attention-cards { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-top: var(--space-2); +} + +.attention-card { + display: flex; + align-items: flex-start; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + background: var(--surface); + border: 1px solid var(--border); + border-left: 3px solid var(--card-accent, var(--border)); + border-radius: var(--radius-md); + cursor: pointer; + text-align: left; + color: var(--text); + font-family: inherit; + font-size: var(--text-sm); + width: 100%; + -webkit-tap-highlight-color: transparent; + transition: background 0.15s; +} + +.attention-card:active { + background: var(--hover); +} + +.attention-card-icon { + flex-shrink: 0; + font-size: var(--text-base); + line-height: 1.3; +} + +.attention-card-content { + flex: 1; + min-width: 0; +} + +.attention-card-title { + font-size: var(--text-sm); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.attention-card-meta { + font-size: var(--text-xs); + color: var(--text-dim); + margin-top: 2px; +} + +.attention-card-source { + font-weight: 600; + text-transform: uppercase; + font-size: var(--text-2xs); + letter-spacing: 0.03em; +} + +.attention-empty { + font-size: var(--text-xs); + color: var(--text-dim); + padding: var(--space-3) 0; + text-align: center; +} + .service-status { display: flex; gap: var(--space-4); @@ -4037,24 +4111,42 @@ textarea:focus { padding: 12px 14px; cursor: pointer; touch-action: pan-y; + display: flex; + flex-direction: column; + gap: 4px; } -.todo-card-header { +/* ─── 3-line card layout ──────────────────────────────────────────── */ + +.todo-card-line1 { display: flex; - align-items: center; + align-items: flex-start; gap: var(--space-2); - margin-bottom: var(--space-1); - font-size: var(--text-xs); } -.todo-card-status { - color: var(--accent); +.todo-card-icon { + flex-shrink: 0; + font-size: var(--text-base); + line-height: 1.3; } -.todo-card-urgency { - font-family: monospace; - letter-spacing: 1px; +.todo-card-summary { + flex: 1; + font-size: var(--text-sm); + line-height: 1.35; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.todo-card-line2 { + display: flex; + align-items: center; + gap: var(--space-1); + font-size: var(--text-xs); color: var(--text-dim); + padding-left: calc(var(--text-base) + var(--space-2)); } .todo-card-source { @@ -4068,22 +4160,72 @@ textarea:focus { .todo-card-age { color: var(--text-dim); - margin-left: auto; } -.todo-card-summary { - font-size: var(--text-sm); - line-height: 1.35; +.todo-card-profile { + color: var(--text-dim); } -.todo-card-meta { - margin-top: var(--space-1); - font-size: var(--text-xxs); +.todo-card-author { color: var(--text-dim); } -.todo-card-author { +.todo-card-line3 { + display: flex; + gap: var(--space-2); + padding-left: calc(var(--text-base) + var(--space-2)); + margin-top: 2px; +} + +/* ─── Section headers ─────────────────────────────────────────────── */ + +.todo-section { + margin-bottom: var(--space-3); +} + +.todo-section-header { + display: flex; + align-items: center; + gap: var(--space-2); + width: 100%; + padding: var(--space-2) 0; + background: transparent; + border: none; color: var(--text-dim); + font-size: var(--text-xs); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.todo-section-label { + flex-shrink: 0; +} + +.todo-section-count { + flex-shrink: 0; + font-weight: 400; + opacity: 0.7; +} + +.todo-section-line { + flex: 1; + height: 1px; + background: var(--border); +} + +.todo-section-chevron { + display: inline-block; + font-size: var(--text-base); + transition: transform 0.2s; + transform: rotate(0deg); + flex-shrink: 0; +} + +.todo-section-chevron--open { + transform: rotate(90deg); } /* --- Hierarchical todo tree --- */ @@ -4132,7 +4274,6 @@ textarea:focus { padding: 2px 8px; border-radius: 10px; cursor: pointer; - margin-top: var(--space-1); opacity: 0.4; transition: opacity 0.15s; } @@ -4143,12 +4284,6 @@ textarea:focus { opacity: 1; } -.todo-card-actions { - display: flex; - gap: var(--space-2); - margin-top: var(--space-1); -} - .todo-card-session-btn { background: none; border: 1px solid var(--accent, #6366f1); @@ -4751,11 +4886,17 @@ textarea:focus { .task-node-body { flex: 1; min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; } .task-node-title { font-size: var(--text-md); line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .task-node-title--done { @@ -4780,6 +4921,12 @@ textarea:focus { color: var(--task-failed); } +.task-node-meta { + font-size: var(--text-xs); + color: var(--text-dim); + line-height: 1.3; +} + .task-node-actions { display: flex; gap: var(--space-1); From 31ae2239476cbfec5befe0f025e4b267516648e3 Mon Sep 17 00:00:00 2001 From: dimakis Date: Sat, 9 May 2026 15:29:11 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix(ui):=20address=20Centaur=20review=20?= =?UTF-8?q?=E2=80=94=20attention=20feed=20robustness=20and=20tier=20recurs?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused AttentionTier 3 and COLOR_BLUE - Add updatedAt for recency tiebreaker in attention sorting - Fix sortByAttention to consider children status for tier calculation - Fix t1Count to recurse into children Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/TaskNode.tsx | 2 +- frontend/src/hooks/useAttentionFeed.ts | 27 +++++++++++++---- frontend/src/pages/TaskBoard.tsx | 40 +++++++++++++++++++------- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/TaskNode.tsx b/frontend/src/components/TaskNode.tsx index 845712bf..c55a96c6 100644 --- a/frontend/src/components/TaskNode.tsx +++ b/frontend/src/components/TaskNode.tsx @@ -189,7 +189,7 @@ export function TaskNode({ {'\u21BB'} {task.retryCount} - + )}
{task.status === 'pending_review' && onApprove && (