feat(phase0+1): wire URL context to ContextualRightPanel
Phase 0: - UnifiedShell: pass blockedOnly to SocialPage; wire TopBar with live counts - thread-drawer: show real issue.status instead of hardcoded "In Progress" - social-page: fix onJumpToActivity to open right panel (not dead ?view=activity) Phase 1: - contextual-right-panel: add taskId branch (ThreadDrawer embedded) and swarmId branch (MissionInspector via SwarmIdBranch inner component); ActivityPanel remains the no-selection fallback All 207 tests pass; no new typecheck errors. Closes beadboard-r1i (Phase 1: Contextual Right Panel) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fccb2dede7
commit
7d37d02af1
6 changed files with 731 additions and 237 deletions
|
|
@ -4,14 +4,19 @@ import React from 'react';
|
|||
import type { BeadIssue } from '../../lib/types';
|
||||
import { ActivityPanel } from './activity-panel';
|
||||
import { SwarmCommandFeed } from './swarm-command-feed';
|
||||
import { ThreadDrawer } from '../shared/thread-drawer';
|
||||
import { MissionInspector } from '../mission/mission-inspector';
|
||||
import { useSwarmList } from '../../hooks/use-swarm-list';
|
||||
|
||||
export interface ContextualRightPanelProps {
|
||||
epicId?: string | null;
|
||||
taskId?: string | null;
|
||||
swarmId?: string | null;
|
||||
issues: BeadIssue[];
|
||||
projectRoot: string;
|
||||
}
|
||||
|
||||
export function ContextualRightPanel({ epicId, issues, projectRoot }: ContextualRightPanelProps) {
|
||||
export function ContextualRightPanel({ epicId, taskId, swarmId, issues, projectRoot }: ContextualRightPanelProps) {
|
||||
if (epicId) {
|
||||
return (
|
||||
<SwarmCommandFeed
|
||||
|
|
@ -22,6 +27,31 @@ export function ContextualRightPanel({ epicId, issues, projectRoot }: Contextual
|
|||
);
|
||||
}
|
||||
|
||||
if (taskId) {
|
||||
const selectedIssue = issues.find(i => i.id === taskId) ?? null;
|
||||
return (
|
||||
<ThreadDrawer
|
||||
isOpen={true}
|
||||
embedded={true}
|
||||
onClose={() => {}}
|
||||
title={selectedIssue?.title ?? taskId}
|
||||
id={taskId}
|
||||
issue={selectedIssue}
|
||||
projectRoot={projectRoot}
|
||||
onIssueUpdated={async () => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (swarmId) {
|
||||
return (
|
||||
<SwarmIdBranch
|
||||
swarmId={swarmId}
|
||||
projectRoot={projectRoot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to Global feed
|
||||
return (
|
||||
<ActivityPanel
|
||||
|
|
@ -30,3 +60,24 @@ export function ContextualRightPanel({ epicId, issues, projectRoot }: Contextual
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Inner component so hooks can be called conditionally via component boundary
|
||||
function SwarmIdBranch({ swarmId, projectRoot }: { swarmId: string; projectRoot: string }) {
|
||||
const { swarms } = useSwarmList(projectRoot);
|
||||
const swarm = swarms.find(s => s.swarmId === swarmId);
|
||||
// Fall back to swarmId as title while swarm list loads
|
||||
const missionTitle = swarm?.title ?? swarmId;
|
||||
// TODO (follow-up): populate assignedAgents from swarm.agents once agent-registry is wired
|
||||
const assignedAgents = swarm?.agents ?? [];
|
||||
|
||||
return (
|
||||
<MissionInspector
|
||||
missionId={swarmId}
|
||||
missionTitle={missionTitle}
|
||||
projectRoot={projectRoot}
|
||||
assignedAgents={assignedAgents}
|
||||
onClose={() => {}}
|
||||
onAssign={async () => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,23 +198,24 @@ export function ThreadDrawer({
|
|||
};
|
||||
|
||||
const handleCommentSubmit = async () => {
|
||||
if (!projectRoot || !id || !comment.trim()) {
|
||||
const targetIssueId = issue?.id ?? '';
|
||||
if (!projectRoot || !targetIssueId || !comment.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCommentState('sending');
|
||||
|
||||
try {
|
||||
await postComment(projectRoot, id, comment.trim());
|
||||
await postComment(projectRoot, targetIssueId, comment.trim());
|
||||
setComment('');
|
||||
setCommentState('sent');
|
||||
// Refresh comments
|
||||
const response = await fetch(`/api/beads/${id}/comments?projectRoot=${encodeURIComponent(projectRoot)}`);
|
||||
const response = await fetch(`/api/beads/${targetIssueId}/comments?projectRoot=${encodeURIComponent(projectRoot)}`);
|
||||
const payload = (await response.json()) as { ok: boolean; comments?: CommentFromApi[] };
|
||||
if (payload.ok && payload.comments) {
|
||||
setComments(payload.comments);
|
||||
}
|
||||
await onIssueUpdated?.(id);
|
||||
await onIssueUpdated?.(targetIssueId);
|
||||
setTimeout(() => setCommentState('ready'), 900);
|
||||
} catch (error) {
|
||||
console.error('Comment failed:', error);
|
||||
|
|
@ -414,7 +415,7 @@ export function ThreadDrawer({
|
|||
<div className="mb-1 flex items-center gap-2">
|
||||
<p className="font-mono text-xs font-semibold text-[var(--ui-accent-info)]">#{id}</p>
|
||||
<span className="rounded-full border border-[var(--ui-accent-ready)]/45 bg-[var(--ui-accent-ready)]/20 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] text-[#d8ffe8]">
|
||||
In Progress
|
||||
{issue?.status?.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) ?? 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="truncate text-[40px] font-semibold leading-[1.12] tracking-[-0.02em] text-[var(--ui-text-primary)]" title={title}>{title}</h2>
|
||||
|
|
@ -473,13 +474,13 @@ export function ThreadDrawer({
|
|||
placeholder="Type a message to neighbors..."
|
||||
className="border-0 bg-transparent text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)]"
|
||||
autoComplete="off"
|
||||
disabled={commentState === 'sending'}
|
||||
disabled={commentState === 'sending' || !issue || !projectRoot}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
className="h-8 rounded-full bg-[var(--ui-accent-action-green)] px-3 text-[#082012] hover:bg-[color-mix(in_srgb,var(--ui-accent-action-green)_86%,white)] disabled:opacity-50"
|
||||
onClick={() => void handleCommentSubmit()}
|
||||
disabled={!comment.trim() || commentState === 'sending'}
|
||||
disabled={!comment.trim() || commentState === 'sending' || !issue || !projectRoot}
|
||||
>
|
||||
<Send className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,226 +1,243 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { BeadIssue } from '../../lib/types';
|
||||
import type { ProjectScopeOption } from '../../lib/project-scope';
|
||||
import { TopBar } from './top-bar';
|
||||
import { LeftPanel, type LeftPanelFilters } from './left-panel';
|
||||
import { RightPanel } from './right-panel';
|
||||
import { MobileNav } from './mobile-nav';
|
||||
import { ThreadDrawer } from './thread-drawer';
|
||||
import { ResizeHandle } from './resize-handle';
|
||||
import { useUrlState } from '../../hooks/use-url-state';
|
||||
import { usePanelResize } from '../../hooks/use-panel-resize';
|
||||
import { SmartDag } from '../graph/smart-dag';
|
||||
import { SocialPage } from '../social/social-page';
|
||||
import { buildSocialCards } from '../../lib/social-cards';
|
||||
import { ContextualRightPanel } from '../activity/contextual-right-panel';
|
||||
import { AssignmentPanel } from '../graph/assignment-panel';
|
||||
import { useSwarmList } from '../../hooks/use-swarm-list';
|
||||
import { useBeadsSubscription } from '../../hooks/use-beads-subscription';
|
||||
|
||||
export interface UnifiedShellProps {
|
||||
issues: BeadIssue[];
|
||||
projectRoot: string;
|
||||
projectScopeKey: string;
|
||||
projectScopeOptions: ProjectScopeOption[];
|
||||
projectScopeMode: 'single' | 'aggregate';
|
||||
}
|
||||
|
||||
export function UnifiedShell({
|
||||
issues: initialIssues,
|
||||
projectRoot,
|
||||
projectScopeOptions,
|
||||
}: UnifiedShellProps) {
|
||||
const router = useRouter();
|
||||
const { view, taskId, setTaskId, swarmId, graphTab, panel, drawer, setDrawer, epicId, setEpicId } = useUrlState();
|
||||
|
||||
// Subscribe to SSE for real-time updates on ALL views
|
||||
const { issues } = useBeadsSubscription(initialIssues, projectRoot);
|
||||
|
||||
const [filters, setFilters] = useState<LeftPanelFilters>({
|
||||
query: '',
|
||||
status: 'all',
|
||||
priority: 'all',
|
||||
preset: 'all',
|
||||
hideClosed: true,
|
||||
});
|
||||
|
||||
const [customRightPanel, setCustomRightPanel] = useState<React.ReactNode | null>(null);
|
||||
|
||||
// Assign mode state for graph view
|
||||
const [assignMode, setAssignMode] = useState(false);
|
||||
const [selectedAssignIssue, setSelectedAssignIssue] = useState<BeadIssue | null>(null);
|
||||
|
||||
const socialCards = useMemo(() => buildSocialCards(issues), [issues]);
|
||||
const { swarms: swarmCards } = useSwarmList(projectRoot);
|
||||
|
||||
const selectedSocialCard = taskId ? socialCards.find(c => c.id === taskId) : null;
|
||||
const selectedSwarmCard = swarmId ? swarmCards.find(c => c.swarmId === swarmId) : null;
|
||||
const selectedIssue = taskId ? issues.find((issue) => issue.id === taskId) ?? null : null;
|
||||
|
||||
const handleGraphSelect = useMemo(() => (id: string) => {
|
||||
setTaskId(id);
|
||||
setCustomRightPanel(null); // Reset when switching context
|
||||
}, [setTaskId]);
|
||||
|
||||
const handleCardSelect = useMemo(() => (id: string) => {
|
||||
if (view === 'social') {
|
||||
setTaskId(id, true);
|
||||
}
|
||||
}, [view, setTaskId]);
|
||||
|
||||
const handleCloseDrawer = useMemo(() => () => {
|
||||
setDrawer('closed');
|
||||
}, [setDrawer]);
|
||||
|
||||
// Handle assign mode change from SmartDag
|
||||
const handleAssignModeChange = useMemo(() => (mode: boolean) => {
|
||||
setAssignMode(mode);
|
||||
if (!mode) {
|
||||
setSelectedAssignIssue(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle selected issue change from SmartDag (for assignment panel)
|
||||
const handleSelectedIssueChange = useMemo(() => (issue: BeadIssue | null) => {
|
||||
setSelectedAssignIssue(issue);
|
||||
}, []);
|
||||
|
||||
// Chat Mode Logic: If a card is selected (drawer='open'), we show Chat popup
|
||||
const isChatOpen = drawer === 'open' && (!!taskId || !!swarmId);
|
||||
const drawerTitle = selectedSocialCard?.title || selectedSwarmCard?.title || '';
|
||||
const drawerId = taskId || swarmId || '';
|
||||
|
||||
// Panel resize hook
|
||||
const { leftWidth, rightWidth, handleLeftResize, handleRightResize } = usePanelResize();
|
||||
|
||||
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 (
|
||||
<SmartDag
|
||||
issues={filteredIssues}
|
||||
epicId={epicId}
|
||||
selectedTaskId={taskId ?? undefined}
|
||||
onSelectTask={handleGraphSelect}
|
||||
projectRoot={projectRoot}
|
||||
hideClosed={graphTab !== 'flow'}
|
||||
onAssignModeChange={handleAssignModeChange}
|
||||
onSelectedIssueChange={handleSelectedIssueChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (view === 'social') {
|
||||
return (
|
||||
<SocialPage
|
||||
issues={filteredIssues}
|
||||
selectedId={taskId ?? undefined}
|
||||
onSelect={handleCardSelect}
|
||||
projectScopeOptions={projectScopeOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Render right panel content based on view and assign mode
|
||||
const renderRightPanelContent = () => {
|
||||
if (customRightPanel) {
|
||||
return customRightPanel;
|
||||
}
|
||||
|
||||
// Show AssignmentPanel when in graph view with assign mode enabled
|
||||
if (view === 'graph' && assignMode) {
|
||||
return (
|
||||
<AssignmentPanel
|
||||
selectedIssue={selectedAssignIssue}
|
||||
projectRoot={projectRoot}
|
||||
issues={issues}
|
||||
epicId={epicId ?? undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: ContextualRightPanel
|
||||
return <ContextualRightPanel epicId={epicId} issues={issues} projectRoot={projectRoot} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-[var(--surface-backdrop)]" data-testid="unified-shell">
|
||||
{/* TOP BAR: 3rem fixed */}
|
||||
<TopBar />
|
||||
|
||||
{/* MAIN AREA: Flex layout for resizable panels */}
|
||||
<div
|
||||
className="flex-1 flex overflow-hidden"
|
||||
data-testid="main-area"
|
||||
>
|
||||
{/* LEFT PANEL */}
|
||||
<div style={{ width: leftWidth }} className="flex-shrink-0 overflow-hidden">
|
||||
<LeftPanel
|
||||
issues={issues}
|
||||
selectedEpicId={epicId}
|
||||
onEpicSelect={setEpicId}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* RESIZE HANDLE: Left */}
|
||||
<ResizeHandle direction="left" onResize={handleLeftResize} />
|
||||
|
||||
{/* MIDDLE CONTENT: flex-1 */}
|
||||
<div className="flex-1 relative overflow-hidden bg-[var(--surface-secondary)]" data-testid="middle-content">
|
||||
{renderMiddleContent()}
|
||||
</div>
|
||||
|
||||
{/* RESIZE HANDLE: Right (only when panel open) */}
|
||||
{panel === 'open' && <ResizeHandle direction="right" onResize={handleRightResize} />}
|
||||
|
||||
{/* RIGHT PANEL */}
|
||||
{panel === 'open' && (
|
||||
<div style={{ width: rightWidth }} className="flex-shrink-0 overflow-hidden">
|
||||
<RightPanel isOpen={true}>
|
||||
{renderRightPanelContent()}
|
||||
</RightPanel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* THREAD DRAWER: Popup overlay when a task is selected */}
|
||||
{isChatOpen ? (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[var(--alpha-black-medium)] p-4">
|
||||
<div className="h-[85vh] w-full max-w-lg overflow-hidden rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-quaternary)] shadow-2xl">
|
||||
<ThreadDrawer
|
||||
isOpen={true}
|
||||
onClose={handleCloseDrawer}
|
||||
title={drawerTitle}
|
||||
id={drawerId}
|
||||
embedded={true}
|
||||
issue={selectedIssue}
|
||||
projectRoot={projectRoot}
|
||||
onIssueUpdated={async () => {
|
||||
router.refresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* MOBILE NAV: Bottom tab bar */}
|
||||
<MobileNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { BeadIssue } from '../../lib/types';
|
||||
import type { ProjectScopeOption } from '../../lib/project-scope';
|
||||
import { TopBar } from './top-bar';
|
||||
import { LeftPanel, type LeftPanelFilters } from './left-panel';
|
||||
import { RightPanel } from './right-panel';
|
||||
import { MobileNav } from './mobile-nav';
|
||||
import { ThreadDrawer } from './thread-drawer';
|
||||
import { ResizeHandle } from './resize-handle';
|
||||
import { useUrlState } from '../../hooks/use-url-state';
|
||||
import { usePanelResize } from '../../hooks/use-panel-resize';
|
||||
import { SmartDag } from '../graph/smart-dag';
|
||||
import { SocialPage } from '../social/social-page';
|
||||
import { buildSocialCards } from '../../lib/social-cards';
|
||||
import { ContextualRightPanel } from '../activity/contextual-right-panel';
|
||||
import { AssignmentPanel } from '../graph/assignment-panel';
|
||||
import { useSwarmList } from '../../hooks/use-swarm-list';
|
||||
import { useBeadsSubscription } from '../../hooks/use-beads-subscription';
|
||||
import { useBdHealth } from '../../hooks/use-bd-health';
|
||||
|
||||
export interface UnifiedShellProps {
|
||||
issues: BeadIssue[];
|
||||
projectRoot: string;
|
||||
projectScopeKey: string;
|
||||
projectScopeOptions: ProjectScopeOption[];
|
||||
projectScopeMode: 'single' | 'aggregate';
|
||||
}
|
||||
|
||||
export function UnifiedShell({
|
||||
issues: initialIssues,
|
||||
projectRoot,
|
||||
projectScopeOptions,
|
||||
}: UnifiedShellProps) {
|
||||
const router = useRouter();
|
||||
const { view, taskId, setTaskId, swarmId, graphTab, panel, drawer, setDrawer, epicId, setEpicId, blockedOnly } = useUrlState();
|
||||
|
||||
// Subscribe to SSE for real-time updates on ALL views
|
||||
const { issues } = useBeadsSubscription(initialIssues, projectRoot);
|
||||
|
||||
const [filters, setFilters] = useState<LeftPanelFilters>({
|
||||
query: '',
|
||||
status: 'all',
|
||||
priority: 'all',
|
||||
preset: 'all',
|
||||
hideClosed: true,
|
||||
});
|
||||
|
||||
const [customRightPanel, setCustomRightPanel] = useState<React.ReactNode | null>(null);
|
||||
|
||||
// Assign mode state for graph view
|
||||
const [assignMode, setAssignMode] = useState(false);
|
||||
const [selectedAssignIssue, setSelectedAssignIssue] = useState<BeadIssue | null>(null);
|
||||
|
||||
const socialCards = useMemo(() => buildSocialCards(issues), [issues]);
|
||||
const { swarms: swarmCards } = useSwarmList(projectRoot);
|
||||
const bdHealth = useBdHealth(projectRoot);
|
||||
|
||||
const selectedSocialCard = taskId ? socialCards.find(c => c.id === taskId) : null;
|
||||
const selectedSwarmCard = swarmId ? swarmCards.find(c => c.swarmId === swarmId) : null;
|
||||
const selectedIssue = taskId ? issues.find((issue) => issue.id === taskId) ?? null : null;
|
||||
|
||||
const handleGraphSelect = useMemo(() => (id: string) => {
|
||||
setTaskId(id);
|
||||
setCustomRightPanel(null); // Reset when switching context
|
||||
}, [setTaskId]);
|
||||
|
||||
const handleCardSelect = useMemo(() => (id: string) => {
|
||||
if (view === 'social') {
|
||||
setTaskId(id, true);
|
||||
}
|
||||
}, [view, setTaskId]);
|
||||
|
||||
const handleCloseDrawer = useMemo(() => () => {
|
||||
setDrawer('closed');
|
||||
}, [setDrawer]);
|
||||
|
||||
// Handle assign mode change from SmartDag
|
||||
const handleAssignModeChange = useMemo(() => (mode: boolean) => {
|
||||
setAssignMode(mode);
|
||||
if (!mode) {
|
||||
setSelectedAssignIssue(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle selected issue change from SmartDag (for assignment panel)
|
||||
const handleSelectedIssueChange = useMemo(() => (issue: BeadIssue | null) => {
|
||||
setSelectedAssignIssue(issue);
|
||||
}, []);
|
||||
|
||||
// Chat Mode Logic: If a card is selected (drawer='open'), we show Chat popup
|
||||
const isChatOpen = drawer === 'open' && (!!taskId || !!swarmId || !!epicId);
|
||||
const selectedEpic = epicId ? issues.find((issue) => issue.id === epicId && issue.issue_type === 'epic') ?? null : null;
|
||||
const drawerTitle = selectedSocialCard?.title || selectedSwarmCard?.title || selectedEpic?.title || '';
|
||||
const drawerId = taskId || swarmId || epicId || '';
|
||||
const selectedItem = selectedEpic ?? selectedIssue;
|
||||
|
||||
// Panel resize hook
|
||||
const { leftWidth, rightWidth, handleLeftResize, handleRightResize } = usePanelResize();
|
||||
|
||||
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 (
|
||||
<SmartDag
|
||||
issues={filteredIssues}
|
||||
epicId={epicId}
|
||||
selectedTaskId={taskId ?? undefined}
|
||||
onSelectTask={handleGraphSelect}
|
||||
projectRoot={projectRoot}
|
||||
hideClosed={graphTab !== 'flow'}
|
||||
onAssignModeChange={handleAssignModeChange}
|
||||
onSelectedIssueChange={handleSelectedIssueChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (view === 'social') {
|
||||
return (
|
||||
<SocialPage
|
||||
issues={filteredIssues}
|
||||
selectedId={taskId ?? undefined}
|
||||
onSelect={handleCardSelect}
|
||||
projectScopeOptions={projectScopeOptions}
|
||||
blockedOnly={blockedOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Render right panel content based on view and assign mode
|
||||
const renderRightPanelContent = () => {
|
||||
if (customRightPanel) {
|
||||
return customRightPanel;
|
||||
}
|
||||
|
||||
// Show AssignmentPanel when in graph view with assign mode enabled
|
||||
if (view === 'graph' && assignMode) {
|
||||
return (
|
||||
<AssignmentPanel
|
||||
selectedIssue={selectedAssignIssue}
|
||||
projectRoot={projectRoot}
|
||||
issues={issues}
|
||||
epicId={epicId ?? undefined}
|
||||
onIssueUpdated={async () => { router.refresh(); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: ContextualRightPanel
|
||||
return <ContextualRightPanel epicId={epicId} taskId={taskId} swarmId={swarmId} issues={issues} projectRoot={projectRoot} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-[var(--surface-backdrop)]" data-testid="unified-shell">
|
||||
{/* TOP BAR: 3rem fixed */}
|
||||
<TopBar
|
||||
totalTasks={issues.filter(i => i.issue_type !== 'epic').length}
|
||||
criticalAlerts={issues.filter(i => i.status === 'blocked').length}
|
||||
busyCount={issues.filter(i => i.status === 'in_progress').length}
|
||||
idleCount={0}
|
||||
/>
|
||||
{!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">
|
||||
<span className="font-semibold">BD setup issue:</span> {bdHealth.message}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* MAIN AREA: Flex layout for resizable panels */}
|
||||
<div
|
||||
className="flex-1 flex overflow-hidden"
|
||||
data-testid="main-area"
|
||||
>
|
||||
{/* LEFT PANEL */}
|
||||
<div style={{ width: leftWidth }} className="flex-shrink-0 overflow-hidden">
|
||||
<LeftPanel
|
||||
issues={issues}
|
||||
selectedEpicId={epicId}
|
||||
onEpicSelect={setEpicId}
|
||||
onEpicEdit={(id) => { setEpicId(id); setDrawer('open'); }}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* RESIZE HANDLE: Left */}
|
||||
<ResizeHandle direction="left" onResize={handleLeftResize} />
|
||||
|
||||
{/* MIDDLE CONTENT: flex-1 */}
|
||||
<div className="flex-1 relative overflow-hidden bg-[var(--surface-secondary)]" data-testid="middle-content">
|
||||
{renderMiddleContent()}
|
||||
</div>
|
||||
|
||||
{/* RESIZE HANDLE: Right (only when panel open) */}
|
||||
{panel === 'open' && <ResizeHandle direction="right" onResize={handleRightResize} />}
|
||||
|
||||
{/* RIGHT PANEL */}
|
||||
{panel === 'open' && (
|
||||
<div style={{ width: rightWidth }} className="flex-shrink-0 overflow-hidden">
|
||||
<RightPanel isOpen={true}>
|
||||
{renderRightPanelContent()}
|
||||
</RightPanel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* THREAD DRAWER: Popup overlay when a task is selected */}
|
||||
{isChatOpen ? (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[var(--alpha-black-medium)] p-4">
|
||||
<div className="h-[85vh] w-full max-w-lg overflow-hidden rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-quaternary)] shadow-2xl">
|
||||
<ThreadDrawer
|
||||
isOpen={true}
|
||||
onClose={handleCloseDrawer}
|
||||
title={drawerTitle}
|
||||
id={drawerId}
|
||||
embedded={true}
|
||||
issue={selectedItem}
|
||||
projectRoot={projectRoot}
|
||||
onIssueUpdated={async () => {
|
||||
router.refresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* MOBILE NAV: Bottom tab bar */}
|
||||
<MobileNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,7 +219,6 @@ export function SocialPage({
|
|||
}
|
||||
onJumpToActivity={(id) =>
|
||||
navigateWithParams({
|
||||
view: 'activity',
|
||||
task: id,
|
||||
right: 'open',
|
||||
panel: 'open',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue