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';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import type { BeadIssue } from '../../lib/types';
|
import type { BeadIssue } from '../../lib/types';
|
||||||
import type { ProjectScopeOption } from '../../lib/project-scope';
|
import type { ProjectScopeOption } from '../../lib/project-scope';
|
||||||
|
|
@ -48,6 +48,19 @@ export function UnifiedShell({
|
||||||
hideClosed: true,
|
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);
|
const [customRightPanel, setCustomRightPanel] = useState<React.ReactNode | null>(null);
|
||||||
|
|
||||||
// Assign mode state for graph view
|
// Assign mode state for graph view
|
||||||
|
|
@ -138,6 +151,7 @@ export function UnifiedShell({
|
||||||
onSelect={handleCardSelect}
|
onSelect={handleCardSelect}
|
||||||
projectScopeOptions={projectScopeOptions}
|
projectScopeOptions={projectScopeOptions}
|
||||||
blockedOnly={blockedOnly}
|
blockedOnly={blockedOnly}
|
||||||
|
projectRoot={projectRoot}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +179,7 @@ export function UnifiedShell({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: ContextualRightPanel
|
// 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 (
|
return (
|
||||||
|
|
@ -176,6 +190,8 @@ export function UnifiedShell({
|
||||||
criticalAlerts={issues.filter(i => i.status === 'blocked').length}
|
criticalAlerts={issues.filter(i => i.status === 'blocked').length}
|
||||||
busyCount={issues.filter(i => i.status === 'in_progress').length}
|
busyCount={issues.filter(i => i.status === 'in_progress').length}
|
||||||
idleCount={0}
|
idleCount={0}
|
||||||
|
actor={actor}
|
||||||
|
onActorChange={handleActorChange}
|
||||||
/>
|
/>
|
||||||
{!bdHealth.loading && !bdHealth.healthy ? (
|
{!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">
|
<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}
|
embedded={true}
|
||||||
issue={selectedItem}
|
issue={selectedItem}
|
||||||
projectRoot={projectRoot}
|
projectRoot={projectRoot}
|
||||||
|
actor={actor}
|
||||||
onIssueUpdated={async () => {
|
onIssueUpdated={async () => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import type { KeyboardEvent, MouseEventHandler } from 'react';
|
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 { Badge } from '@/components/ui/badge';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
import type { SocialCard as SocialCardData, AgentStatus } from '../../lib/social-cards';
|
import type { SocialCard as SocialCardData, AgentStatus } from '../../lib/social-cards';
|
||||||
import { AgentAvatar } from '../shared/agent-avatar';
|
import { AgentAvatar } from '../shared/agent-avatar';
|
||||||
|
import { useArchetypePicker } from '../../hooks/use-archetype-picker';
|
||||||
|
import type { AgentArchetype } from '../../lib/types-swarm';
|
||||||
|
|
||||||
interface SocialCardProps {
|
interface SocialCardProps {
|
||||||
data: SocialCardData;
|
data: SocialCardData;
|
||||||
|
|
@ -22,6 +24,7 @@ interface SocialCardProps {
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
blockedByDetails?: Array<{ id: string; title: string; epic?: string }>;
|
blockedByDetails?: Array<{ id: string; title: string; epic?: string }>;
|
||||||
unblocksDetails?: 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>) {
|
function handleCardKeyDown(event: KeyboardEvent<HTMLDivElement>, onClick?: MouseEventHandler<HTMLDivElement>) {
|
||||||
|
|
@ -116,8 +119,11 @@ export function SocialCard({
|
||||||
unreadCount = 0,
|
unreadCount = 0,
|
||||||
blockedByDetails = [],
|
blockedByDetails = [],
|
||||||
unblocksDetails = [],
|
unblocksDetails = [],
|
||||||
|
archetypes = [],
|
||||||
}: SocialCardProps) {
|
}: SocialCardProps) {
|
||||||
const status = statusVisual(data.status);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -176,6 +182,32 @@ export function SocialCard({
|
||||||
{data.agents.length === 0 ? <span className="text-xs text-[var(--text-tertiary)]">No crew</span> : null}
|
{data.agents.length === 0 ? <span className="text-xs text-[var(--text-tertiary)]">No crew</span> : null}
|
||||||
</div>
|
</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="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)]">
|
<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>
|
<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 type { ProjectScopeOption } from '../../lib/project-scope';
|
||||||
import { buildSocialCards } from '../../lib/social-cards';
|
import { buildSocialCards } from '../../lib/social-cards';
|
||||||
import { SocialCard } from './social-card';
|
import { SocialCard } from './social-card';
|
||||||
|
import { useArchetypes } from '../../hooks/use-archetypes';
|
||||||
|
|
||||||
interface SocialPageProps {
|
interface SocialPageProps {
|
||||||
issues: BeadIssue[];
|
issues: BeadIssue[];
|
||||||
|
|
@ -14,6 +15,7 @@ interface SocialPageProps {
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
projectScopeOptions?: ProjectScopeOption[];
|
projectScopeOptions?: ProjectScopeOption[];
|
||||||
blockedOnly?: boolean;
|
blockedOnly?: boolean;
|
||||||
|
projectRoot: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SectionKey = 'ready' | 'in_progress' | 'blocked' | 'deferred' | 'done';
|
type SectionKey = 'ready' | 'in_progress' | 'blocked' | 'deferred' | 'done';
|
||||||
|
|
@ -63,10 +65,12 @@ export function SocialPage({
|
||||||
onSelect,
|
onSelect,
|
||||||
projectScopeOptions = [],
|
projectScopeOptions = [],
|
||||||
blockedOnly = false,
|
blockedOnly = false,
|
||||||
|
projectRoot,
|
||||||
}: SocialPageProps) {
|
}: SocialPageProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const cards = useMemo(() => buildSocialCards(issues), [issues]);
|
const cards = useMemo(() => buildSocialCards(issues), [issues]);
|
||||||
|
const { archetypes } = useArchetypes(projectRoot);
|
||||||
|
|
||||||
const navigateWithParams = (updates: Record<string, string | null>) => {
|
const navigateWithParams = (updates: Record<string, string | null>) => {
|
||||||
const next = new URLSearchParams(searchParams.toString());
|
const next = new URLSearchParams(searchParams.toString());
|
||||||
|
|
@ -233,6 +237,7 @@ export function SocialPage({
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
blockedByDetails={toDependencyDetails(card.unblocks)}
|
blockedByDetails={toDependencyDetails(card.unblocks)}
|
||||||
unblocksDetails={toDependencyDetails(card.blocks)}
|
unblocksDetails={toDependencyDetails(card.blocks)}
|
||||||
|
archetypes={archetypes}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue