Skip to content

Task pool board card: browse the pool and grab a task by hand - #278

Merged
DavidLeuter merged 6 commits into
devfrom
feature/browse-and-grab-issues
Sep 29, 2026
Merged

DavidLeuter merged 6 commits into
devfrom
feature/browse-and-grab-issues

Conversation

@DavidLeuter

Copy link
Copy Markdown
Collaborator

Summary

Until now a hire could only claim a starter task through the buddy. This adds a "Task pool" card to the board: the whole live pool, best fit first, grabbable directly — with the buddy one press away for anyone who wants help choosing.

Depends on SprintStartProject/sprintstart-backend#264 (new TASK_POOL card kind, and POST /me/goal pinning the current-task card). Without it the card simply does not appear.

The card (board/components/TaskPoolCard.tsx)

  • Every live task in rank order (never re-sorted client-side), with the same reasons the buddy reads. Badges: Best fit, task type, Someone may be on this.
  • Search, type filter chips, Hide taken; the list scrolls inside the card. More expands summary and rationale.
  • Grab this (task-pool/GrabTaskButton) with a two-press confirm that names what it replaces ("Swap 'X' for this?") → POST /me/goal → board re-read. The task the hire is on shows You're on this one instead.
  • Buddy stays in reach: Not sure? Help me choose on the card, Is this a good fit? on every task — both open the buddy with a real question drafted.

Switch instead of dismissal

Dismissing a card is sticky on the server — neither the hire nor the buddy can bring it back. For the one surface that lets somebody grab without the buddy, a single stray press would have been permanent. So the pool card works like the path strip: its X only hides it (with undo), and a new switch in the board's right rail brings it back. Stored in local storage per board (layout/taskPoolShown.ts), like the path strip.

Other cards

  • Good next tasks: Grab this replaces I want to work on this; Is this a good fit? goes to the buddy.
  • What you're working on: empty/closed states point at the task pool card; Help me choose for the buddy.
  • Removed the generic Ask your buddy about any of this link at the bottom of the board — it only navigated to /buddy with no question attached.

Verification

  • Back-merged dev; tsc -b, npm run lint, npm run build, npm run test (391 files / 3316 tests) all green. Changed files pass prettier --check.
  • New tests: TaskPoolCard.test.tsx (rank order, filters, two-press grab, current task marked, buddy hand-off, empty pool). Updated CurrentTaskCard, BoardGrid and BoardSubmenus tests for the new wording and the grab button.

🤖 Generated with Claude Code

DavidLeuter and others added 5 commits September 26, 2026 17:06
Until now a hire could only claim a starter task through the buddy. The
board now offers the whole live pool in a dialog: ranked by fit, with the
same reasons the buddy reads, searchable and filterable by type and by
whether somebody is already on it. A task is grabbed with a two-press
confirm that names the task it replaces.

The buddy stays one press away: "Help me choose" at the top of the
dialog and on the empty current-task card, "Is this a good fit?" on
every task.

Uses the existing GET /me/matches and POST /me/goal; no backend change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the browse dialog with a TASK_POOL card rendered from the
board's own read: the whole pool best fit first, search, type filter,
"hide taken", grab with a two-press confirm, and the buddy one press
away ("Help me choose", "Is this a good fit?").

The current-task and suggested-tasks cards point at the pool card
rather than opening anything. Grabbing re-reads the board only; the
separate /me/matches read is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dismissing a card is sticky on the server, and neither the hire nor the
buddy can bring one back. For the pool - the one way to grab a task
without the buddy - that made a single stray press permanent. Its X now
only hides it (with an undo toast), and a switch in the board's rail
brings it back. Kept in local storage, like the path strip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It only navigated to /buddy with no question attached. Every card that
has something to ask the buddy already does so with its own prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BabuPlk

BabuPlk commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

