feat(8ij.1): extract useArchetypePicker hook from AssignmentPanel
Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
parent
65fff3cf57
commit
b996d889d5
2 changed files with 108 additions and 58 deletions
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
70
src/hooks/use-archetype-picker.ts
Normal file
70
src/hooks/use-archetype-picker.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue