fix(layout): unified right sidebar with chat mode and collapsing activity rail
This commit is contained in:
parent
24c904554b
commit
c4622ea0b6
6 changed files with 112 additions and 59 deletions
BIN
assets/activity-with-corect-midddle.jpg
Normal file
BIN
assets/activity-with-corect-midddle.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
BIN
assets/chat-with-incorrect-middle.jpg
Normal file
BIN
assets/chat-with-incorrect-middle.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -6,8 +6,8 @@ import type { ActivityEvent } from '../../lib/activity';
|
|||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type AgentStatus = 'active' | 'stale' | 'stuck' | 'dead';
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ interface AgentRosterEntry {
|
|||
|
||||
interface ActivityPanelProps {
|
||||
issues: BeadIssue[];
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
const AGENT_LABEL = 'gt:agent';
|
||||
|
|
@ -99,17 +100,6 @@ function formatRelativeTime(timestamp: string): string {
|
|||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
// Get status badge variant
|
||||
function getStatusVariant(status: AgentStatus): 'default' | 'secondary' | 'outline' | 'destructive' {
|
||||
switch (status) {
|
||||
case 'active': return 'default';
|
||||
case 'stale': return 'secondary';
|
||||
case 'stuck': return 'outline';
|
||||
case 'dead': return 'destructive';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
// Get event kind icon/color
|
||||
function getEventKindInfo(kind: string): { label: string; color: string } {
|
||||
const events: Record<string, { label: string; color: string }> = {
|
||||
|
|
@ -131,7 +121,7 @@ function getInitials(name: string): string {
|
|||
return name.split(/[-_\s]/).map(p => p[0]).join('').toUpperCase().slice(0, 2);
|
||||
}
|
||||
|
||||
export function ActivityPanel({ issues }: ActivityPanelProps) {
|
||||
export function ActivityPanel({ issues, collapsed = false }: ActivityPanelProps) {
|
||||
const [activities, setActivities] = useState<ActivityEvent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
|
|
@ -182,6 +172,44 @@ export function ActivityPanel({ issues }: ActivityPanelProps) {
|
|||
const activeAgents = agentRoster.filter(a => a.status === 'active').length;
|
||||
const staleAgents = agentRoster.filter(a => a.status === 'stale').length;
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 py-4 h-full bg-black/20">
|
||||
{/* Collapsed Agent Icons */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{agentRoster.slice(0, 5).map(agent => (
|
||||
<div key={agent.beadId} className="relative group cursor-help" title={`${agent.name} (${agent.status})`}>
|
||||
<Avatar className="h-8 w-8 ring-1 ring-white/10 hover:ring-white/30 transition-all">
|
||||
<AvatarFallback className="text-[10px] bg-white/5">
|
||||
{getInitials(agent.name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className={cn(
|
||||
"absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-[#1e1e1e]",
|
||||
agent.status === 'active' ? 'bg-emerald-500' :
|
||||
agent.status === 'stale' ? 'bg-amber-500' : 'bg-rose-500'
|
||||
)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="w-4 h-[1px] bg-white/10" />
|
||||
|
||||
{/* Mini Activity Dots (Just visual pulse) */}
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Just show a few recent activity dots as a visual "heartbeat" */}
|
||||
{activities.slice(0, 5).map((act) => (
|
||||
<div key={act.id} className={cn(
|
||||
"w-1.5 h-1.5 rounded-full opacity-50",
|
||||
act.kind === 'created' ? 'bg-emerald-500' : 'bg-cyan-500'
|
||||
)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* AGENT ROSTER SECTION */}
|
||||
|
|
@ -294,4 +322,4 @@ export function ActivityPanel({ issues }: ActivityPanelProps) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,30 +7,49 @@ import { useUrlState } from '../../hooks/use-url-state';
|
|||
|
||||
export interface RightPanelProps {
|
||||
children?: ReactNode;
|
||||
rail?: ReactNode;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
export function RightPanel({ children, isOpen: externalIsOpen }: RightPanelProps) {
|
||||
export function RightPanel({ children, rail, isOpen: externalIsOpen }: RightPanelProps) {
|
||||
const { isMobile, isDesktop } = useResponsive();
|
||||
const { panel, togglePanel } = useUrlState();
|
||||
|
||||
const isOpen = externalIsOpen ?? (panel === 'open');
|
||||
|
||||
// Calculate width based on content (Standard 17rem vs Chat Mode ~26rem)
|
||||
// If rail is present, we are in "Chat Mode" (Wide Panel + Rail)
|
||||
// If no rail, we are in "Activity Mode" (Standard Panel)
|
||||
const panelWidth = isOpen ? (rail ? '26rem' : '17rem') : '0';
|
||||
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<div
|
||||
className="overflow-y-auto transition-all duration-300"
|
||||
className="overflow-hidden transition-all duration-300 flex"
|
||||
style={{
|
||||
width: isOpen ? '17rem' : '0',
|
||||
width: panelWidth,
|
||||
backgroundColor: 'var(--color-bg-card)',
|
||||
borderLeft: isOpen ? '1px solid rgba(255, 255, 255, 0.1)' : 'none',
|
||||
}}
|
||||
data-testid="right-panel-desktop"
|
||||
>
|
||||
{isOpen && (
|
||||
<div className="p-4" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
{children || <span>Right Panel</span>}
|
||||
</div>
|
||||
<>
|
||||
{/* Main Content (Chat or Activity) */}
|
||||
<div className="flex-1 min-w-0 h-full overflow-hidden flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-0">
|
||||
{/* Remove default padding to allow edge-to-edge chat */}
|
||||
{children || <span>Right Panel</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Side Rail (Mini Activity - Only if provided) */}
|
||||
{rail && (
|
||||
<div className="w-12 h-full flex-shrink-0 border-l border-white/10 bg-black/20">
|
||||
{rail}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -109,4 +128,4 @@ export function RightPanel({ children, isOpen: externalIsOpen }: RightPanelProps
|
|||
);
|
||||
}
|
||||
|
||||
export default RightPanel;
|
||||
export default RightPanel;
|
||||
|
|
@ -10,6 +10,7 @@ interface ThreadDrawerProps {
|
|||
title: string;
|
||||
id: string;
|
||||
items?: ThreadItem[];
|
||||
embedded?: boolean; // New prop for embedded mode
|
||||
}
|
||||
|
||||
// Sample data for demo
|
||||
|
|
@ -37,23 +38,24 @@ const SAMPLE_ITEMS: ThreadItem[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS }: ThreadDrawerProps) {
|
||||
export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS, embedded = false }: ThreadDrawerProps) {
|
||||
const [comment, setComment] = useState('');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full w-[24rem] overflow-hidden flex flex-col"
|
||||
className="h-full flex flex-col"
|
||||
style={{
|
||||
width: embedded ? '100%' : '24rem', // Full width when embedded
|
||||
backgroundColor: 'var(--color-bg-card)',
|
||||
borderLeft: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
boxShadow: '-4px 0 20px rgba(0, 0, 0, 0.3)',
|
||||
borderLeft: embedded ? 'none' : '1px solid rgba(255, 255, 255, 0.1)',
|
||||
boxShadow: embedded ? 'none' : '-4px 0 20px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="flex items-center justify-between p-4 border-b"
|
||||
className="flex items-center justify-between p-4 border-b flex-shrink-0"
|
||||
style={{ borderColor: 'rgba(255, 255, 255, 0.1)' }}
|
||||
>
|
||||
<div className="flex-1 min-w-0 mr-4">
|
||||
|
|
@ -78,13 +80,13 @@ export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS
|
|||
</div>
|
||||
|
||||
{/* Thread Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
|
||||
<ThreadView items={items} />
|
||||
</div>
|
||||
|
||||
{/* Compose */}
|
||||
<div
|
||||
className="p-4 border-t"
|
||||
className="p-4 border-t flex-shrink-0"
|
||||
style={{ borderColor: 'rgba(255, 255, 255, 0.1)' }}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -93,7 +95,7 @@ export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS
|
|||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder="Add a comment..."
|
||||
className="flex-1 px-3 py-2 rounded-md text-sm"
|
||||
className="flex-1 px-3 py-2 rounded-md text-sm outline-none focus:ring-1 focus:ring-teal-500/50 transition-all"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-bg-input)',
|
||||
color: 'var(--color-text-primary)',
|
||||
|
|
@ -106,7 +108,7 @@ export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS
|
|||
}}
|
||||
/>
|
||||
<button
|
||||
className="p-2 rounded-md"
|
||||
className="p-2 rounded-md hover:opacity-90 transition-opacity"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-accent-green)',
|
||||
color: '#fff',
|
||||
|
|
@ -119,4 +121,4 @@ export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,14 +52,32 @@ export function UnifiedShell({
|
|||
setDrawer('closed');
|
||||
};
|
||||
|
||||
// Thread drawer - shows when card selected
|
||||
const isDrawerOpen = drawer === 'open' && (!!taskId || !!swarmId);
|
||||
// Chat Mode Logic: If a card is selected (drawer='open'), we show Chat + Mini Activity Rail
|
||||
const isChatOpen = drawer === 'open' && (!!taskId || !!swarmId);
|
||||
const drawerTitle = selectedSocialCard?.title || selectedSwarmCard?.title || '';
|
||||
const drawerId = taskId || swarmId || '';
|
||||
|
||||
const renderRightPanel = () => {
|
||||
return <ActivityPanel issues={issues} />;
|
||||
};
|
||||
// Right Panel Content Logic
|
||||
// - Chat Mode: Main = Chat, Rail = Activity(Collapsed)
|
||||
// - Default: Main = Activity, Rail = None
|
||||
const rightPanelMain = isChatOpen ? (
|
||||
<ThreadDrawer
|
||||
isOpen={true} // Always "open" inside the panel
|
||||
onClose={handleCloseDrawer}
|
||||
title={drawerTitle}
|
||||
id={drawerId}
|
||||
embedded={true} // New prop to tell ThreadDrawer it's embedded, not an overlay
|
||||
/>
|
||||
) : (
|
||||
<ActivityPanel issues={issues} />
|
||||
);
|
||||
|
||||
const rightPanelRail = isChatOpen ? (
|
||||
<ActivityPanel issues={issues} collapsed={true} />
|
||||
) : undefined;
|
||||
|
||||
// Grid Layout: Expand Right Panel width when Chat is open
|
||||
const rightPanelWidth = isChatOpen ? '26rem' : '17rem';
|
||||
|
||||
const renderMiddleContent = () => {
|
||||
if (view === 'graph') {
|
||||
|
|
@ -103,42 +121,28 @@ export function UnifiedShell({
|
|||
{/* TOP BAR: 3rem fixed */}
|
||||
<TopBar />
|
||||
|
||||
{/* MAIN AREA: CSS Grid [13rem | 1fr | 17rem] */}
|
||||
{/* MAIN AREA: CSS Grid [13rem | 1fr | RightPanel] */}
|
||||
<div
|
||||
className="flex-1 grid overflow-hidden"
|
||||
style={{ gridTemplateColumns: '13rem 1fr 17rem' }}
|
||||
className="flex-1 grid overflow-hidden transition-all duration-300"
|
||||
style={{ gridTemplateColumns: `13rem 1fr ${rightPanelWidth}` }}
|
||||
data-testid="main-area"
|
||||
>
|
||||
{/* LEFT PANEL: 13rem channel tree */}
|
||||
<LeftPanel issues={issues} />
|
||||
|
||||
{/* MIDDLE CONTENT: flex-1 - contains card grid AND thread drawer */}
|
||||
<div className="relative overflow-hidden" data-testid="middle-content">
|
||||
{/* MIDDLE CONTENT: flex-1 */}
|
||||
<div className="relative overflow-hidden bg-black/10 shadow-inner" data-testid="middle-content">
|
||||
{renderMiddleContent()}
|
||||
|
||||
{/* THREAD DRAWER: Inside middle section, attached to right edge */}
|
||||
{isDrawerOpen && (
|
||||
<div className="absolute top-0 right-0 h-full z-50">
|
||||
<ThreadDrawer
|
||||
isOpen={isDrawerOpen}
|
||||
onClose={handleCloseDrawer}
|
||||
title={drawerTitle}
|
||||
id={drawerId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* RIGHT PANEL: 17rem - Always shows Activity (bb-ui2.29) */}
|
||||
<RightPanel isOpen={panel === 'open'}>
|
||||
{renderRightPanel()}
|
||||
{/* RIGHT PANEL: Dynamic Content + Optional Rail */}
|
||||
<RightPanel isOpen={panel === 'open'} rail={rightPanelRail}>
|
||||
{rightPanelMain}
|
||||
</RightPanel>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* MOBILE NAV: Bottom tab bar */}
|
||||
<MobileNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue