fix(web): task durations over an hour no longer wrap in the tasks panel#13472

merged4
main
GitHub fork: fix/tasks-panel-duration-wrap
openedSep 24, 2026byJjamesvillarrubia
View on GitHubTwo-way sync enabledPR last imported 2d ago

Task durations over an hour could wrap onto two lines in the composer tasks panel. The duration now keeps a 48px minimum width and expands for longer values, preserving right alignment while the step label wraps beside it.

Fixes #13447. Preserves the task-row alignment from #12165. Original implementation by @jamesvillarrubia in this PR; retained after independent reassessment against current main.

Validation: file-scoped formatting and lint passed; independent Opus and Sol reviews found no defects. No new unit tests: duration formatting is already covered, and the change is visual layout only.

Live verification used the real integrated web app with an isolated plan fixture at 1280×800 and 390×844, with the task drawer expanded. No provider turn was started. The long duration changed from a wrapped 48×32px box to one line at about 59×16px; short durations and “now” stayed 48×16px. Long labels, missing durations, and opening/collapsing the drawer were also checked. This covers the shared web component used by the desktop renderer; no native Electron test is claimed.

Before — wide:

Before: long task duration wraps at 1280px

After — wide:

After: long task duration stays on one line at 1280px

Before — narrow:

Before: task duration wraps at 390px

After — narrow:

After: task duration stays aligned at 390px

Prepared with Claude Opus 5.5, Claude Fable 5.1, and GPT-6.1 Sol via T3 Code, coordinated with Codex.

Evidence

Captured by the maintainer evidence run (not the contributor screenshots). Before = merge-base on main 8d3b061, After = PR head 40410f7. Real web app (dev server, isolated home, seeded plan fixture), whole expanded tasks panel. Diff0 = pixelmatch threshold 0 (amplified); DiffT = pixelmatch threshold 0.1. Pixel counts follow each diff link. Duration boxes are CSS px (w×h).

Environment: fs-08, Ubuntu 24.04.5 LTS, Node v24.22.1, headless Chromium 148.0.7778.96 (Playwright), DPR 2, reduced motion, Date frozen at 2026-10-22T12:00:00Z, viewports 1440 and 390, light and dark.

RowViewportThemeBefore (8d3b061)After (40410f7)Diff0DiffTDuration boxes before → after
R1-sub-min390lightBeforeAfterdiff0 0 pxdiffT 0 px800ms 48.0x16; 4.2s 48.0x16; 42s 48.0x16; 59s 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R1-sub-min390darkBeforeAfterdiff0 0 pxdiffT 0 px800ms 48.0x16; 4.2s 48.0x16; 42s 48.0x16; 59s 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R1-sub-min1440lightBeforeAfterdiff0 0 pxdiffT 0 px800ms 48.0x16; 4.2s 48.0x16; 42s 48.0x16; 59s 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R1-sub-min1440darkBeforeAfterdiff0 0 pxdiffT 0 px800ms 48.0x16; 4.2s 48.0x16; 42s 48.0x16; 59s 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R2-minutes390lightBeforeAfterdiff0 0 pxdiffT 0 px2m 5s 48.0x16; 12m 34s 48.0x16; 59m 48.0x16; 1m 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R2-minutes390darkBeforeAfterdiff0 0 pxdiffT 0 px2m 5s 48.0x16; 12m 34s 48.0x16; 59m 48.0x16; 1m 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R2-minutes1440lightBeforeAfterdiff0 0 pxdiffT 0 px2m 5s 48.0x16; 12m 34s 48.0x16; 59m 48.0x16; 1m 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R2-minutes1440darkBeforeAfterdiff0 0 pxdiffT 0 px2m 5s 48.0x16; 12m 34s 48.0x16; 59m 48.0x16; 1m 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R3-under-hour390lightBeforeAfterdiff0 0 pxdiffT 0 px59m 59s 48.0x16; 59m 59s 48.0x16 → 48.0x16; 48.0x16
R3-under-hour390darkBeforeAfterdiff0 0 pxdiffT 0 px59m 59s 48.0x16; 59m 59s 48.0x16 → 48.0x16; 48.0x16
R3-under-hour1440lightBeforeAfterdiff0 0 pxdiffT 0 px59m 59s 48.0x16; 59m 59s 48.0x16 → 48.0x16; 48.0x16
R3-under-hour1440darkBeforeAfterdiff0 0 pxdiffT 0 px59m 59s 48.0x16; 59m 59s 48.0x16 → 48.0x16; 48.0x16
R4-just-over390lightBeforeAfterdiff0 0 pxdiffT 0 px1h 48.0x16; 1h 1m 1s 48.0x16; 1h 2m 5s 48.0x16; 1h 23m 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R4-just-over390darkBeforeAfterdiff0 0 pxdiffT 0 px1h 48.0x16; 1h 1m 1s 48.0x16; 1h 2m 5s 48.0x16; 1h 23m 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R4-just-over1440lightBeforeAfterdiff0 0 pxdiffT 0 px1h 48.0x16; 1h 1m 1s 48.0x16; 1h 2m 5s 48.0x16; 1h 23m 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R4-just-over1440darkBeforeAfterdiff0 0 pxdiffT 0 px1h 48.0x16; 1h 1m 1s 48.0x16; 1h 2m 5s 48.0x16; 1h 23m 48.0x16 → 48.0x16; 48.0x16; 48.0x16; 48.0x16
R5-multi-hour390lightBefore R5-multi-hour 390 lightAfter R5-multi-hour 390 lightdiff0 16504 pxdiffT 3801 px2h 30m 15s 48.0x32; 12h 34m 56s 48.0x32; 23h 59m 59s 48.0x32; 42s 48.0x16 → 54.8x16; 60.8x16; 60.8x16; 48.0x16
R5-multi-hour390darkBefore R5-multi-hour 390 darkAfter R5-multi-hour 390 darkdiff0 78879 pxdiffT 62186 px2h 30m 15s 48.0x32; 12h 34m 56s 48.0x32; 23h 59m 59s 48.0x32; 42s 48.0x16 → 54.8x16; 60.8x16; 60.8x16; 48.0x16
R5-multi-hour1440lightBefore R5-multi-hour 1440 lightAfter R5-multi-hour 1440 lightdiff0 20944 pxdiffT 3803 px2h 30m 15s 48.0x32; 12h 34m 56s 48.0x32; 23h 59m 59s 48.0x32; 42s 48.0x16 → 54.8x16; 60.8x16; 60.8x16; 48.0x16
R5-multi-hour1440darkBefore R5-multi-hour 1440 darkAfter R5-multi-hour 1440 darkdiff0 159539 pxdiffT 133226 px2h 30m 15s 48.0x32; 12h 34m 56s 48.0x32; 23h 59m 59s 48.0x32; 42s 48.0x16 → 54.8x16; 60.8x16; 60.8x16; 48.0x16
R6-mixed-live390lightBefore R6-mixed-live 390 lightAfter R6-mixed-live 390 lightdiff0 8723 pxdiffT 1501 px42s 48.0x16; 12m 34s 48.0x16; 2h 30m 15s 48.0x32 → 48.0x16; 48.0x16; 54.8x16
R6-mixed-live390darkBefore R6-mixed-live 390 darkAfter R6-mixed-live 390 darkdiff0 32324 pxdiffT 20777 px42s 48.0x16; 12m 34s 48.0x16; 2h 30m 15s 48.0x32 → 48.0x16; 48.0x16; 54.8x16
R6-mixed-live1440lightBefore R6-mixed-live 1440 lightAfter R6-mixed-live 1440 lightdiff0 13163 pxdiffT 1505 px42s 48.0x16; 12m 34s 48.0x16; 2h 30m 15s 48.0x32 → 48.0x16; 48.0x16; 54.8x16
R6-mixed-live1440darkBefore R6-mixed-live 1440 darkAfter R6-mixed-live 1440 darkdiff0 65624 pxdiffT 44459 px42s 48.0x16; 12m 34s 48.0x16; 2h 30m 15s 48.0x32 → 48.0x16; 48.0x16; 54.8x16
R7-long-names390lightBefore R7-long-names 390 lightAfter R7-long-names 390 lightdiff0 20432 pxdiffT 5410 px2h 30m 15s 48.0x32; 12m 34s 48.0x16; 12h 34m 56s 48.0x32 → 54.8x16; 48.0x16; 60.8x16
R7-long-names390darkBefore R7-long-names 390 darkAfter R7-long-names 390 darkdiff0 44393 pxdiffT 24793 px2h 30m 15s 48.0x32; 12m 34s 48.0x16; 12h 34m 56s 48.0x32 → 54.8x16; 48.0x16; 60.8x16
R7-long-names1440lightBefore R7-long-names 1440 lightAfter R7-long-names 1440 lightdiff0 26965 pxdiffT 6543 px2h 30m 15s 48.0x32; 12m 34s 48.0x16; 12h 34m 56s 48.0x32 → 54.8x16; 48.0x16; 60.8x16
R7-long-names1440darkBefore R7-long-names 1440 darkAfter R7-long-names 1440 darkdiff0 74256 pxdiffT 42981 px2h 30m 15s 48.0x32; 12m 34s 48.0x16; 12h 34m 56s 48.0x32 → 54.8x16; 48.0x16; 60.8x16

Duration-column crops for every row, plus all diffs: table.md. Fixture matrix: matrix.md. Driver: capture.cjs, fixture: fixture.json.

What else changed (besides the duration column):

  • R5, R6, R7: each row holding a multi-hour duration (2h 30m 15s, 12h 34m 56s, 23h 59m 59s) drops from 40px to 24px tall, because the duration no longer wraps (48×32 → 54.8–60.8×16). The drawer gets shorter and content below moves up.
  • Labels beside a widened duration lose 6.8px or 12.8px of width. In R7 row 1 the label rewraps at a different word with the same line count.
  • No horizontal overflow on either side at 390 or 1440.
  • R1–R4 (under a minute through 1h 23m, including 59m 59s and 1h 1m 1s) and R8 (collapsed badge, which shows no durations): 0 changed pixels at threshold 0.
  • Dark-theme diff counts are higher only because the shorter after image is padded to the before height.
Verification log
  • Head captured: 40410f7 (main merged into the branch). Before: merge-base 8d3b061.
  • Proof audit pass 1 (Claude Opus 5.5): matrix built, formatDuration strings measured by running the source. Gaps: the live "now" row can't be seeded, and the wrap point for 8-character durations was estimated (measurement later showed 1h 1m 1s fits in 48px).
  • Proof audit pass 2 (Claude Opus 5.5): all 240 links resolve as PNGs, pixel counts and boxes match summary.json, worktree SHAs match the two above. One capture (R5, 1440, light, before) rebuilt from the uploaded driver: 0 px different from the uploaded image. Gap: the uploaded artifacts themselves don't record the SHAs; this section does.
  • Review A, Claude Sonnet 5.5: VERDICT: MERGE. Smallest correct fix: the row is a grid with an auto column, so whitespace-nowrap stops the squeeze and min-w-12 keeps the 48px column for short values. No regression at 390.
  • Review B, Claude Opus 5.5: VERDICT: MERGE. Same conclusion. vp fmt --check and vp lint pass on the file. Mobile shows only an x/y summary, so no per-step duration exists there to fix.
  • CI on 40410f7: 33/33 checks successful, 0 pending.
  • Unverified: R6 live "now" label. A seeded run is settled at server start, so that row is blank on both sides. "now" is three characters and this change doesn't affect it.
  • Model/harness: Claude Opus 5.5 (lead, audits, capture, review B) and Claude Sonnet 5.5 (review A) via T3 Code on Claude Code.

Checks

All checks passed

Added 1 commit

  • fix(web): task durations over an hour no longer wrap in the tasks panel865f852
Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 865f8525c0387926b76ea126b717f0edcd5201e9 and 40410f7e59e95e0225e9eab9490e6d85a250cf22.

📒 Files selected for processing: 1
  • apps/web/src/components/chat/ComposerTasksBadge.tsx
🚧 Files skipped from review as they are similar to previous changes: 1
  • apps/web/src/components/chat/ComposerTasksBadge.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📝 Walkthrough

The task duration label now uses a minimum width instead of a fixed width. Its contents do not wrap.

Changes: Task duration label

Layer / File(s)Summary
Duration label styling
apps/web/src/components/chat/ComposerTasksBadge.tsx
The duration label uses a minimum width and prevents its contents from wrapping.

Priority: ⬇️ Low

Estimated code review effort: 1 (Trivial) | ~3 minutes

Change: Bug fix · Severity of issue fixed: Low

Suggested reviewers: juliusmarminge

Merge Risk: ⚪ Minimal · up to 40410

The duration layout change is mergeable after normal checks.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main web layout fix for task durations that exceed one hour.
Linked Issues checkPassedIssue #13447 requires completed-task durations over one hour to avoid wrapping when space is available. The PR changes the duration element in apps/web/src/components/chat/ComposerTasksBadge.tsx fro…
Out of Scope Changes checkPassedThe whole-PR diff contains one class change on the task-duration element in apps/web/src/components/chat/ComposerTasksBadge.tsx. The change directly supports issue #13447. No unrelated changes are p…
Description checkPassedThe pull request description is complete and focused. It explains the problem and change, links the related issue and prior alignment work, documents formatting and lint checks, provides detailed manu…
Full details: Linked Issues check

Explanation

Issue #13447 requires completed-task durations over one hour to avoid wrapping when space is available. The PR changes the duration element in apps/web/src/components/chat/ComposerTasksBadge.tsx from fixed w-12 to min-w-12 and adds whitespace-nowrap. This keeps the 48px minimum, permits leftward expansion, and prevents duration wrapping.

Full details: Out of Scope Changes check

Explanation

The whole-PR diff contains one class change on the task-duration element in apps/web/src/components/chat/ComposerTasksBadge.tsx. The change directly supports issue #13447. No unrelated changes are present in the reviewed diff.

Full details: Description check

Explanation

The pull request description is complete and focused. It explains the problem and change, links the related issue and prior alignment work, documents formatting and lint checks, provides detailed manual verification with before/after screenshots across viewports and themes, states unverified coverage, and identifies the agent models and harness. The content maps to the required template even though the template headings are not repeated verbatim.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

Synced from GitHub

Approvability

Verdict: Approved at 865f852

Macroscope's review found this PR approvable — This is a focused one-line UI bug fix that prevents long task durations from wrapping while preserving the existing layout for shorter values. It has no schema, deployment, security, billing, authentication, product-default, or static-analysis implications.

No code changes detected at 8922e95. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

Added 1 commit

  • Merge branch 'main' into fix/tasks-panel-duration-wrap748f207

Taking over this one. I’ll reduce the tests to the useful coverage, update the implementation against current main, and verify it before publishing the changes.

Added 2 commits

  • Merge remote-tracking branch 'origin/main' into takeover/134728922e95
  • Merge branch 'main' into fix/tasks-panel-duration-wrap40410f7