feat(8ij.2): add inline assign affordance to SocialCard
Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
parent
b996d889d5
commit
6d560b6c49
3 changed files with 57 additions and 3 deletions
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { BeadIssue } from '../../lib/types';
|
||||
import type { ProjectScopeOption } from '../../lib/project-scope';
|
||||
|
|
@ -48,6 +48,19 @@ export function UnifiedShell({
|
|||
hideClosed: true,
|
||||
});
|
||||
|
||||
const [actor, setActor] = useState<string>('');
|
||||
|
||||
// Read from localStorage after hydration to avoid SSR/client mismatch
|
||||
useEffect(() => {
|
||||
const stored = window.localStorage.getItem('bb.humanActor');
|
||||
if (stored) setActor(stored);
|
||||
}, []);
|
||||
|
||||
const handleActorChange = useCallback((name: string) => {
|
||||
setActor(name);
|
||||
window.localStorage.setItem('bb.humanActor', name);
|
||||
}, []);
|
||||
|
||||
const [customRightPanel, setCustomRightPanel] = useState<React.ReactNode | null>(null);
|
||||
|
||||
// Assign mode state for graph view
|
||||
|
|
@ -138,6 +151,7 @@ export function UnifiedShell({
|
|||
onSelect={handleCardSelect}
|
||||
projectScopeOptions={projectScopeOptions}
|
||||
blockedOnly={blockedOnly}
|
||||
projectRoot={projectRoot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -165,7 +179,7 @@ export function UnifiedShell({
|
|||
}
|
||||
|
||||
// Default: ContextualRightPanel
|
||||
return <ContextualRightPanel epicId={epicId} taskId={taskId} swarmId={swarmId} issues={issues} projectRoot={projectRoot} />;
|
||||
return <ContextualRightPanel epicId={epicId} taskId={taskId} swarmId={swarmId} issues={issues} projectRoot={projectRoot} actor={actor} />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -176,6 +190,8 @@ export function UnifiedShell({
|
|||
criticalAlerts={issues.filter(i => i.status === 'blocked').length}
|
||||
busyCount={issues.filter(i => i.status === 'in_progress').length}
|
||||
idleCount={0}
|
||||
actor={actor}
|
||||
onActorChange={handleActorChange}
|
||||
/>
|
||||
{!bdHealth.loading && !bdHealth.healthy ? (
|
||||
<div className="border-b border-amber-500/35 bg-amber-500/12 px-4 py-2 text-xs text-amber-100">
|
||||
|
|
@ -231,6 +247,7 @@ export function UnifiedShell({
|
|||
embedded={true}
|
||||
issue={selectedItem}
|
||||
projectRoot={projectRoot}
|
||||
actor={actor}
|
||||
onIssueUpdated={async () => {
|
||||
router.refresh();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import type { KeyboardEvent, MouseEventHandler } from 'react';
|
||||
import { Activity, Clock3, GitBranch, Link2, MessageCircle, Orbit } from 'lucide-react';
|
||||
import { Activity, Clock3, GitBranch, Link2, MessageCircle, Orbit, UserPlus } from 'lucide-react';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import type { SocialCard as SocialCardData, AgentStatus } from '../../lib/social-cards';
|
||||
import { AgentAvatar } from '../shared/agent-avatar';
|
||||
import { useArchetypePicker } from '../../hooks/use-archetype-picker';
|
||||
import type { AgentArchetype } from '../../lib/types-swarm';
|
||||
|
||||
interface SocialCardProps {
|
||||
data: SocialCardData;
|
||||
|
|
@ -22,6 +24,7 @@ interface SocialCardProps {
|
|||
unreadCount?: number;
|
||||
blockedByDetails?: Array<{ id: string; title: string; epic?: string }>;
|
||||
unblocksDetails?: Array<{ id: string; title: string; epic?: string }>;
|
||||
archetypes?: AgentArchetype[];
|
||||
}
|
||||
|
||||
function handleCardKeyDown(event: KeyboardEvent<HTMLDivElement>, onClick?: MouseEventHandler<HTMLDivElement>) {
|
||||
|
|
@ -116,8 +119,11 @@ export function SocialCard({
|
|||
unreadCount = 0,
|
||||
blockedByDetails = [],
|
||||
unblocksDetails = [],
|
||||
archetypes = [],
|
||||
}: SocialCardProps) {
|
||||
const status = statusVisual(data.status);
|
||||
const { selectedArchetype, setSelectedArchetype, isAssigning, assignSuccess, handleAssign } = useArchetypePicker();
|
||||
const showAssign = (data.status === 'blocked' || data.agents.length === 0) && archetypes.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -176,6 +182,32 @@ export function SocialCard({
|
|||
{data.agents.length === 0 ? <span className="text-xs text-[var(--text-tertiary)]">No crew</span> : null}
|
||||
</div>
|
||||
|
||||
{showAssign && (
|
||||
<div className="mt-2 flex gap-2 items-center" onClick={(e) => e.stopPropagation()}>
|
||||
<select
|
||||
value={selectedArchetype ?? ''}
|
||||
onChange={(e) => setSelectedArchetype(e.target.value || null)}
|
||||
className="flex-1 text-xs border border-[var(--border-subtle)] rounded-md px-2 py-1.5 bg-[var(--surface-input)] text-[var(--text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--accent-info)]"
|
||||
>
|
||||
<option value="" disabled>Select agent role...</option>
|
||||
{archetypes.map((a) => (
|
||||
<option key={a.id} value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await handleAssign(data.id);
|
||||
}}
|
||||
disabled={!selectedArchetype || isAssigning || assignSuccess}
|
||||
className={`px-3 py-1.5 text-xs font-semibold rounded-md transition-colors disabled:opacity-50 flex items-center gap-1 ${assignSuccess ? 'bg-[var(--accent-success)] text-white' : 'bg-[var(--accent-info)] text-white hover:bg-[var(--accent-info)]/90'}`}
|
||||
>
|
||||
<UserPlus className="w-3 h-3" />
|
||||
{isAssigning ? 'Assigning...' : assignSuccess ? 'Assigned!' : 'Assign'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-auto border-t border-[var(--border-subtle)] pt-1.5">
|
||||
<div className="mb-1.5 flex items-center justify-between text-xs text-[var(--text-tertiary)]">
|
||||
<span className="inline-flex items-center gap-1"><Clock3 className="h-3.5 w-3.5" aria-hidden="true" />{updatedLabel}</span>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { BeadIssue } from '../../lib/types';
|
|||
import type { ProjectScopeOption } from '../../lib/project-scope';
|
||||
import { buildSocialCards } from '../../lib/social-cards';
|
||||
import { SocialCard } from './social-card';
|
||||
import { useArchetypes } from '../../hooks/use-archetypes';
|
||||
|
||||
interface SocialPageProps {
|
||||
issues: BeadIssue[];
|
||||
|
|
@ -14,6 +15,7 @@ interface SocialPageProps {
|
|||
onSelect: (id: string) => void;
|
||||
projectScopeOptions?: ProjectScopeOption[];
|
||||
blockedOnly?: boolean;
|
||||
projectRoot: string;
|
||||
}
|
||||
|
||||
type SectionKey = 'ready' | 'in_progress' | 'blocked' | 'deferred' | 'done';
|
||||
|
|
@ -63,10 +65,12 @@ export function SocialPage({
|
|||
onSelect,
|
||||
projectScopeOptions = [],
|
||||
blockedOnly = false,
|
||||
projectRoot,
|
||||
}: SocialPageProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const cards = useMemo(() => buildSocialCards(issues), [issues]);
|
||||
const { archetypes } = useArchetypes(projectRoot);
|
||||
|
||||
const navigateWithParams = (updates: Record<string, string | null>) => {
|
||||
const next = new URLSearchParams(searchParams.toString());
|
||||
|
|
@ -233,6 +237,7 @@ export function SocialPage({
|
|||
unreadCount={unreadCount}
|
||||
blockedByDetails={toDependencyDetails(card.unblocks)}
|
||||
unblocksDetails={toDependencyDetails(card.blocks)}
|
||||
archetypes={archetypes}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue