Skip to content

feat(ui): task board redesign with attention sorting#314

Merged
dimakis merged 2 commits into
mainfrom
session/2026-05-04-91e632862647
May 9, 2026
Merged

feat(ui): task board redesign with attention sorting#314
dimakis merged 2 commits into
mainfrom
session/2026-05-04-91e632862647

Conversation

@dimakis
Copy link
Copy Markdown
Owner

@dimakis dimakis commented May 5, 2026

Summary

  • State-colored cards: 2-line task cards with status-colored left borders (purple active, amber review, red blocked/failed, green done) and T1 background tints
  • Attention-tier sorting: blocked/review/failed tasks surface to top; done tasks sort newest-first; "Tiers/Tree" toggle switches between sorted and natural order
  • Contextual loop controls: idle = compact expandable trigger, running = inline 2-line bar with progress + token count, review = amber approval card
  • Progressive fade: done tasks dim to 50% after 5min, hide after 30min (60s recompute timer)
  • Shared utilities: extracted formatElapsed from SessionOverview → reuses existing formatRelativeTime; added formatDuration for active task elapsed display

Files changed (7)

File Change
lib/formatTime.ts +formatDuration()
components/SessionOverview.tsx Use shared formatRelativeTime
styles/global.css 7 --task-* color vars, card/tier/fade/loop styles
hooks/useTaskBoard.ts Sorting, fade, displayMeta, attendCounts, tokenUsage
components/TaskNode.tsx 2-line cards, context line, compact children
components/LoopControls.tsx Contextual idle/running/review rendering
pages/TaskBoard.tsx Wire sorted tasks, show-all toggle

Test plan

  • 29/29 existing tests pass (TaskNode, SessionOverview, useSessionOverview)
  • 0 frontend TypeScript errors
  • Visual verification: idle state shows compact trigger + tier-sorted task tree
  • Visual verification: running state shows inline progress bar with token count
  • Visual verification: T1 items (review/blocked/failed) have colored borders + background tint
  • Visual verification: done tasks fade over time

Design spec: mgmt/local_features/atb-redesign/spec.md

🤖 Generated with Claude Code

Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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]
  • 🔵 bugs (L51): 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]
  • 🔵 bugs (L222): 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]
  • 🔵 style (L237): 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]
  • 🟡 missing_tests: The PR adds ~130 lines of pure logic (getAttendTier, computeFadeOpacity, buildDisplayMeta, sortByAttendTier, sumTokenUsage) but the existing useTaskBoard.test.ts has 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 formatTokens utility has no test file. Edge case: formatTokens(999500) returns "1000k" instead of "1.0M" since it hits the >= 1000 branch 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 formatDuration function 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]

Comment thread frontend/src/hooks/useTaskBoard.ts Outdated

// Fade timer — recompute every 60s for done task opacity
useEffect(() => {
const hasDone = tasks.some(
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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));
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]

Comment thread frontend/src/hooks/useTaskBoard.ts Outdated
}, [tasks, now]);

// Attend counts
const attendCounts = useMemo(() => {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]

Comment thread frontend/src/components/TaskNode.tsx Outdated
@@ -138,7 +203,9 @@ export function TaskNode({
key={child.id}
task={child}
depth={depth + 1}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]

dimakis and others added 2 commits May 9, 2026 14:38
…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>
@dimakis dimakis force-pushed the session/2026-05-04-91e632862647 branch from dad3da9 to c40a661 Compare May 9, 2026 13:40
@dimakis dimakis merged commit 2b5a9ff into main May 9, 2026
1 check passed
@dimakis dimakis deleted the session/2026-05-04-91e632862647 branch May 9, 2026 13:42
Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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]

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 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]
  • 🔵 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 formatDuration function has no tests. It also doesn't handle negative input (e.g., if now - claimedAt is 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';
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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]);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant