Skip to content

Comments

feat: Blocker Resolution View - Answer Agent Questions#390

Merged
frankbria merged 3 commits intomainfrom
feature/issue-333-blocker-resolution-view
Feb 19, 2026
Merged

feat: Blocker Resolution View - Answer Agent Questions#390
frankbria merged 3 commits intomainfrom
feature/issue-333-blocker-resolution-view

Conversation

@frankbria
Copy link
Owner

@frankbria frankbria commented Feb 19, 2026

Summary

Implements #333: Blocker Resolution View — a dedicated page for responding to agent questions and unblocking stuck tasks.

  • New /blockers route with inline answer forms and real-time polling
  • Sidebar notification badge showing open blocker count
  • Collapsible resolved blockers section with answer history
  • Full accessibility support (ARIA attributes, keyboard navigation)

Changes

New Files

  • web-ui/src/app/blockers/page.tsx — Page route following existing workspace pattern
  • web-ui/src/components/blockers/BlockerResolutionView.tsx — Main view with SWR polling (5s interval)
  • web-ui/src/components/blockers/BlockerCard.tsx — Individual blocker card with inline answer form
  • web-ui/src/components/blockers/ResolvedBlockersSection.tsx — Collapsible resolved blockers

Modified Files

  • web-ui/src/lib/api.ts — Added blockersApi.getAll() for listing blockers with optional filters
  • web-ui/src/components/layout/AppSidebar.tsx — Enabled blockers route + badge count with SWR polling

Test Files

  • web-ui/__tests__/components/blockers/ResolvedBlockersSection.test.tsx — 14 tests for collapsible section
  • Updated AppSidebar.test.tsx to reflect enabled blockers route

Acceptance Criteria

  • List all open blockers with task context
  • Blocker question prominently displayed
  • Shows timestamp metadata
  • Inline answer form (text area + submit)
  • Submit answer auto-resumes task execution (backend handles)
  • Option to skip/cancel blocker (keeps task BLOCKED)
  • Collapsed section for resolved blockers
  • Notification badge count on sidebar icon
  • Full context view (task ID + question context)

Test Plan

  • Build passes (next build — zero TypeScript errors)
  • All 317 unit tests passing (28 test suites)
  • Lint clean for all changed files
  • ResolvedBlockersSection: 14 tests covering toggle, badges, timestamps, accessibility
  • AppSidebar: 7 tests updated for enabled blockers route

Closes #333

Summary by CodeRabbit

  • New Features

    • New Blockers page and view to list open blockers, submit answers (with character counter), keyboard submit (Ctrl/Cmd+Enter), success and error feedback, and collapsible resolved section with status badges and timestamps.
    • Sidebar now shows an open-blocker count badge and Blockers navigation when a workspace is selected.
    • Relative-time formatting for blocker timestamps.
  • Tests

    • Added comprehensive tests for blocker UI, interactions, accessibility, and sidebar behavior.

Add dedicated /blockers page with inline answer forms, real-time polling,
and sidebar notification badge for open blocker count.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

Adds a Blocker Resolution feature: new page and client components for viewing/answering blockers, SWR-backed API client method, sidebar badge for open blockers, relative-time util, tests, and a minor icon mock + types path update.

Changes

Cohort / File(s) Summary
Blocker UI components
web-ui/src/components/blockers/BlockerCard.tsx, web-ui/src/components/blockers/BlockerResolutionView.tsx, web-ui/src/components/blockers/ResolvedBlockersSection.tsx
New client components: BlockerCard (answer form, submit flow, error handling), BlockerResolutionView (SWR fetching, open vs resolved split, retry/loading), ResolvedBlockersSection (collapsible resolved list, ARIA, timestamps).
Page & routing
web-ui/src/app/blockers/page.tsx
New client route/page BlockersPage that reads selected workspacePath, handles hydration, shows guidance when no workspace selected, or renders BlockerResolutionView.
API client
web-ui/src/lib/api.ts
Adds blockersApi.getAll(workspacePath, options?) with optional status/limit query params; imports BlockerStatus type.
Formatting util
web-ui/src/lib/format.ts
Adds formatRelativeTime(isoDate: string) to produce human-friendly relative timestamps used by blocker components.
Sidebar integration
web-ui/src/components/layout/AppSidebar.tsx
Re-enables Blockers nav item; tracks workspacePath; fetches open blocker count via SWR (/api/v2/blockers/sidebar?path=...) and shows a red badge when >0; updates workspace change handling.
Tests
web-ui/__tests__/components/blockers/BlockerCard.test.tsx, web-ui/__tests__/components/blockers/ResolvedBlockersSection.test.tsx, web-ui/__tests__/components/layout/AppSidebar.test.tsx
Adds comprehensive tests for BlockerCard and ResolvedBlockersSection (rendering, accessibility, interactions, timings); updates AppSidebar tests and SWR mocks to reflect Blockers enabled and badge behavior.
Mocks & types
web-ui/__mocks__/@hugeicons/react.js, web-ui/next-env.d.ts
Adds ArrowUp01Icon export to icon mock; adjusts route types import path from ./.next/dev/types/routes.d.ts to ./.next/types/routes.d.ts.

Sequence Diagram

sequenceDiagram
    actor User
    participant BlockerPage as BlockersPage
    participant BlockerResView as BlockerResolutionView
    participant BlockerCard as BlockerCard
    participant SWR as SWR Cache
    participant API as blockersApi

    User->>BlockerPage: Navigate to /blockers
    BlockerPage->>BlockerResView: Render(workspacePath)
    BlockerResView->>SWR: useSWR(/api/v2/blockers?path=...)
    SWR->>API: getAll(workspacePath)
    API-->>SWR: BlockerListResponse
    SWR-->>BlockerResView: data, mutate
    BlockerResView->>BlockerResView: derive openBlockers/resolvedBlockers
    BlockerResView->>BlockerCard: Render each open blocker
    User->>BlockerCard: Enter answer & submit
    BlockerCard->>API: answer(workspacePath, blockerId, text)
    API-->>BlockerCard: Success
    BlockerCard->>BlockerResView: onAnswered()
    BlockerResView->>SWR: mutate() to refetch
    SWR->>API: getAll(workspacePath)
    API-->>SWR: Updated BlockerListResponse
    SWR-->>BlockerResView: Updated data
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped to the sidebar and sniffed a clue,
I nudged open questions and stitched answers true.
Cards filled with replies, the blockers retreat,
I twitched my nose — unblocked tasks skip a beat. 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Blocker Resolution View - Answer Agent Questions' clearly and concisely summarizes the main change: implementing a Blocker Resolution View for answering agent questions.
Linked Issues check ✅ Passed The PR implements all major coding requirements from issue #333: Blocker Resolution View with open/resolved blockers, answer form submission, sidebar badge, polling mechanism, and all new components and API methods.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #333: new blocker components, API methods, routes, and tests. No unrelated modifications detected outside the Blocker Resolution feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-333-blocker-resolution-view

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

@macroscopeapp
Copy link
Contributor

macroscopeapp bot commented Feb 19, 2026

Add Blocker Resolution View to /blockers and enable sidebar Blockers link with live OPEN count to support answering agent questions

Introduce a blockers dashboard that fetches blockers via SWR, renders open items as actionable cards with submission and skip, and lists resolved items in a collapsible section. Add blockersApi.getAll for filtered fetching and formatRelativeTime for timestamps. Update the sidebar to show an enabled Blockers link with a red badge indicating the number of OPEN blockers.

📍Where to Start

Start with the top-level view in page.tsx, then review data flow and fetching in BlockerResolutionView.tsx.


Macroscope summarized 492f44e.

