feat(ui): task board redesign with attention sorting#314
Conversation
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 8 issue(s) (2 warning).
frontend/src/hooks/useTaskBoard.ts
Solid UI redesign. Main concern: the now refresh timer only activates for done tasks, leaving active task elapsed labels stale when no done tasks exist. Also computeFadeOpacity bypasses the memoization dependency by calling Date.now() directly. Pure helper functions need test coverage.
- 🟡 bugs (L218): The 60s timer that updates
nowonly activates when done tasks exist (hasDone), butelapsedLabelfor active tasks also depends onnow. If there are active tasks but no done tasks,elapsedLabelis frozen at mount time and never updates. The condition should include active tasks:const needsTimer = tasks.some(t => t.status === 'done' || t.status === 'active' || t.children.some(c => c.status === 'done' || c.status === 'active')).[fixable] - 🔵 bugs (L51):
computeFadeOpacitycallsDate.now()directly instead of using thenowparameter passed throughbuildDisplayMeta. This breaks the memoization contract —displayMetais memoized on[tasks, now]butfadeOpacityvalues escape that dependency. Use thenowparam consistently: pass it intocomputeFadeOpacity(task.completedAt, now)and computeelapsed = now - completedAt.[fixable] - 🔵 bugs (L222): The
hasDonecheck only traverses 2 levels (root tasks and direct children). Done tasks at depth 2+ won't activate the fade timer. Use a recursive helper orcollectDisplayMeta's traversal pattern to check all depths.[fixable] - 🔵 style (L237):
attendCountsis computed viauseMemoand returned from the hook, butTaskBoard.tsxnever destructures or uses it. This is dead code that runs on every task change. Either use it in the UI (e.g., a badge on the Tiers button) or remove it.[fixable] - 🟡 missing_tests: The PR adds ~130 lines of pure logic (
getAttendTier,computeFadeOpacity,buildDisplayMeta,sortByAttendTier,sumTokenUsage) but the existinguseTaskBoard.test.tshas no coverage for any of it. These are easily unit-testable pure functions. The new hook outputs (sortedTasks,displayMeta,totalTokenUsage,showAll) are also untested.[fixable]
frontend/src/lib/formatTokens.ts
Solid UI redesign. Main concern: the now refresh timer only activates for done tasks, leaving active task elapsed labels stale when no done tasks exist. Also computeFadeOpacity bypasses the memoization dependency by calling Date.now() directly. Pure helper functions need test coverage.
- 🔵 missing_tests: New
formatTokensutility has no test file. Edge case:formatTokens(999500)returns"1000k"instead of"1.0M"since it hits the>= 1000branch before>= 1_000_000. A test would catch this.[fixable]
frontend/src/lib/formatTime.ts
Solid UI redesign. Main concern: the now refresh timer only activates for done tasks, leaving active task elapsed labels stale when no done tasks exist. Also computeFadeOpacity bypasses the memoization dependency by calling Date.now() directly. Pure helper functions need test coverage.
- 🔵 missing_tests: New
formatDurationfunction has no tests. Boundary cases (0ms, negative ms from clock skew, exactly 60s, exactly 3600s) are untested.[fixable]
frontend/src/components/TaskNode.tsx
Solid UI redesign. Main concern: the now refresh timer only activates for done tasks, leaving active task elapsed labels stale when no done tasks exist. Also computeFadeOpacity bypasses the memoization dependency by calling Date.now() directly. Pure helper functions need test coverage.
- 🔵 style (L205): Children are always rendered with
compact={true}, which suppresses context lines for non-active children. This means tier-1 children (pending_review, blocked, failed) lose their contextual text ("awaiting approval", blocker summary). The status color and T1 background tint still show, but the text context that distinguishes e.g. a specific blocker reason is hidden. Consider making compact conditional on tier.[fixable]
|
|
||
| // Fade timer — recompute every 60s for done task opacity | ||
| useEffect(() => { | ||
| const hasDone = tasks.some( |
There was a problem hiding this comment.
🟡 bugs: The 60s timer that updates now only activates when done tasks exist (hasDone), but elapsedLabel for active tasks also depends on now. If there are active tasks but no done tasks, elapsedLabel is frozen at mount time and never updates. The condition should include active tasks: const needsTimer = tasks.some(t => t.status === 'done' || t.status === 'active' || t.children.some(c => c.status === 'done' || c.status === 'active')). [fixable]
| if (elapsed >= FADE_END_MS) return 0; | ||
| // Linear fade from 1 → 0.5 between 5min and 30min | ||
| return 0.5 + 0.5 * (1 - (elapsed - FADE_START_MS) / (FADE_END_MS - FADE_START_MS)); | ||
| } |
There was a problem hiding this comment.
🔵 bugs: computeFadeOpacity calls Date.now() directly instead of using the now parameter passed through buildDisplayMeta. This breaks the memoization contract — displayMeta is memoized on [tasks, now] but fadeOpacity values escape that dependency. Use the now param consistently: pass it into computeFadeOpacity(task.completedAt, now) and compute elapsed = now - completedAt. [fixable]
| (t) => t.status === 'done' || t.children.some((c) => c.status === 'done'), | ||
| ); | ||
| if (!hasDone) return; | ||
| const interval = setInterval(() => setNow(Date.now()), 60_000); |
There was a problem hiding this comment.
🔵 bugs: The hasDone check only traverses 2 levels (root tasks and direct children). Done tasks at depth 2+ won't activate the fade timer. Use a recursive helper or collectDisplayMeta's traversal pattern to check all depths. [fixable]
| }, [tasks, now]); | ||
|
|
||
| // Attend counts | ||
| const attendCounts = useMemo(() => { |
There was a problem hiding this comment.
🔵 style: attendCounts is computed via useMemo and returned from the hook, but TaskBoard.tsx never destructures or uses it. This is dead code that runs on every task change. Either use it in the UI (e.g., a badge on the Tiers button) or remove it. [fixable]
| @@ -138,7 +203,9 @@ export function TaskNode({ | |||
| key={child.id} | |||
| task={child} | |||
| depth={depth + 1} | |||
There was a problem hiding this comment.
🔵 style: Children are always rendered with compact={true}, which suppresses context lines for non-active children. This means tier-1 children (pending_review, blocked, failed) lose their contextual text ("awaiting approval", blocker summary). The status color and T1 background tint still show, but the text context that distinguishes e.g. a specific blocker reason is hidden. Consider making compact conditional on tier. [fixable]
…sorting Contextual loop controls, attend-tier sorting, progressive fade for completed tasks, token usage display, and show-all toggle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix timer to activate for all tasks, not just done tasks, so active
task elapsed times update every 60s
- Pass `now` parameter to computeFadeOpacity instead of calling
Date.now() directly, preserving the memoization pattern
- Remove shallow hasDone check (was only 2 levels deep); timer now
activates whenever tasks.length > 0, making depth irrelevant
- Remove unused attendCounts from hook return and its supporting code
- Show context lines for tier-1 children (pending_review/blocked/failed)
instead of always suppressing them with compact={true}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dad3da9 to
c40a661
Compare
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (1 critical) (3 warning).
frontend/src/components/LoopControls.tsx
Missing formatTokens module will break the build; the timer effect dependency on tasks causes unnecessary interval churn; new pure helper logic lacks test coverage.
- 🔴 bugs (L4): Import
formatTokensfrom'../lib/formatTokens'references a module that does not exist. NoformatTokens.tsfile exists infrontend/src/lib/. This will cause a build/runtime error when the component is loaded, even iftotalTokenUsageis 0 (the import itself fails).[fixable]
frontend/src/hooks/useTaskBoard.ts
Missing formatTokens module will break the build; the timer effect dependency on tasks causes unnecessary interval churn; new pure helper logic lacks test coverage.
- 🟡 unsafe_assumptions (L202): The timer
useEffecthas[tasks]as its dependency. Sincetasksis a Zustand store selector returning a new array reference on every store update (each WS event), the interval is torn down and recreated on every task mutation. This is wasteful. The guardtasks.length === 0only needs to know whether tasks exist, so the dependency should betasks.length > 0(via a separate boolean state or ref) or the effect should use a ref to avoid churn.[fixable] - 🔵 style (L43): Comment says "Linear fade from 1 → 0.5" but the function actually fades from 1.0 down to just above 0.5, then jumps to 0 (hidden via
task-node--hidden) at the 30-minute boundary. The comment should mention the final jump to 0 / hidden to avoid surprising a future reader.[fixable] - 🟡 missing_tests: The new pure helper functions (
getAttendTier,computeFadeOpacity,sortByAttendTier,sumTokenUsage,buildDisplayMeta) contain non-trivial logic (fade math, multi-key sorting, recursive tree traversal) but have no test coverage. These are pure functions that would be straightforward to unit test.[fixable]
frontend/src/lib/formatTime.ts
Missing formatTokens module will break the build; the timer effect dependency on tasks causes unnecessary interval churn; new pure helper logic lacks test coverage.
- 🟡 missing_tests: The new
formatDurationfunction has no tests. It also doesn't handle negative input (e.g., ifnow - claimedAtis negative due to clock skew), which would produce a negative seconds string like "-3s".[fixable]
| import { useState } from 'react'; | ||
| import type { LoopStatus } from '../types/task'; | ||
| import type { Task } from '../types/task'; | ||
| import { formatTokens } from '../lib/formatTokens'; |
There was a problem hiding this comment.
🔴 bugs: Import formatTokens from '../lib/formatTokens' references a module that does not exist. No formatTokens.ts file exists in frontend/src/lib/. This will cause a build/runtime error when the component is loaded, even if totalTokenUsage is 0 (the import itself fails). [fixable]
| if (tasks.length === 0) return; | ||
| const interval = setInterval(() => setNow(Date.now()), 60_000); | ||
| return () => clearInterval(interval); | ||
| }, [tasks]); |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The timer useEffect has [tasks] as its dependency. Since tasks is a Zustand store selector returning a new array reference on every store update (each WS event), the interval is torn down and recreated on every task mutation. This is wasteful. The guard tasks.length === 0 only needs to know whether tasks exist, so the dependency should be tasks.length > 0 (via a separate boolean state or ref) or the effect should use a ref to avoid churn. [fixable]
| const elapsed = now - completedAt; | ||
| if (elapsed < FADE_START_MS) return 1; | ||
| if (elapsed >= FADE_END_MS) return 0; | ||
| // Linear fade from 1 → 0.5 between 5min and 30min |
There was a problem hiding this comment.
🔵 style: Comment says "Linear fade from 1 → 0.5" but the function actually fades from 1.0 down to just above 0.5, then jumps to 0 (hidden via task-node--hidden) at the 30-minute boundary. The comment should mention the final jump to 0 / hidden to avoid surprising a future reader. [fixable]
Summary
formatElapsedfrom SessionOverview → reuses existingformatRelativeTime; addedformatDurationfor active task elapsed displayFiles changed (7)
lib/formatTime.tsformatDuration()components/SessionOverview.tsxformatRelativeTimestyles/global.css--task-*color vars, card/tier/fade/loop styleshooks/useTaskBoard.tscomponents/TaskNode.tsxcomponents/LoopControls.tsxpages/TaskBoard.tsxTest plan
Design spec:
mgmt/local_features/atb-redesign/spec.md🤖 Generated with Claude Code