feat(21-04): create ChatPanel shell and wire into Layout and main.tsx

- ChatPanel: 380px right-side drawer with transition-[width] and hidden md:flex
- Two-column skeleton: 160px conversation list + flex thread area with ChatInput
- Layout: import ChatPanel, MessageSquare, useChatPanel; add chat toggle button
- Layout: useEffect closes PropertiesPanel when chatOpen becomes true
- Layout: ChatPanel rendered before PropertiesPanel in flex row
- main.tsx: ChatPanelProvider wrapping app inside PanelProvider
This commit is contained in:
Nexus Dev 2026-04-01 16:49:11 +00:00
parent d1438192b8
commit 6bfabdb701
3 changed files with 99 additions and 7 deletions

View file

@ -0,0 +1,63 @@
import { X } from "lucide-react";
import { useChatPanel } from "../context/ChatPanelContext";
import { ChatInput } from "./ChatInput";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
export function ChatPanel() {
const { chatOpen, setChatOpen } = useChatPanel();
return (
<aside
aria-label="Chat"
className="hidden md:flex overflow-hidden transition-[width] duration-100 ease-out flex-shrink-0 border-l border-border flex-col bg-background"
style={{ width: chatOpen ? 380 : 0 }}
>
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-4 py-2 min-w-[380px]">
<span className="text-sm font-medium">Chat</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => setChatOpen(false)}
aria-label="Close chat"
>
<X className="h-4 w-4" />
</Button>
</div>
{/* Two-column layout: conversation list (left) + thread (right) */}
<div className="flex flex-1 min-h-0 min-w-[380px]">
{/* Left column: conversation list -- placeholder for Plan 05 */}
<div className="w-[160px] flex-shrink-0 border-r border-border bg-card overflow-hidden">
<div className="p-3 text-center text-xs text-muted-foreground">
No conversations yet
</div>
</div>
{/* Right column: message thread + input */}
<div className="flex flex-1 flex-col min-w-0">
<ScrollArea className="flex-1 p-3">
{/* Messages placeholder -- wired in Plan 05 */}
<div className="flex items-center justify-center h-full">
<p className="text-sm text-muted-foreground">
Send a message to start this conversation.
</p>
</div>
</ScrollArea>
{/* Input area */}
<div className="border-t border-border px-3 py-2">
<ChatInput
onSend={(content) => {
// TODO: Wire to API in Plan 05
console.log("send:", content);
}}
/>
</div>
</div>
</div>
</aside>
);
}

View file

@ -1,11 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { BookOpen, Moon, Settings, Sun } from "lucide-react";
import { BookOpen, MessageSquare, Moon, Settings, Sun } from "lucide-react";
import { Link, Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
import { CompanyRail } from "./CompanyRail";
import { Sidebar } from "./Sidebar";
import { InstanceSidebar } from "./InstanceSidebar";
import { BreadcrumbBar } from "./BreadcrumbBar";
import { ChatPanel } from "./ChatPanel";
import { PropertiesPanel } from "./PropertiesPanel";
import { CommandPalette } from "./CommandPalette";
import { NewIssueDialog } from "./NewIssueDialog";
@ -18,6 +19,7 @@ import { WorktreeBanner } from "./WorktreeBanner";
import { DevRestartBanner } from "./DevRestartBanner";
import { useDialog } from "../context/DialogContext";
import { usePanel } from "../context/PanelContext";
import { useChatPanel } from "../context/ChatPanelContext";
import { useCompany } from "../context/CompanyContext";
import { useSidebar } from "../context/SidebarContext";
import { useTheme, THEME_META } from "../context/ThemeContext";
@ -49,7 +51,8 @@ function readRememberedInstanceSettingsPath(): string {
export function Layout() {
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
const { openNewIssue, openOnboarding } = useDialog();
const { togglePanelVisible } = usePanel();
const { togglePanelVisible, setPanelVisible } = usePanel();
const { chatOpen, toggleChat } = useChatPanel();
const {
companies,
loading: companiesLoading,
@ -144,6 +147,13 @@ export function Layout() {
const togglePanel = togglePanelVisible;
// Close PropertiesPanel when chat panel opens
useEffect(() => {
if (chatOpen) {
setPanelVisible(false);
}
}, [chatOpen, setPanelVisible]);
useCompanyPageMemory();
useKeyboardShortcuts({
@ -389,6 +399,21 @@ export function Layout() {
<Settings className="h-4 w-4" />
</Link>
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="hidden md:inline-flex text-muted-foreground shrink-0"
onClick={toggleChat}
aria-label={chatOpen ? "Close chat" : "Open chat"}
>
<MessageSquare className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{chatOpen ? "Close chat" : "Open chat"}</TooltipContent>
</Tooltip>
<Button
type="button"
variant="ghost"
@ -431,6 +456,7 @@ export function Layout() {
<Outlet />
)}
</main>
<ChatPanel />
<PropertiesPanel />
</div>
</div>

View file

@ -9,6 +9,7 @@ import { CompanyProvider } from "./context/CompanyContext";
import { LiveUpdatesProvider } from "./context/LiveUpdatesProvider";
import { BreadcrumbProvider } from "./context/BreadcrumbContext";
import { PanelProvider } from "./context/PanelContext";
import { ChatPanelProvider } from "./context/ChatPanelContext";
import { SidebarProvider } from "./context/SidebarContext";
import { DialogProvider } from "./context/DialogContext";
import { ToastProvider } from "./context/ToastContext";
@ -48,11 +49,13 @@ createRoot(document.getElementById("root")!).render(
<BreadcrumbProvider>
<SidebarProvider>
<PanelProvider>
<PluginLauncherProvider>
<DialogProvider>
<App />
</DialogProvider>
</PluginLauncherProvider>
<ChatPanelProvider>
<PluginLauncherProvider>
<DialogProvider>
<App />
</DialogProvider>
</PluginLauncherProvider>
</ChatPanelProvider>
</PanelProvider>
</SidebarProvider>
</BreadcrumbProvider>