Read the diff at 5412930 against dev (merge-base bc53deb), together with the backend side (SprintStartProject/sprintstart-backend#264), and checked the contract between them: POST /me/goal?projectId=… with { taskId } matches claimGoalForMe, and the TASK_POOL content shape lines up with the new card kind.

CI: the pull_request run is green. The push run on the same SHA failed in npm run unit on BuddyComposerIsolation.test.tsx ("does not re-render … for a keystroke in the dock", expected 9 renders, got 12). This PR doesn't touch BuddyWidget or anything that test renders, and the same commit passed in the other run, so it looks like a timing flake in that render-count test rather than something introduced here. Probably worth its own issue though, since it will keep turning random pushes red.

1. A 409 on grab leaves the stale task in the pool

useGrabTask only invalidates the board on success. claimForMe reconciles the task against its source first and returns 409 if it's no longer LIVE. At that point we know the pool card is out of date, but the task stays in the list with a live "Grab this" button until the next board read. Invalidating the board in the 409 branch (or in onSettled) would make the task disappear right under the toast that says it's gone.

Small wording point in the same place: 409 means "not LIVE", which covers more than "closed where it lives" (retired, rejected by the PM). Something like "That task isn't open anymore, pick another one" would be accurate in every case.

2. Focus is lost on the first press

Pressing "Grab this" replaces the button with the confirm group, so the focused element is unmounted and keyboard focus falls back to <body>. A keyboard or screen-reader user then has to tab through the whole board again to find "Yes, grab it", and nothing announces that a question appeared. Moving focus to "Yes, grab it" (or "Cancel") when confirming flips, and back to "Grab this" on cancel, would fix it. Given #268 was a keyboard-focus hotfix, this seems worth doing before merge.

3. The rail switch shows even when there's no pool card

The new LayoutList switch is only disabled while !board. Until #264 is deployed, or on any board where the backend doesn't place a TASK_POOL card, the switch toggles aria-pressed and does nothing visible. I'd only render it (or enable it) when board.cards actually contains a TASK_POOL card.

Related to merge order: #264 is still open. The card just doesn't appear without it, which is fine, but "Good next tasks" now claims through POST /me/goal directly, and on current dev that endpoint doesn't pin the CURRENT_TASK card yet. So a hand grab would succeed without the board showing the task. Merging backend first avoids that window.

4. Two sources for "what am I on"

In TaskPoolCard, isCurrent comes from the pool content (currentTaskId), while the confirm text ("Swap 'X' for this?") comes from the CURRENT_TASK card in the cache via useCurrentTask. Usually they agree, but if the current-task card isn't on the board (e.g. dismissed before #264's pinning existed), the pool correctly marks "You're on this one" on one task and still asks "Make this your task?" on the others. Not a blocker. Just flagging that the pool content already knows the current task, so it could hand the title down instead.

5. Small things

  • useGrabTask invalidates queryKeys.board.all() although the project is known. useInvalidateBoard(projectId) exists for exactly this and would keep other projects' cached boards intact.
  • PoolTaskRow calls openAiBuddy directly for "Is this a good fit?", while SuggestedTasksCard uses <AskTheBuddy> for the same question. I assume this is to avoid its mt-3. If so, a className prop on AskTheBuddy would keep one mechanism, which is what its doc comment promises.
  • key={reason} in the reasons list will warn if the backend ever sends the same reason twice. Index or ${taskId}-${i} is safer.
  • The toast uses curly quotes (“${title}”) while the rest of the board copy uses straight ones.

6. Tests

TaskPoolCard.test.tsx covers the card itself well (rank order preserved under filters, two-press grab, current marker, buddy hand-off, empty pool). What's missing is the part that's new behaviour rather than new UI:

  • taskPoolShown.ts has no test. pathWindowFold.test.ts is the template (default true, per-board key, unreadable/foreign-version values fall back to shown, a storage that throws doesn't break anything).
  • The X-hides-instead-of-dismisses path in BoardPage: that pressing X on the pool card does not call dismissCard, that Undo brings it back, and that the rail switch toggles it. BoardPageUndo.test.tsx already has the setup for this, and since the server-side dismissal is exactly what this is meant to avoid, it's the one I'd want pinned.
  • A 409 on grab (see 1).

What's good here

  • Switch instead of dismissal is the right call and it's explained where someone will find it (taskPoolShown.ts, the rail comment, handleDismiss). A sticky server-side dismissal on the only non-buddy way to grab a task would have been a nasty trap.
  • The order is the backend's and is never re-sorted. Filters only hide. That keeps the pool and the buddy reading the same ranking, and the test checks it.
  • Two-press confirm that names what it replaces ("Swap 'X' for this?") keeps the confirm step the buddy route always had without forcing a conversation. The three-valued sourceHasAssignee ("Someone may be on this", only on true) is honest about what the tracker actually knows.
  • The doc comments in AskTheBuddy and SuggestedTasksCard were updated to the new reality instead of being left to contradict the code.
  • Removing the generic "Ask your buddy about any of this" link in favour of buddy entries with a real drafted question is a clear improvement.

Nice PR overall. I'd like 1 and 2 settled before merge and #264 to go in first; 3 is quick, the rest can be follow-ups.

- Re-read the project's board after every grab attempt (onSettled via
  useInvalidateBoard), so a task that turned out not to be open leaves
  the pool under the toast saying so. 409 copy no longer claims the
  task "closed where it lives" — it covers retired/removed too.
