feat(8ij.2): add inline assign affordance to SocialCard

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
zenchantlive 2026-03-01 17:18:13 -08:00
parent b996d889d5
commit 6d560b6c49
3 changed files with 57 additions and 3 deletions

View file

@ -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();
}}

View file

@ -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>

View file

@ -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}
/>
);
})}