fix: remove buildProjectContext usage causing build error
This commit is contained in:
parent
842f931f71
commit
c8c91736b8
9 changed files with 13403 additions and 8 deletions
430
docs/plans/2026-03-01-blocked-triage-modal.md
Normal file
430
docs/plans/2026-03-01-blocked-triage-modal.md
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
# Blocked Triage Modal Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build a BlockedTriageModal that shows all blocked tasks with blocker chain context and inline archetype assignment, replacing the TopBar "Blocked Items" toggle.
|
||||
|
||||
**Architecture:** The modal will use the same blocked computation logic as Kanban (`deriveBlockedIds` in kanban.ts) to accurately identify blocked tasks - both explicitly blocked (status='blocked') and derivation-blocked (has open blockers in dependency chain). Each row shows the blocker chain using `buildBlockedByTree`.
|
||||
|
||||
**Tech Stack:** React, shadcn/ui Dialog, useArchetypePicker hook, Next.js
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Verify deriveBlockedIds export in kanban.ts
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/kanban.ts:90-107`
|
||||
|
||||
**Step 1: Check if deriveBlockedIds is exported**
|
||||
|
||||
Run: `grep -n "export function deriveBlockedIds" src/lib/kanban.ts`
|
||||
Expected: Should find export on line 90
|
||||
|
||||
If NOT exported, edit line 90:
|
||||
```typescript
|
||||
// Before
|
||||
function deriveBlockedIds(issues: BeadIssue[]): Set<string> {
|
||||
|
||||
// After
|
||||
export function deriveBlockedIds(issues: BeadIssue[]): Set<string> {
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/kanban.ts
|
||||
git commit -m "feat: export deriveBlockedIds for reuse in BlockedTriageModal"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create BlockedTriageModal component
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/shared/blocked-triage-modal.tsx`
|
||||
- Test: `tests/components/blocked-triage-modal.test.tsx`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Create `tests/components/blocked-triage-modal.test.tsx`:
|
||||
|
||||
```tsx
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BlockedTriageModal } from '@/components/shared/blocked-triage-modal';
|
||||
import { BeadIssue } from '@/lib/types';
|
||||
|
||||
const mockIssues: BeadIssue[] = [
|
||||
{
|
||||
id: 'task-1',
|
||||
title: 'Task One',
|
||||
status: 'blocked',
|
||||
issue_type: 'task',
|
||||
priority: 0,
|
||||
dependencies: [],
|
||||
labels: [],
|
||||
created_at: '2026-01-01',
|
||||
created_by: 'test',
|
||||
updated_at: '2026-01-01'
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
title: 'Task Two',
|
||||
status: 'open',
|
||||
issue_type: 'task',
|
||||
priority: 0,
|
||||
dependencies: [{ target: 'task-1', type: 'blocks' }],
|
||||
labels: [],
|
||||
created_at: '2026-01-01',
|
||||
created_by: 'test',
|
||||
updated_at: '2026-01-01'
|
||||
}
|
||||
];
|
||||
|
||||
describe('BlockedTriageModal', () => {
|
||||
it('renders blocked tasks from both explicit status and derived blockers', () => {
|
||||
const onClose = jest.fn();
|
||||
const onAssign = jest.fn();
|
||||
|
||||
render(
|
||||
<BlockedTriageModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
issues={mockIssues}
|
||||
projectRoot="test"
|
||||
/>
|
||||
);
|
||||
|
||||
// task-1 is explicitly blocked
|
||||
expect(screen.getByText('Task One')).toBeInTheDocument();
|
||||
// task-2 is blocked by task-1 (derived)
|
||||
expect(screen.getByText('Task Two')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows blocker chain for each blocked task', () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
render(
|
||||
<BlockedTriageModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
issues={mockIssues}
|
||||
projectRoot="test"
|
||||
/>
|
||||
);
|
||||
|
||||
// Should show blocker chain
|
||||
expect(screen.getByText(/blocked by/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `npm run test -- tests/components/blocked-triage-modal.test.tsx`
|
||||
Expected: FAIL - file does not exist
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Create `src/components/shared/blocked-triage-modal.tsx`:
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { deriveBlockedIds, buildBlockedByTree } from '@/lib/kanban';
|
||||
import { useArchetypePicker } from '@/hooks/use-archetype-picker';
|
||||
import type { BeadIssue } from '@/lib/types';
|
||||
|
||||
interface BlockedTriageModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
issues: BeadIssue[];
|
||||
projectRoot: string;
|
||||
}
|
||||
|
||||
interface BlockedTaskRowProps {
|
||||
issue: BeadIssue;
|
||||
blockerChain: { id: string; title: string; status: string }[];
|
||||
onAssign: (issueId: string) => void;
|
||||
}
|
||||
|
||||
function BlockedTaskRow({ issue, blockerChain, onAssign }: BlockedTaskRowProps) {
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const {
|
||||
selectedArchetype,
|
||||
setSelectedArchetype,
|
||||
isAssigning,
|
||||
assignError,
|
||||
assignSuccess,
|
||||
handleAssign,
|
||||
resetAssignState
|
||||
} = useArchetypePicker();
|
||||
|
||||
const handleAssignClick = async () => {
|
||||
await handleAssign(issue.id);
|
||||
if (!assignError) {
|
||||
setShowPicker(false);
|
||||
resetAssignState();
|
||||
onAssign(issue.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-card)] p-3 mb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-[var(--text-tertiary)]">{issue.id}</span>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">{issue.title}</span>
|
||||
</div>
|
||||
|
||||
{blockerChain.length > 0 && (
|
||||
<div className="mt-1 text-xs text-[var(--text-tertiary)]">
|
||||
<span className="text-[var(--accent-warning)]">Blocked by: </span>
|
||||
{blockerChain.map((blocker, idx) => (
|
||||
<span key={blocker.id}>
|
||||
{idx > 0 && ', '}
|
||||
<span className={blocker.status === 'closed' ? 'line-through' : ''}>
|
||||
{blocker.id}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPicker(!showPicker)}
|
||||
className="ml-2 rounded-md bg-[var(--accent-info)] px-2 py-1 text-xs font-medium text-[var(--text-inverse)] hover:brightness-110"
|
||||
>
|
||||
Assign
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showPicker && (
|
||||
<div className="mt-2 pt-2 border-t border-[var(--border-subtle)]">
|
||||
<p className="text-xs text-[var(--text-tertiary)] mb-2">Select an archetype:</p>
|
||||
{/* Archetype picker UI - simplified for now */}
|
||||
<div className="flex gap-2">
|
||||
{['agent:coder', 'agent:reviewer', 'agent:writer'].map((archetype) => (
|
||||
<button
|
||||
key={archetype}
|
||||
type="button"
|
||||
onClick={() => setSelectedArchetype(archetype)}
|
||||
className={`rounded px-2 py-1 text-xs ${
|
||||
selectedArchetype === archetype
|
||||
? 'bg-[var(--accent-success)] text-[var(--text-inverse)]'
|
||||
: 'bg-[var(--surface-elevated)] text-[var(--text-secondary)] border border-[var(--border-default)]'
|
||||
}`}
|
||||
>
|
||||
{archetype.replace('agent:', '')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{assignError && (
|
||||
<p className="mt-1 text-xs text-[var(--accent-danger)]">{assignError}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAssignClick}
|
||||
disabled={!selectedArchetype || isAssigning}
|
||||
className="mt-2 rounded bg-[var(--accent-success)] px-3 py-1 text-xs font-medium text-[var(--text-inverse)] disabled:opacity-50"
|
||||
>
|
||||
{isAssigning ? 'Assigning...' : 'Confirm Assignment'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BlockedTriageModal({ isOpen, onClose, issues, projectRoot }: BlockedTriageModalProps) {
|
||||
const blockedIds = useMemo(() => deriveBlockedIds(issues), [issues]);
|
||||
|
||||
const blockedTasks = useMemo(() => {
|
||||
return issues.filter((issue) =>
|
||||
issue.status === 'blocked' || blockedIds.has(issue.id)
|
||||
);
|
||||
}, [issues, blockedIds]);
|
||||
|
||||
const handleAssign = (issueId: string) => {
|
||||
// Trigger refresh - parent will handle SSE update
|
||||
console.log('Assigned agent to:', issueId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-lg max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<span className="text-[var(--accent-warning)]">⚠</span>
|
||||
Blocked Triage
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-2">
|
||||
{blockedTasks.length === 0 ? (
|
||||
<p className="text-center text-[var(--text-tertiary)] py-8">
|
||||
No blocked tasks found.
|
||||
</p>
|
||||
) : (
|
||||
blockedTasks.map((issue) => {
|
||||
const blockerTree = buildBlockedByTree(issues, issue.id, { maxNodes: 5 });
|
||||
const blockerChain = blockerTree.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
status: node.status
|
||||
}));
|
||||
|
||||
return (
|
||||
<BlockedTaskRow
|
||||
key={issue.id}
|
||||
issue={issue}
|
||||
blockerChain={blockerChain}
|
||||
onAssign={handleAssign}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `npm run test -- tests/components/blocked-triage-modal.test.tsx`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Run typecheck**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
Expected: PASS with 0 errors
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/shared/blocked-triage-modal.tsx tests/components/blocked-triage-modal.test.tsx
|
||||
git commit -m "feat: add BlockedTriageModal with inline archetype picker"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Wire BlockedTriageModal into UnifiedShell and TopBar
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/shared/unified-shell.tsx`
|
||||
- Modify: `src/components/shared/top-bar.tsx`
|
||||
|
||||
**Step 1: Add modal state to UnifiedShell**
|
||||
|
||||
Read `src/components/shared/unified-shell.tsx` to find where state is defined, then add:
|
||||
|
||||
```tsx
|
||||
const [blockedTriageOpen, setBlockedTriageOpen] = useState(false);
|
||||
```
|
||||
|
||||
Add handler:
|
||||
```tsx
|
||||
const handleOpenBlockedTriage = useCallback(() => {
|
||||
setBlockedTriageOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseBlockedTriage = useCallback(() => {
|
||||
setBlockedTriageOpen(false);
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Step 2: Pass handler to TopBar**
|
||||
|
||||
Find the `<TopBar` component and add:
|
||||
```tsx
|
||||
onOpenBlockedTriage={handleOpenBlockedTriage}
|
||||
```
|
||||
|
||||
**Step 3: Update TopBar to accept and use the handler**
|
||||
|
||||
In `top-bar.tsx`, add to TopBarProps:
|
||||
```tsx
|
||||
onOpenBlockedTriage?: () => void;
|
||||
```
|
||||
|
||||
In the TopBar component, change the blocked button:
|
||||
```tsx
|
||||
// Before
|
||||
onClick={toggleBlockedOnly}
|
||||
|
||||
// After
|
||||
onClick={onOpenBlockedTriage}
|
||||
```
|
||||
|
||||
**Step 4: Import and render BlockedTriageModal in UnifiedShell**
|
||||
|
||||
Add import:
|
||||
```tsx
|
||||
import { BlockedTriageModal } from './blocked-triage-modal';
|
||||
```
|
||||
|
||||
Add render (at end of return, inside main container):
|
||||
```tsx
|
||||
<BlockedTriageModal
|
||||
isOpen={blockedTriageOpen}
|
||||
onClose={handleCloseBlockedTriage}
|
||||
issues={issues}
|
||||
projectRoot={projectRoot}
|
||||
/>
|
||||
```
|
||||
|
||||
**Step 5: Run typecheck**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
Expected: PASS with 0 errors
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/shared/unified-shell.tsx src/components/shared/top-bar.tsx
|
||||
git commit -m "feat: wire BlockedTriageModal to TopBar blocked button"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Verify full integration
|
||||
|
||||
**Step 1: Run all tests**
|
||||
|
||||
Run: `npm run test`
|
||||
Expected: All tests pass
|
||||
|
||||
**Step 2: Run lint**
|
||||
|
||||
Run: `npm run lint`
|
||||
Expected: 0 errors
|
||||
|
||||
**Step 3: Run typecheck**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
Expected: 0 errors
|
||||
|
||||
**Step 4: Update bead notes**
|
||||
|
||||
```bash
|
||||
bd update beadboard-d2x.1 --notes "BlockedTriageModal implemented with deriveBlockedIds for accurate blocked computation. Uses buildBlockedByTree for blocker chain context. Inline archetype picker per row. Typecheck, lint, test all pass."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Summary
|
||||
|
||||
| Task | Description |
|
||||
|------|--------------|
|
||||
| 1 | Export `deriveBlockedIds` from kanban.ts |
|
||||
| 2 | Create BlockedTriageModal component with tests |
|
||||
| 3 | Wire modal to UnifiedShell and TopBar |
|
||||
| 4 | Verify full integration |
|
||||
Loading…
Add table
Add a link
Reference in a new issue