- Move focus to "Yes, grab it" when the confirm appears (described by
  the question) and back to "Grab this" on cancel or failure.
- Render the rail switch only on a board that has a TASK_POOL card.
- The pool card hands the current task's title to its grab buttons, so
  "Swap ... for this?" agrees with "You're on this one" even without a
  current-task card on the board.
- AskTheBuddy takes a className; the pool card uses it instead of
  calling openAiBuddy directly. Reasons keyed by position. Straight
  quotes in toast and confirm copy.
- Tests: taskPoolShown storage; BoardPage X hides without dismissing,
  Undo, rail switch, no switch without a pool card; focus handling;
  409 re-reads the board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DavidLeuter

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. All of it is addressed in bcbd3182:

1. Stale task after a 409: useGrabTask now re-reads the board in onSettled, so a task that turned out not to be LIVE disappears from the pool under the toast. The copy now says "That task isn't open anymore — pick another one.", which is accurate for closed, retired and removed tasks alike.

2. Focus: when the confirm appears, focus moves to Yes, grab it. That button is aria-describedby the question, so a screen reader reads "Swap … for this?" / "Make this your task?" with it. Focus goes back to Grab this on cancel or on a failed grab.

3. Rail switch: it's only rendered when board.cards contains a TASK_POOL card. Agreed on merge order: backend#264 first.

4. Two sources: the pool card now passes the current task's title (from its own currentTaskId) to every grab button. The confirm text and the You're on this one marker can't disagree anymore. The cached CURRENT_TASK card is only the fallback when the current task isn't in the live pool.

5. Small things:

  • useInvalidateBoard(projectId) instead of board.all().
  • AskTheBuddy takes a className and the pool card uses it, so there's one mechanism again.
  • Reasons are keyed by ${taskId}-${index}.
  • Straight quotes in the toast and the confirm.

6. Tests:

  • taskPoolShown.test.ts: round-trip, per-board key, default shown, unreadable/foreign version falls back to shown, storage that throws.
  • BoardPageTaskPool.test.tsx: X hides the card and dismissCard is never called (even past the undo window), Undo brings it back, the rail switch toggles it, no switch on a board without a pool card.
  • TaskPoolCard.test.tsx: focus moves to the confirm and back on cancel, the replaced title comes from the pool, and a 409 invalidates ["board", "p1"] and returns focus.

Full routine is green: lint, prettier on changed files, build, 393 files / 3328 tests.

On the BuddyComposerIsolation flake: agreed, it also failed on a dev push on 28.09. I've put it on the list as a separate task.

🤖 Generated with Claude Code

@BabuPlk BabuPlk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Checked bcbd3182. Everything from the review is addressed, and the way it's done reads well:

  • 409: onSettled with useInvalidateBoard(projectId) covers both success and the stale case, and the new copy is right for every non-LIVE state.
  • Focus: moving focus to "Yes, grab it" with aria-describedby on the question is exactly the fix. The wasConfirming ref keeps the first render from stealing focus.
  • Rail switch: now only rendered when there's a TASK_POOL card.
  • One source for "what am I on": currentTitle comes from the pool's own currentTaskId, and the cached card is only a fallback. Together with backend#264 now reviving a dismissed CURRENT_TASK on a grab, the case I described can't happen anymore.
  • Tests: taskPoolShown.test.ts and BoardPageTaskPool.test.tsx cover exactly the parts that were missing, including "dismissCard is never called, even past the undo window".

I ran it on a fresh npm ci at bcbd3182: tsc -b, npm run lint and npm run build are clean, and npm run unit passed 338 files / 3255 tests, plus BoardPage.a11y. Prettier passes on all changed files.

One small thing, not blocking: after a successful grab, focus goes back to "Grab this", but once the board re-read lands that button is replaced by "You're on this one", so focus falls to <body> again. Making that marker focusable (tabIndex={-1}) and focusing it on success would close the loop. Fine as a follow-up.

Approving. Merge after backend#264, as agreed.

@DavidLeuter
DavidLeuter merged commit cc051be into dev Sep 29, 2026
4 checks passed
@DavidLeuter
DavidLeuter deleted the feature/browse-and-grab-issues branch September 29, 2026 16:18
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