fix(bb-ui2): separate ThreadDrawer from RightPanel

- Created ThreadDrawer component (24rem) that slides from right edge of middle
- RightPanel now reserved for Activity Feed + Agent roster (bb-ui2.29)
- Updated URL state: added drawer and agentId params
- Thread shows in drawer when card selected

Architecture now matches PRD:
- Right Panel (17rem): Activity Feed + Agent roster
- Thread Drawer (24rem): Opens from middle when card clicked

Beads: bb-ui2.31 thread drawer created, bb-ui2.13 closed
This commit is contained in:
zenchantlive 2026-02-16 10:16:33 -08:00
parent f6c5398f0c
commit a7787733b9
6 changed files with 208 additions and 70 deletions

View file

@ -0,0 +1,133 @@
'use client';
import { X, Send } from 'lucide-react';
import { ThreadView, type ThreadItem } from './thread-view';
import { useState } from 'react';
interface ThreadDrawerProps {
isOpen: boolean;
onClose: () => void;
title: string;
id: string;
items?: ThreadItem[];
}
// Sample data for demo
const SAMPLE_ITEMS: ThreadItem[] = [
{
id: '1',
type: 'status_change',
from: 'backlog',
to: 'in_progress',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
},
{
id: '2',
type: 'comment',
author: 'zenchantlive',
content: 'Started working on this task.',
timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000),
},
{
id: '3',
type: 'protocol_event',
event: 'HANDOFF',
content: 'Handed off to agent',
timestamp: new Date(Date.now() - 30 * 60 * 1000),
},
];
export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS }: ThreadDrawerProps) {
const [comment, setComment] = useState('');
if (!isOpen) return null;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40 bg-black/30"
onClick={onClose}
aria-hidden="true"
/>
{/* Drawer */}
<div
className="fixed top-0 right-0 h-full z-50 w-[24rem] overflow-hidden"
style={{
backgroundColor: 'var(--color-bg-card)',
borderLeft: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '-4px 0 20px rgba(0, 0, 0, 0.3)',
}}
>
{/* Header */}
<div
className="flex items-center justify-between p-4 border-b"
style={{ borderColor: 'rgba(255, 255, 255, 0.1)' }}
>
<div className="flex-1 min-w-0 mr-4">
<span className="text-teal-400 font-mono text-sm">
{id}
</span>
<h2
className="text-sm font-semibold truncate"
style={{ color: 'var(--color-text-primary)' }}
title={title}
>
{title}
</h2>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-md hover:bg-white/10 transition-colors flex-shrink-0"
aria-label="Close"
>
<X size={18} style={{ color: 'var(--color-text-muted)' }} />
</button>
</div>
{/* Thread Content */}
<div className="flex-1 overflow-y-auto p-4" style={{ height: 'calc(100% - 8rem)' }}>
<ThreadView items={items} />
</div>
{/* Compose */}
<div
className="p-4 border-t"
style={{ borderColor: 'rgba(255, 255, 255, 0.1)' }}
>
<div className="flex gap-2">
<input
type="text"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Add a comment..."
className="flex-1 px-3 py-2 rounded-md text-sm"
style={{
backgroundColor: 'var(--color-bg-input)',
color: 'var(--color-text-primary)',
border: '1px solid rgba(255, 255, 255, 0.1)',
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && comment.trim()) {
// TODO: Post comment
setComment('');
}
}}
/>
<button
className="p-2 rounded-md"
style={{
backgroundColor: 'var(--color-accent-green)',
color: '#fff',
}}
aria-label="Send comment"
>
<Send size={16} />
</button>
</div>
</div>
</div>
</>
);
}

View file

@ -7,12 +7,11 @@ import { TopBar } from './top-bar';
import { LeftPanel } from './left-panel';
import { RightPanel } from './right-panel';
import { MobileNav } from './mobile-nav';
import { ThreadDrawer } from './thread-drawer';
import { useUrlState } from '../../hooks/use-url-state';
import { GraphView } from '../graph/graph-view';
import { SocialPage } from '../social/social-page';
import { SocialDetail } from '../social/social-detail';
import { SwarmPage } from '../swarm/swarm-page';
import { SwarmDetail } from '../swarm/swarm-detail';
import { buildSocialCards } from '../../lib/social-cards';
import { buildSwarmCards } from '../../lib/swarm-cards';
@ -27,7 +26,7 @@ export interface UnifiedShellProps {
export function UnifiedShell({
issues,
}: UnifiedShellProps) {
const { view, taskId, setTaskId, swarmId, setSwarmId, graphTab, setGraphTab, panel } = useUrlState();
const { view, taskId, setTaskId, swarmId, setSwarmId, graphTab, setGraphTab, panel, drawer, setDrawer } = useUrlState();
const socialCards = useMemo(() => buildSocialCards(issues), [issues]);
const swarmCards = useMemo(() => buildSwarmCards(issues), [issues]);
@ -39,14 +38,33 @@ export function UnifiedShell({
setTaskId(id);
};
const handleCardSelect = (id: string) => {
if (view === 'social') {
setTaskId(id);
} else if (view === 'swarm') {
setSwarmId(id);
}
setDrawer('open');
};
const handleCloseDrawer = () => {
setDrawer('closed');
};
// Thread drawer - shows when card selected
const isDrawerOpen = drawer === 'open' && (!!taskId || !!swarmId);
const drawerTitle = selectedSocialCard?.title || selectedSwarmCard?.title || '';
const drawerId = taskId || swarmId || '';
const renderRightPanel = () => {
if (view === 'social' && taskId && selectedSocialCard) {
return <SocialDetail data={selectedSocialCard} />;
}
if (view === 'swarm' && swarmId && selectedSwarmCard) {
return <SwarmDetail card={selectedSwarmCard} />;
}
return null;
// TODO: Wire up ActivityPanel (bb-ui2.29) - for now show placeholder
return (
<div className="p-4 text-center text-text-muted text-sm">
Activity Panel coming
<br />
<span className="text-xs">(bb-ui2.29)</span>
</div>
);
};
const renderMiddleContent = () => {
@ -105,12 +123,20 @@ export function UnifiedShell({
{renderMiddleContent()}
</div>
{/* RIGHT PANEL: 17rem detail strip */}
{/* RIGHT PANEL: 17rem - Always shows Activity (bb-ui2.29) */}
<RightPanel isOpen={panel === 'open'}>
{renderRightPanel()}
</RightPanel>
</div>
{/* THREAD DRAWER: 24rem - Slides from right edge of middle when card selected */}
<ThreadDrawer
isOpen={isDrawerOpen}
onClose={handleCloseDrawer}
title={drawerTitle}
id={drawerId}
/>
{/* MOBILE NAV: Bottom tab bar */}
<MobileNav />
</div>

View file

@ -3,34 +3,8 @@
import type { SocialCard as SocialCardData, AgentStatus } from '../../lib/social-cards';
import { StatusBadge } from '../shared/status-badge';
import { AgentAvatar } from '../shared/agent-avatar';
import { ThreadView, type ThreadItem } from '../shared/thread-view';
import { Plus } from 'lucide-react';
// Sample data for demo - remove when real data connected
const SAMPLE_THREAD_ITEMS: ThreadItem[] = [
{
id: '1',
type: 'status_change',
from: 'backlog',
to: 'in_progress',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
},
{
id: '2',
type: 'comment',
author: 'zenchantlive',
content: 'Started working on this task. Will need input from the API team.',
timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000),
},
{
id: '3',
type: 'protocol_event',
event: 'HANDOFF',
content: 'Handed off to bb-agent-1 for implementation',
timestamp: new Date(Date.now() - 30 * 60 * 1000),
},
];
interface SocialDetailProps {
data: SocialCardData;
}
@ -67,7 +41,9 @@ export function SocialDetail({ data }: SocialDetailProps) {
<h3 className="text-text-muted text-xs font-semibold uppercase tracking-wider">
Thread
</h3>
<ThreadView items={SAMPLE_THREAD_ITEMS} />
<p className="text-text-muted text-sm italic">
Thread drawer coming (bb-ui2.31)
</p>
</div>
{data.blocks.length > 0 && (

View file

@ -3,7 +3,6 @@
import type { SwarmCard as SwarmCardType } from '../../lib/swarm-cards';
import { Badge } from '../../../components/ui/badge';
import { AgentAvatar } from '../shared/agent-avatar';
import { ThreadView, type ThreadItem } from '../shared/thread-view';
import { cn } from '../../lib/utils';
import { AlertTriangle, Clock, Users } from 'lucide-react';
@ -149,38 +148,15 @@ function LastActivitySection({ date }: { date: Date }) {
);
}
// Sample data for demo - remove when real data connected
const SAMPLE_SWARM_THREAD: ThreadItem[] = [
{
id: '1',
type: 'status_change',
from: 'planning',
to: 'in_progress',
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000),
},
{
id: '2',
type: 'comment',
author: 'bb-agent-1',
content: 'Starting work on the first batch of tasks.',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
},
{
id: '3',
type: 'protocol_event',
event: 'CLOSED',
content: 'Task bb-buff.1 completed',
timestamp: new Date(Date.now() - 30 * 60 * 1000),
},
];
function ThreadSection() {
return (
<div className="space-y-2">
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }}>
Thread
</span>
<ThreadView items={SAMPLE_SWARM_THREAD} />
<p className="text-text-muted text-sm italic">
Thread drawer coming (bb-ui2.31)
</p>
</div>
);
}