feat(8ij.1): extract useArchetypePicker hook from AssignmentPanel

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
zenchantlive 2026-03-01 17:15:30 -08:00
parent 65fff3cf57
commit b996d889d5
2 changed files with 108 additions and 58 deletions

View file

@ -9,6 +9,7 @@ import { TemplatePicker } from '../swarm/template-picker';
import { useArchetypes } from '../../hooks/use-archetypes'; import { useArchetypes } from '../../hooks/use-archetypes';
import { useTemplates } from '../../hooks/use-templates'; import { useTemplates } from '../../hooks/use-templates';
import { useGraphAnalysis } from '../../hooks/use-graph-analysis'; import { useGraphAnalysis } from '../../hooks/use-graph-analysis';
import { useArchetypePicker } from '../../hooks/use-archetype-picker';
import type { BeadIssue } from '../../lib/types'; import type { BeadIssue } from '../../lib/types';
import type { AgentArchetype, SwarmTemplate } from '../../lib/types-swarm'; import type { AgentArchetype, SwarmTemplate } from '../../lib/types-swarm';
import { getArchetypeDisplayChar } from '../../lib/utils'; import { getArchetypeDisplayChar } from '../../lib/utils';
@ -18,6 +19,7 @@ export interface AssignmentPanelProps {
projectRoot: string; projectRoot: string;
issues: BeadIssue[]; issues: BeadIssue[];
epicId?: string; epicId?: string;
onIssueUpdated?: () => void;
} }
function hasAgentLabel(labels: string[]): boolean { function hasAgentLabel(labels: string[]): boolean {
@ -38,22 +40,25 @@ function truncateTitle(title: string, maxLength: number = 30): string {
} }
function getTemplateId(issue: BeadIssue): string | null { function getTemplateId(issue: BeadIssue): string | null {
const templateLabel = issue.labels?.find(l => l.startsWith('template:'));
if (templateLabel) {
return templateLabel.replace('template:', '');
}
if (issue.metadata?.templateId && typeof issue.metadata.templateId === 'string') { if (issue.metadata?.templateId && typeof issue.metadata.templateId === 'string') {
return issue.metadata.templateId; return issue.metadata.templateId;
} }
return issue.templateId; return issue.templateId;
} }
export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }: AssignmentPanelProps) { export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId, onIssueUpdated }: AssignmentPanelProps) {
const [inspectingArchetypeId, setInspectingArchetypeId] = useState<string | null>(null); const [inspectingArchetypeId, setInspectingArchetypeId] = useState<string | null>(null);
const [inspectingTemplateId, setInspectingTemplateId] = useState<string | null>(null); const [inspectingTemplateId, setInspectingTemplateId] = useState<string | null>(null);
const [showArchetypeList, setShowArchetypeList] = useState(false); const [showArchetypeList, setShowArchetypeList] = useState(false);
const [showTemplateList, setShowTemplateList] = useState(false); const [showTemplateList, setShowTemplateList] = useState(false);
const [selectedArchetypeForPrep, setSelectedArchetypeForPrep] = useState<string>('');
const [isPrepping, setIsPrepping] = useState(false);
const [prepSuccess, setPrepSuccess] = useState(false);
const [quickAssignDropdown, setQuickAssignDropdown] = useState<string | null>(null); const [quickAssignDropdown, setQuickAssignDropdown] = useState<string | null>(null);
const { selectedArchetype, setSelectedArchetype, isAssigning, assignSuccess, handleAssign } = useArchetypePicker();
const [needsAgentCollapsed, setNeedsAgentCollapsed] = useState(false); const [needsAgentCollapsed, setNeedsAgentCollapsed] = useState(false);
const [preAssignedCollapsed, setPreAssignedCollapsed] = useState(false); const [preAssignedCollapsed, setPreAssignedCollapsed] = useState(false);
const [squadRosterCollapsed, setSquadRosterCollapsed] = useState(false); const [squadRosterCollapsed, setSquadRosterCollapsed] = useState(false);
@ -127,21 +132,28 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }:
const handleApplyTemplateToEpic = async (templateId: string, targetEpicId: string) => { const handleApplyTemplateToEpic = async (templateId: string, targetEpicId: string) => {
try { try {
const epic = issues.find(issue => issue.id === targetEpicId);
const currentLabels = epic?.labels || [];
const newLabels = [...currentLabels.filter(l => !l.startsWith('template:')), `template:${templateId}`];
const res = await fetch('/api/beads/update', { const res = await fetch('/api/beads/update', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
projectRoot, projectRoot,
id: targetEpicId, id: targetEpicId,
metadata: { templateId } labels: newLabels
}) })
}); });
if (!res.ok) { if (!res.ok) {
throw new Error('Failed to apply template'); const errorData = await res.json();
console.error('Template API error:', errorData);
throw new Error(errorData?.error?.message || 'Failed to apply template');
} }
console.log('Template applied successfully:', { templateId, epicId: targetEpicId }); console.log('Template applied successfully:', { templateId, epicId: targetEpicId });
onIssueUpdated?.();
} catch (error) { } catch (error) {
console.error('Failed to apply template:', error); console.error('Failed to apply template:', error);
} }
@ -149,74 +161,42 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }:
const handleRemoveTemplateFromEpic = async (targetEpicId: string) => { const handleRemoveTemplateFromEpic = async (targetEpicId: string) => {
try { try {
const epic = issues.find(issue => issue.id === targetEpicId);
const currentLabels = epic?.labels || [];
const newLabels = currentLabels.filter(l => !l.startsWith('template:'));
const res = await fetch('/api/beads/update', { const res = await fetch('/api/beads/update', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
projectRoot, projectRoot,
id: targetEpicId, id: targetEpicId,
metadata: { templateId: null } labels: newLabels
}) })
}); });
if (!res.ok) { if (!res.ok) {
throw new Error('Failed to remove template'); const errorData = await res.json();
console.error('Template API error:', errorData);
throw new Error(errorData?.error?.message || 'Failed to remove template');
} }
console.log('Template removed successfully'); console.log('Template removed successfully');
onIssueUpdated?.();
} catch (error) { } catch (error) {
console.error('Failed to remove template:', error); console.error('Failed to remove template:', error);
} }
}; };
const handlePrepTask = async () => { const handlePrepTask = async () => {
if (!selectedIssue || !selectedArchetypeForPrep) return; if (!selectedIssue) return;
await handleAssign(selectedIssue.id);
setIsPrepping(true);
setPrepSuccess(false);
try {
const res = await fetch('/api/swarm/prep', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
beadId: selectedIssue.id,
archetypeId: selectedArchetypeForPrep
})
});
if (!res.ok) {
throw new Error('Failed to prep task');
}
setPrepSuccess(true);
setTimeout(() => setPrepSuccess(false), 2000);
} catch (error) {
console.error('Failed to prep task:', error);
} finally {
setIsPrepping(false);
}
}; };
const handleQuickAssign = async (issueId: string, archetypeId: string) => { const handleQuickAssign = async (issueId: string, archetypeId: string) => {
try { setSelectedArchetype(archetypeId);
const res = await fetch('/api/swarm/prep', { await handleAssign(issueId);
method: 'POST', setQuickAssignDropdown(null);
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
beadId: issueId,
archetypeId: archetypeId
})
});
if (!res.ok) {
throw new Error('Failed to assign agent');
}
setQuickAssignDropdown(null);
} catch (error) {
console.error('Failed to assign agent:', error);
}
}; };
const getArchetypeForAgentLabel = (label: string): AgentArchetype | undefined => { const getArchetypeForAgentLabel = (label: string): AgentArchetype | undefined => {
@ -315,7 +295,7 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }:
isOpen={showArchetypeList} isOpen={showArchetypeList}
onClose={() => setShowArchetypeList(false)} onClose={() => setShowArchetypeList(false)}
onSelect={(archetype) => { onSelect={(archetype) => {
setSelectedArchetypeForPrep(archetype.id); setSelectedArchetype(archetype.id);
setShowArchetypeList(false); setShowArchetypeList(false);
}} }}
onEdit={(archetypeId) => { onEdit={(archetypeId) => {
@ -458,8 +438,8 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }:
<div> <div>
<label className="text-xs font-medium text-[var(--text-tertiary)] mb-1.5 block">Assign Agent Archetype</label> <label className="text-xs font-medium text-[var(--text-tertiary)] mb-1.5 block">Assign Agent Archetype</label>
<select <select
value={selectedArchetypeForPrep} value={selectedArchetype ?? ''}
onChange={(e) => setSelectedArchetypeForPrep(e.target.value)} onChange={(e) => setSelectedArchetype(e.target.value || null)}
className="w-full bg-[var(--surface-input)] border border-[var(--border-subtle)] rounded-md px-3 py-2 text-sm text-[var(--text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--accent-info)]" className="w-full bg-[var(--surface-input)] border border-[var(--border-subtle)] rounded-md px-3 py-2 text-sm text-[var(--text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--accent-info)]"
> >
<option value="" disabled>Select archetype...</option> <option value="" disabled>Select archetype...</option>
@ -470,10 +450,10 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }:
</div> </div>
<button <button
onClick={handlePrepTask} onClick={handlePrepTask}
disabled={!selectedArchetypeForPrep || isPrepping || prepSuccess} disabled={!selectedArchetype || isAssigning || assignSuccess}
className={`w-full py-2 text-[var(--text-inverse)] text-sm font-bold rounded-md disabled:opacity-50 transition-colors flex items-center justify-center ${prepSuccess ? 'bg-[var(--accent-success)]' : 'bg-[var(--accent-info)] hover:bg-[var(--accent-info)]/90'}`} className={`w-full py-2 text-[var(--text-inverse)] text-sm font-bold rounded-md disabled:opacity-50 transition-colors flex items-center justify-center ${assignSuccess ? 'bg-[var(--accent-success)]' : 'bg-[var(--accent-info)] hover:bg-[var(--accent-info)]/90'}`}
> >
{isPrepping ? <Loader2 className="w-4 h-4 animate-spin" /> : prepSuccess ? 'Prep Successful!' : 'Prep Task for Swarm'} {isAssigning ? <Loader2 className="w-4 h-4 animate-spin" /> : assignSuccess ? 'Prep Successful!' : 'Prep Task for Swarm'}
</button> </button>
</div> </div>
) : ( ) : (

View file

@ -0,0 +1,70 @@
import { useState, useCallback } from 'react';
export interface UseArchetypePickerReturn {
selectedArchetype: string | null;
setSelectedArchetype: (id: string | null) => void;
isAssigning: boolean;
assignError: string | null;
assignSuccess: boolean;
handleAssign: (issueId: string) => Promise<void>;
resetAssignState: () => void;
}
export function useArchetypePicker(): UseArchetypePickerReturn {
const [selectedArchetype, setSelectedArchetype] = useState<string | null>(null);
const [isAssigning, setIsAssigning] = useState(false);
const [assignError, setAssignError] = useState<string | null>(null);
const [assignSuccess, setAssignSuccess] = useState(false);
const handleAssign = useCallback(async (issueId: string) => {
if (!selectedArchetype) {
setAssignError('No archetype selected');
return;
}
setIsAssigning(true);
setAssignError(null);
setAssignSuccess(false);
try {
const res = await fetch('/api/swarm/prep', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
beadId: issueId,
archetypeId: selectedArchetype
})
});
if (!res.ok) {
const errorData = await res.json().catch(() => null);
throw new Error(errorData?.error || 'Failed to assign agent');
}
setAssignSuccess(true);
// Auto-reset success after 2 seconds
setTimeout(() => setAssignSuccess(false), 2000);
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to assign agent';
setAssignError(message);
console.error('Failed to assign agent:', error);
} finally {
setIsAssigning(false);
}
}, [selectedArchetype]);
const resetAssignState = useCallback(() => {
setAssignError(null);
setAssignSuccess(false);
}, []);
return {
selectedArchetype,
setSelectedArchetype,
isAssigning,
assignError,
assignSuccess,
handleAssign,
resetAssignState
};
}