@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Blocker Resolution View (#333)

This is clean, well-structured frontend work. The component decomposition is logical, accessibility attributes are in place, and the SWR polling approach is appropriate. A few issues to address:


Bug: collapsed state in BlockerCard is a dead-end

Once the user clicks "Skip", setCollapsed(true) hides the answer form. But there is no UI element to reverse this — no "Show form" button, no indicator the card is in a skipped state. The blocker still shows OPEN (red border, Alert icon), giving the impression it needs attention, but there's no action available. Either add a way to re-expand the form, or remove the Skip/collapse concept entirely and let users leave blockers unanswered until they're ready.


Code duplication: formatRelativeTime defined twice

The function body in BlockerCard.tsx and ResolvedBlockersSection.tsx is identical. Extract it to web-ui/src/lib/format.ts (or a similar shared utility) and import from both components.


SWR cache invalidation gap for sidebar badge

The sidebar uses key /api/v2/blockers/sidebar?path=... (10s interval); the main view uses /api/v2/blockers?path=... (5s interval). When mutate() is called in BlockerResolutionView after answering a blocker, only the main view cache is invalidated. The sidebar badge count won't decrement until the next 10s poll.

Two clean fixes:

  • Share the same SWR key between sidebar and main view (no extra API call)
  • Or broadcast a mutate to the sidebar key alongside the main view key inside onAnswered

Fragile badge logic in AppSidebar

{label === 'Blockers' && openBlockerCount > 0 && (
  <span ...>{openBlockerCount}</span>
)}

Tying badge rendering to a display string is fragile. Consider adding an optional badgeCount?: number field to the NavItem interface, resolved before rendering, so the badge condition reads the data structure rather than matching a hardcoded label.


Missing test coverage

BlockerCard and BlockerResolutionView have no tests. Suggested cases for BlockerCard:

  • Successful submit flow (form → success state → onAnswered callback fires)
  • Error display when API call fails
  • Ctrl+Enter keyboard shortcut triggers submit
  • Submit button disabled while submitting or when answer is empty

The 14 tests for ResolvedBlockersSection are thorough — same standard would be good to apply to the other two components.


Minor: next-env.d.ts path change

The path changed from .next/dev/types/routes.d.ts to .next/types/routes.d.ts. This file is auto-generated (// NOTE: This file should not be edited). Verify this isn't an accidental local build artifact that will regress in CI; it should re-generate correctly on next build in any environment.


Minor: ANSWERED badge variant

const badgeVariant = blocker.status === 'RESOLVED' ? 'done' : 'in-progress';

ANSWERED blockers receive the in-progress variant, implying ongoing work. Since they appear in the resolved section, done (or a neutral variant) would be more semantically accurate for both statuses.


The core flow — SWR polling, inline answer submission, optimistic success state, resolved section collapsing, and accessibility attributes — is solid. The Skip dead-end and formatRelativeTime duplication are the two concrete items to resolve before merging.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
web-ui/__tests__/components/blockers/ResolvedBlockersSection.test.tsx (1)

91-142: Consider adding a test for blockers without an answer.

The component conditionally renders the answer block ({blocker.answer && ...}), but there's no explicit test verifying behavior when answer is null or undefined. This edge case could be worth covering to ensure the component handles resolved blockers that might not have an associated answer.

🧪 Suggested test case
it('renders without answer section when answer is null', async () => {
  const user = userEvent.setup();
  const blocker = makeBlocker({ answer: null });

  render(<ResolvedBlockersSection blockers={[blocker]} />);
  await user.click(screen.getByRole('button'));

  expect(screen.getByText('Which database should we use?')).toBeInTheDocument();
  expect(screen.queryByText('Use PostgreSQL for persistence.')).not.toBeInTheDocument();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/__tests__/components/blockers/ResolvedBlockersSection.test.tsx` around
lines 91 - 142, Add a unit test that verifies ResolvedBlockersSection handles
blockers with no answer: create a blocker via makeBlocker({ answer: null }),
render <ResolvedBlockersSection blockers={[blocker]} />, expand the section
(user.click on the role button) and assert the question text is present while
the expected answer text is absent (use screen.getByText for the question and
screen.queryByText for the answer). Name the test something like 'renders
without answer section when answer is null' to mirror other cases and keep
assertions consistent with existing tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web-ui/__tests__/components/blockers/ResolvedBlockersSection.test.tsx`:
- Around line 91-142: Add a unit test that verifies ResolvedBlockersSection
handles blockers with no answer: create a blocker via makeBlocker({ answer: null
}), render <ResolvedBlockersSection blockers={[blocker]} />, expand the section
(user.click on the role button) and assert the question text is present while
the expected answer text is absent (use screen.getByText for the question and
screen.queryByText for the answer). Name the test something like 'renders
without answer section when answer is null' to mirror other cases and keep
assertions consistent with existing tests.

- Extract formatRelativeTime to shared lib/format.ts (dedup)
- Add "Show form" button when Skip collapses answer form
- Fix SWR cache invalidation for sidebar badge after answering
- Use 'done' badge variant for all resolved-section blockers
@claude
Copy link

claude bot commented Feb 19, 2026

Follow-up Review

My previous review contained several incorrect observations that I should correct now that I've read the diff more carefully.

Corrections to my previous review:

  • collapsed dead-end — I was wrong. There IS a "Show answer form" button in the {isOpen && collapsed} branch that calls setCollapsed(false). The skip/re-expand flow works correctly.
  • formatRelativeTime duplication — I was wrong. format.ts is a new shared utility file in this PR, and both BlockerCard and ResolvedBlockersSection import from it. No duplication.
  • SWR cache invalidation gap — I was wrong. revalidateAll explicitly calls both mutate() for the main view and globalMutate(/api/v2/blockers/sidebar?path=...) for the sidebar badge. Both caches are invalidated on answer.
  • ANSWERED badge variant — I was wrong. The component uses 'done' as const for all resolved/answered blockers — not in-progress.

Apologies for the noise in the previous review.


Issues from the previous review that still stand:

Fragile badge logic in AppSidebar (still unresolved):

{label === 'Blockers' && openBlockerCount > 0 && (
  <span ...>{openBlockerCount}</span>
)}

This ties badge rendering to a hardcoded display string. If the label is ever renamed or internationalized, the badge silently breaks. An optional badgeCount?: number field on the NavItem interface would be cleaner.

Missing tests for BlockerCard and BlockerResolutionView (still unresolved): These components have no test coverage. At minimum for BlockerCard: submit success (success state → onAnswered fires), API error display, Ctrl+Enter shortcut, and submit button disabled when answer is empty.


New issues identified in this pass:

setTimeout not cleaned up on unmount in BlockerCard:

setTimeout(() => onAnswered(), 1500);

If the component unmounts (e.g., the page navigates away) before the 1.5 s delay, this fires on an unmounted component. Store the timeout ID and cancel it in a useEffect cleanup.

Unnecessary wrapper function on onAnswered in BlockerResolutionView:

onAnswered={() => revalidateAll()}

revalidateAll is already useCallback-memoized. Pass it directly as onAnswered={revalidateAll} to avoid recreating the arrow function on every render.

err as ApiError type cast in BlockerCard can mask unexpected shapes:

const apiErr = err as ApiError;
setError(apiErr.detail || 'Failed to submit answer');

If the error is a TypeError or network error, apiErr.detail is undefined and the fallback fires — so the UX is acceptable. A minor improvement would be to check err instanceof Error first to safely access message as an additional fallback, but the current code is not broken.


The core implementation is solid. The two actionable items worth resolving before merge are the setTimeout cleanup and the missing BlockerCard/BlockerResolutionView tests.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
web-ui/src/lib/format.ts (1)

5-16: Consider adding defensive handling for invalid date inputs.

If isoDate is malformed or empty, new Date(isoDate) produces an Invalid Date, causing getTime() to return NaN. This propagates through the arithmetic and results in "NaNd ago" being displayed.

Since this is a shared utility, consider guarding against this edge case:

🛡️ Proposed defensive fix
 export function formatRelativeTime(isoDate: string): string {
   const now = new Date();
   const date = new Date(isoDate);
+  if (isNaN(date.getTime())) {
+    return '';
+  }
   const diffMs = now.getTime() - date.getTime();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/lib/format.ts` around lines 5 - 16, The formatRelativeTime
function can produce "NaNd ago" for invalid isoDate inputs; update
formatRelativeTime to defensively handle malformed or empty isoDate by
validating the parsed Date (e.g., checking isNaN(date.getTime()) or Date.parse
result) before using now/getTime/diffMs, and return a safe fallback (empty
string or a clear placeholder like "Unknown date" or "just now") when the input
is invalid; ensure the check occurs immediately after creating date in
formatRelativeTime and short-circuit the rest of the arithmetic if invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web-ui/src/lib/format.ts`:
- Around line 5-16: The formatRelativeTime function can produce "NaNd ago" for
invalid isoDate inputs; update formatRelativeTime to defensively handle
malformed or empty isoDate by validating the parsed Date (e.g., checking
isNaN(date.getTime()) or Date.parse result) before using now/getTime/diffMs, and
return a safe fallback (empty string or a clear placeholder like "Unknown date"
or "just now") when the input is invalid; ensure the check occurs immediately
after creating date in formatRelativeTime and short-circuit the rest of the
arithmetic if invalid.

- Clean up setTimeout on unmount in BlockerCard (memory leak fix)
- Pass revalidateAll directly instead of wrapping in arrow function
- Add 15 BlockerCard tests (submit flow, error, skip/expand, a11y)
@claude
Copy link

claude bot commented Feb 19, 2026

Review: Blocker Resolution View (#390)

This is a well-structured implementation that integrates cleanly with the existing UI patterns. The component decomposition, SWR polling strategy, accessibility attributes, and test coverage are all solid. Below are observations that build on the prior CodeRabbit reviews.


Carry-forward from prior reviews (not yet addressed)

  1. formatRelativeTime — invalid date input (flagged by CodeRabbit): new Date('') or a malformed string produces NaN, which propagates to display "NaNd ago". A guard on isNaN(date.getTime()) before the arithmetic would eliminate this.

  2. ResolvedBlockersSection — null answer test (flagged by CodeRabbit): the conditional {blocker.answer && ...} has no test covering the null/undefined case.


Additional observations

ResolvedBlockersSection.tsxbadgeVariant is always 'done'

const badgeVariant = 'done' as const;
// …
<Badge variant={badgeVariant}>{blocker.status}</Badge>

ANSWERED and RESOLVED statuses receive identical styling. If the design treats them as equivalent, the variable is unnecessary and an inline variant="done" would be clearer. If there's intent to differentiate them visually in future, a conditional is needed. Either way, the indirection currently adds noise without purpose.


AppSidebar.tsx — badge count depends on by_status?.OPEN

const openBlockerCount = blockerData?.by_status?.OPEN ?? 0;

This works correctly given the ?? 0 fallback, but it's worth confirming that by_status is always present in the BlockerListResponse shape when blockers exist. If the backend omits the field on empty results, the count will silently stay at 0 even if blockerData.blockers has items. Cross-checking the API response shape — or deriving count directly from the list via blockerData?.blockers.filter(b => b.status === 'OPEN').length — would make this more robust.


BlockerResolutionView.tsx — workspace path in SWR key

The SWR keys embed the raw workspace path string. Workspace paths can contain spaces and special characters. Using the raw path as a key doesn't cause a runtime error (SWR keys are just identifiers), but it's inconsistent with how the fetcher constructs the actual HTTP request via axios params. This is fine as-is, but worth noting in case path-based SWR key collisions become a problem.


BlockerCard.tsx — dead code path not tested

The card contains a read-only answer display for non-OPEN blockers:

{!isOpen && blocker.answer && (
  <div className="rounded-md border bg-muted/50 p-3"></div>
)}

Since BlockerResolutionView only passes OPEN blockers to BlockerCard, this branch is never exercised. Either add a test covering a non-OPEN blocker to confirm the read-only display works, or document that the component is only intended for OPEN blockers (and remove the dead branch).


No tests for BlockerResolutionView or formatRelativeTime

Both are covered indirectly, but:

  • formatRelativeTime is a pure utility — it's straightforward to unit test directly, and the invalid-date edge case would be caught there.
  • BlockerResolutionView loading/error states have no direct tests.

Summary

The core functionality is well implemented and follows the project patterns. The two items already raised by CodeRabbit (invalid date guard and null-answer test) are the most actionable fixes. The badgeVariant indirection and the dead code path in BlockerCard are low-priority but worth cleaning up for clarity.

@frankbria frankbria merged commit 3821731 into main Feb 19, 2026
10 checks passed
try {
await blockersApi.answer(workspacePath, blocker.id, answer.trim());
setSubmitted(true);
timerRef.current = setTimeout(() => onAnswered(), 1500);
Copy link
Contributor

Choose a reason for hiding this comment

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

🟢 Low

blockers/BlockerCard.tsx:42 Consider adding an isMounted ref to guard against state updates and timer scheduling after unmount. The cleanup effect only clears the timer that exists at unmount time, not timers created after the async call resolves post-unmount.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file web-ui/src/components/blockers/BlockerCard.tsx around line 42:

Consider adding an `isMounted` ref to guard against state updates and timer scheduling after unmount. The cleanup effect only clears the timer that exists at unmount time, not timers created after the async call resolves post-unmount.

Evidence trail:
Viewed `web-ui/src/components/blockers/BlockerCard.tsx` lines 22-70 and 45-60 at commit `492f44e` (cleanup effect and async `handleSubmit` with `setTimeout`).

}, [mutate, globalMutate, workspacePath]);

const openBlockers = useMemo(
() => data?.blockers.filter((b) => b.status === 'OPEN') ?? [],
Copy link
Contributor

Choose a reason for hiding this comment

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

🟠 High

blockers/BlockerResolutionView.tsx:32 data?.blockers.filter(...) crashes when data is undefined because optional chaining doesn't extend to .filter. Consider data?.blockers?.filter(...) instead.

Suggested change
() => data?.blockers.filter((b) => b.status === 'OPEN') ?? [],
() => data?.blockers?.filter((b) => b.status === 'OPEN') ?? [],

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file web-ui/src/components/blockers/BlockerResolutionView.tsx around line 32:

`data?.blockers.filter(...)` crashes when `data` is undefined because optional chaining doesn't extend to `.filter`. Consider `data?.blockers?.filter(...)` instead.

Evidence trail:
Viewed `web-ui/src/components/blockers/BlockerResolutionView.tsx` lines ~29-38 at commit `492f44e`.

@frankbria frankbria deleted the feature/issue-333-blocker-resolution-view branch February 19, 2026 20:01
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.

[Phase 3] Blocker Resolution View - Answer Agent Questions

1 participant