Skip to content

fix(board): review findings in the archive cap, drag path and docs - #350

Merged
simion merged 3 commits into
simion:mainfrom
lymanzhao:fix/board-review-findings
Sep 30, 2026
Merged

simion merged 3 commits into
simion:mainfrom
lymanzhao:fix/board-review-findings

Conversation

@lymanzhao

Copy link
Copy Markdown
Contributor

Nine findings from a review pass over the kanban board that shipped in #339.

Correctness

  • The Unlimited archive limit rendered an empty Archived column: recentArchived sliced a non-finite limit to zero while the badge showed the full count.
  • A task archived, deleted or moved project mid-drag let the drop handler merge dead ids into the store's task list, so a live card vanished from tasks, or undefined itself did and the next render threw.
  • The reorder preview was keyed by projectId alone, so every other same-project group applied it, rendered zero cards, and lit a drop ring for the whole drag.
  • The preview and its ring were never cleared when the pointer left the origin group, so the drop-lands-here signal outlived the gesture it described.
  • The Inactive rail lit every row on any settle/createPr target, instead of only the row whose column the hovered command belongs to.
  • boardLanes counted a task whose project had left the profile, forcing agent dividers onto columns whose visible cards were a single agent.
  • The custom archive-limit input stored 0 when cleared (Number("") is 0), emptying the Archived column mid-edit and sticking a 0 back into the field.
  • docs/ui.md said project sub-headers hide on single-project groups and that exactly two drags are wired; the code always renders the headers and wires four commands.

Performance

  • setDrag wrote a fresh snapshot per pointermove and nothing in the board tree was memoized, so a drag reconciled every column, group and card at input frequency in WKWebView.
  • Columns, groups and cards are memo-ized now, ctx/workPrefs/handlers are identity-stable, and the four drop targets are module singletons, so a move that only moves the ghost re-renders BoardView itself and stops there.

Testing

  • tsc -b clean.
  • npm test 2561 passing, including new unit cases for the Infinity slice, the resolver-to-render bridge, and mergeReorderedGroup.
  • Rebuilt e2e binary: board spec 8/8, settings spec 101/101 (fresh seed).
  • Manually verified by the author before opening.
  • The drag-path fixes (preview keying, stale-id snap-back) are covered by the existing reorder/snap-back/rail e2e cases only; the multi-agent-same-column blanking regression has no automated test of its own.

Refs #318

🤖 Generated with Claude Code

lymanzhao and others added 3 commits September 30, 2026 21:28
…on#318)

Findings from a review pass over the merged kanban board:

- recentArchived treated a non-finite limit as zero, so the Unlimited
  mode rendered an empty Archived column under a badge showing the
  full count. Slice to tasks.length instead; the unit matrix now
  bridges resolveBoardArchiveLimit into recentArchived so the seam
  stays covered.
- The drop handler merged drag-time preview ids into the drop-time
  task list unvalidated, so a task archived, deleted or moved project
  mid-drag consumed a store slot with a dead id: a live card vanished
  from tasks, or undefined itself did and the next render threw. The
  merge now lives in mergeReorderedGroup, returns null on any stale
  id, and the drop snaps back instead of writing.
- The reorder preview was keyed by projectId alone, so every other
  same-project group in any lane or column applied it and rendered
  zero cards under a lit drop ring for the whole drag. The preview
  now carries lane and column, and only the origin group applies it.
- Nothing cleared the preview when the pointer left the origin group,
  so the drop-lands-here ring outlived the gesture it described.
- setDrag wrote a fresh snapshot per pointermove and nothing in the
  board tree was memoized, so a drag reconciled every column, group
  and card at input frequency. Columns, groups and cards are
  memoized now, ctx/workPrefs/handlers are identity-stable, and the
  four drop targets are module singletons, so a move that only moves
  the ghost re-renders BoardView itself and stops there.
- The Inactive rail lit every row on any settle/createPr target; a
  row now lights only when the hovered command is its own column's.
- boardLanes counted a task whose project left the profile, forcing
  agent dividers onto columns whose visible cards were one agent; it
  reads the filtered liveTasks now.

Refs simion#318

Co-Authored-By: Claude Code <noreply@anthropic.com>
…imion#318)

Number("") is 0, so clearing the custom Kanban archived-column limit
field to retype it stored 0, emptied the Archived column mid-edit, and
put a stuck "0" back into the controlled field. The input keeps a
local draft now; the store only ever sees a parseable number, and blur
returns the stored value.

Refs simion#318

Co-Authored-By: Claude Code <noreply@anthropic.com>
The Kanban section still said project sub-headers hide on
single-project groups (they have rendered always since the GH simion#318
feedback landed) and that exactly two drags are wired (drop-on-Settled
and drop-on-In-review shipped two more commands). Both sentences now
describe the code a reader will find.

Refs simion#318

Co-Authored-By: Claude Code <noreply@anthropic.com>

@simion simion left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

All nine findings are real and correctly fixed, but ctx in BoardView is still a plain object literal instead of being memoized, so it changes identity on every pointermove and defeats the memoization this PR's performance section relies on. Wrap it in useMemo over [agents, useBranchAsTaskName, workPrefs] before merging. Thanks for the contribution!

@simion simion left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Verified locally on top of main (9ce447e): clean merge (only docs/ui.md auto-merged), tsc -b clean, npm test 2568 passing, board spec 8/8 and settings spec 101/101 on a rebuilt e2e binary with a fresh seed.

The three findings that land in code I wrote are all real, and the rail one is the sharpest:
each row of the Inactive column carries its own data-board-cell/data-column so hiding a column never takes its command with it,
and I never checked that only the hovered row lights up.
Making each DragTarget a module singleton whose kind names exactly one column fixes the highlight and the identity churn in one move, which is the right shape.

mergeReorderedGroup returning null rather than writing a stale merge is the correct call.
Worth noting it also covers the empty-preview case: previewIds.every(...) is vacuously true on [], and inserted then stays false, so that returns null too.

Thanks for going back over your own feature this carefully, the memoization pass especially.

@simion
simion merged commit 6099f2d into simion:main Sep 30, 2026
6 of 7 checks passed
simion added a commit that referenced this pull request Sep 30, 2026
…rag sees

#350 fixed a reorder preview keyed by projectId alone: every other group of
the same project applied it, matched none of the ids, and rendered zero cards
under a lit drop ring for the length of the gesture. It shipped without a test
of its own, and the PR said so.

The reason there was none is that the evidence does not survive the gesture.
On pointerup the preview clears and the cards come back, so a whole-gesture
drag, which is all `pointerDrag` could do, sees a correct board before and a
correct board after. `pointerDrag` now takes `hold`, which stops one event
short and leaves the pointer down over the target, and `pointerRelease`
finishes it somewhere harmless.

The new case holds a drag inside the fakeagent lane and asserts the same
project's fakecapture lane, in the same column, still renders its card. It
releases over a column that is not a drop target, so nothing is written and
the case leaves the board as it found it.

Verified by control, not by the suite going green: re-keying the preview on
projectId alone makes this case and only this case fail, with the other lane
rendering [] instead of its card. The first control attempt passed with the
bug supposedly reinstated, because the patch script used str.replace with no
assertion and silently matched nothing. A control that cannot fail is worth
exactly as much as no control.

Refs #350

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBZ9SY1zTBpPW33QAekZC1
@simion

simion commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Merged. Thank you for this one in particular: going back over your own feature with this much rigour is rarer than writing it in the first place, and seven of the nine findings are things a reviewer reading the diff cold would not have caught.

The rail finding is the one I am most glad you chased.
Each row of the Inactive column carries its own data-board-cell/data-column precisely so that hiding a column never takes its command with it, and I never checked that only the hovered row lights up.
Turning each DragTarget into a module singleton whose kind names exactly one column fixes the highlight and the identity churn in the same move, which is the right shape rather than a patch over the symptom.

I picked up the gap you flagged yourself, the same-project multi-lane blanking, in 150628d.
It needed a new e2e primitive: the evidence does not survive the gesture, since pointerup clears the preview and the cards come back, so a whole-gesture drag sees a correct board before and after.
pointerDrag now takes hold, which stops one event short and leaves the pointer down, and pointerRelease finishes it over a non-target so nothing is written.
The case holds a drag in one lane and asserts the project's other lane in the same column still renders its card.

Confirmed by control rather than by the suite going green: re-keying the preview on projectId alone makes that case, and only that case, fail with the other lane rendering [].
Worth mentioning that my first control attempt passed with the bug supposedly reinstated, because the patch script used str.replace and silently matched nothing.

One small note on mergeReorderedGroup, not a change request: returning null rather than writing a stale merge is the right call, and it also covers the empty-preview case for free.
previewIds.every(...) is vacuously true on [], inserted then stays false, and that returns null too.

Verified before merging: clean merge onto main, tsc -b clean, npm test 2568 passing, board spec 8/8 and settings 101/101 on a rebuilt e2e binary.

@lymanzhao
lymanzhao deleted the fix/board-review-findings branch October 1, 2026 04:16
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.

2 participants