From c79dfff0c6e2400824b7ae65bcf963d4814b61ad Mon Sep 17 00:00:00 2001 From: zenchantlive Date: Tue, 17 Feb 2026 00:10:28 -0800 Subject: [PATCH] fix(ui3): final polish - media player layout, functional sidebars, agent visibility --- .beads/bd.sock.startlock | 2 +- src/components/activity/activity-panel.tsx | 18 +- src/components/shared/left-panel.tsx | 219 +++++++++------------ src/components/shared/unified-shell.tsx | 32 ++- src/hooks/use-url-state.ts | 14 +- 5 files changed, 135 insertions(+), 150 deletions(-) diff --git a/.beads/bd.sock.startlock b/.beads/bd.sock.startlock index 7cbf78a..6bc3942 100644 --- a/.beads/bd.sock.startlock +++ b/.beads/bd.sock.startlock @@ -1 +1 @@ -95088 +97492 diff --git a/src/components/activity/activity-panel.tsx b/src/components/activity/activity-panel.tsx index f15a74b..309f8e7 100644 --- a/src/components/activity/activity-panel.tsx +++ b/src/components/activity/activity-panel.tsx @@ -53,11 +53,13 @@ function extractAgentName(issue: BeadIssue): string | null { // Build agent roster - filter out dead agents unless none are active function buildAgentRoster(issues: BeadIssue[]): AgentRosterEntry[] { const agentIssues = issues.filter(issue => - issue.labels.includes(AGENT_LABEL) || issue.labels.some(l => l.startsWith('gt:agent')) + issue.labels.includes(AGENT_LABEL) || + issue.labels.some(l => l.startsWith('gt:agent')) || + issue.labels.includes('agent') ); const roster = agentIssues.map(issue => { - const name = extractAgentName(issue) || issue.id; + const name = extractAgentName(issue) || issue.title.replace('Agent: ', '') || issue.id; const status = deriveAgentStatus(issue.updated_at); return { @@ -71,16 +73,8 @@ function buildAgentRoster(issues: BeadIssue[]): AgentRosterEntry[] { return statusOrder[a.status] - statusOrder[b.status]; }); - // Filter: if there are active agents, show only active + stale (max 5) - // If no active, show stale + stuck (max 3) - // Dead agents never show unless it's the only thing - const activeCount = roster.filter(a => a.status === 'active').length; - - if (activeCount > 0) { - return roster.filter(a => a.status !== 'dead').slice(0, 5); - } else { - return roster.filter(a => a.status !== 'dead').slice(0, 3); - } + // Show all non-dead agents, or at least the most recent ones + return roster.filter(a => a.status !== 'dead' || a.lastSeen).slice(0, 10); } // Format relative time diff --git a/src/components/shared/left-panel.tsx b/src/components/shared/left-panel.tsx index a73c70b..b7a5d81 100644 --- a/src/components/shared/left-panel.tsx +++ b/src/components/shared/left-panel.tsx @@ -61,18 +61,14 @@ export function LeftPanel({ }; const handleEpicClick = (epicId: string) => { - onEpicSelect?.(epicId); + onEpicSelect?.(epicId === selectedEpicId ? null : epicId); // Toggle selection toggleEpic(epicId); }; if (isTablet) { return (
{epicTree.map(({ epic }) => ( @@ -81,13 +77,12 @@ export function LeftPanel({ type="button" onClick={() => handleEpicClick(epic.id)} className={cn( - 'w-9 h-9 rounded flex items-center justify-center text-xs font-medium transition-colors', + 'w-10 h-10 rounded-xl flex items-center justify-center text-xs font-bold transition-all duration-200', selectedEpicId === epic.id - ? 'bg-[var(--color-accent-green)]/20 text-[var(--color-accent-green)]' - : 'hover:bg-white/5' + ? 'bg-teal-500/20 text-teal-400 ring-1 ring-teal-500/30 shadow-[0_0_10px_rgba(45,212,191,0.2)]' + : 'hover:bg-white/5 text-text-muted hover:text-white' )} - style={{ color: selectedEpicId === epic.id ? undefined : 'var(--color-text-muted-dark)' }} - title={epic.id} + title={epic.title || epic.id} > {epic.id.slice(0, 2).toUpperCase()} @@ -99,135 +94,107 @@ export function LeftPanel({ return (
-
- - Channels - -
+
+ {/* Header */} +
+ Channels + {epicTree.length} +
-
- {epicTree.map(({ epic, children }) => { - const isExpanded = expandedEpics.has(epic.id); - const isSelected = selectedEpicId === epic.id; - const childCount = children.length; + {/* Tree */} +
+ {epicTree.map(({ epic, children }) => { + const isExpanded = expandedEpics.has(epic.id); + const isSelected = selectedEpicId === epic.id; + const childCount = children.length; - return ( -
- + + {childCount > 0 && ( + + {childCount} + + )} + - {isExpanded && childCount > 0 && ( -
- {children.map(child => { - const childSelected = selectedEpicId === child.id; - return ( - - ); - })} -
- )} + {child.title} +
+ ))} + {childCount > 5 && ( +
+ +{childCount - 5} more +
+ )} +
+ )} +
+ ); + })} + + {epicTree.length === 0 && ( +
+
📡
+

NO_CHANNELS

- ); - })} + )} +
- {epicTree.length === 0 && ( -
- No epics found -
- )} -
- -
- - Scope - -
-
@@ -235,4 +202,4 @@ export function LeftPanel({ ); } -export default LeftPanel; +export default LeftPanel; \ No newline at end of file diff --git a/src/components/shared/unified-shell.tsx b/src/components/shared/unified-shell.tsx index 9cefe07..8b34d26 100644 --- a/src/components/shared/unified-shell.tsx +++ b/src/components/shared/unified-shell.tsx @@ -27,7 +27,7 @@ export interface UnifiedShellProps { export function UnifiedShell({ issues, }: UnifiedShellProps) { - const { view, taskId, setTaskId, swarmId, setSwarmId, graphTab, setGraphTab, panel, drawer, setDrawer } = useUrlState(); + const { view, taskId, setTaskId, swarmId, setSwarmId, graphTab, setGraphTab, panel, drawer, setDrawer, epicId, setEpicId } = useUrlState(); const socialCards = useMemo(() => buildSocialCards(issues), [issues]); const swarmCards = useMemo(() => buildSwarmCards(issues), [issues]); @@ -80,10 +80,19 @@ export function UnifiedShell({ const rightPanelWidth = isChatOpen ? '26rem' : '17rem'; const renderMiddleContent = () => { + // Filter issues by Epic if selected (Global Filter) + const filteredIssues = epicId + ? issues.filter(issue => { + if (issue.issue_type === 'epic') return issue.id === epicId; + const parent = issue.dependencies.find(d => d.type === 'parent'); + return parent?.target === epicId; + }) + : issues; + if (view === 'graph') { return ( @@ -106,7 +115,7 @@ export function UnifiedShell({ if (view === 'swarm') { return ( @@ -121,14 +130,19 @@ export function UnifiedShell({ {/* TOP BAR: 3rem fixed */} - {/* MAIN AREA: CSS Grid [13rem | 1fr | RightPanel] */} + {/* MAIN AREA: CSS Grid [18rem | 1fr | RightPanel] */} + {/* Increased Left Panel width to 18rem per redesign request */}
- {/* LEFT PANEL: 13rem channel tree */} - + {/* LEFT PANEL: 18rem channel tree */} + {/* MIDDLE CONTENT: flex-1 */}
@@ -145,4 +159,4 @@ export function UnifiedShell({
); -} \ No newline at end of file +} diff --git a/src/hooks/use-url-state.ts b/src/hooks/use-url-state.ts index 6e4736e..465a614 100644 --- a/src/hooks/use-url-state.ts +++ b/src/hooks/use-url-state.ts @@ -17,6 +17,8 @@ export interface UrlState { setSwarmId: (id: string | null) => void; agentId: string | null; setAgentId: (id: string | null) => void; + epicId: string | null; + setEpicId: (id: string | null) => void; panel: PanelState; togglePanel: () => void; drawer: DrawerState; @@ -41,6 +43,7 @@ export function parseUrlState(searchParams: URLSearchParams): { taskId: string | null; swarmId: string | null; agentId: string | null; + epicId: string | null; panel: PanelState; drawer: DrawerState; graphTab: GraphTabType; @@ -53,6 +56,7 @@ export function parseUrlState(searchParams: URLSearchParams): { const taskId = searchParams.get('task'); const swarmId = searchParams.get('swarm'); const agentId = searchParams.get('agent'); + const epicId = searchParams.get('epic'); const panelParam = searchParams.get('panel'); const panel: PanelState = panelParam && VALID_PANELS.includes(panelParam as PanelState) @@ -69,7 +73,7 @@ export function parseUrlState(searchParams: URLSearchParams): { ? (graphTabParam as GraphTabType) : DEFAULT_GRAPH_TAB; - return { view, taskId, swarmId, agentId, panel, drawer, graphTab }; + return { view, taskId, swarmId, agentId, epicId, panel, drawer, graphTab }; } export function buildUrlParams( @@ -117,6 +121,10 @@ export function useUrlState(): UrlState { updateUrl({ agent: id, panel: id ? 'open' : null }); }, [updateUrl]); + const setEpicId = useCallback((id: string | null) => { + updateUrl({ epic: id }); + }, [updateUrl]); + const togglePanel = useCallback(() => { const newPanel = state.panel === 'open' ? 'closed' : 'open'; updateUrl({ panel: newPanel }); @@ -131,7 +139,7 @@ export function useUrlState(): UrlState { }, [updateUrl]); const clearSelection = useCallback(() => { - updateUrl({ task: null, swarm: null, panel: 'closed', drawer: 'closed' }); + updateUrl({ task: null, swarm: null, epic: null, panel: 'closed', drawer: 'closed' }); }, [updateUrl]); return { @@ -143,6 +151,8 @@ export function useUrlState(): UrlState { setSwarmId, agentId: state.agentId, setAgentId, + epicId: state.epicId, + setEpicId, panel: state.panel, togglePanel, drawer: state.drawer,