From 6a41350bd1d4508a7d108050136db6c0ab00d27e Mon Sep 17 00:00:00 2001 From: Philip Zeyliger Date: Sun, 11 Jan 2026 05:45:48 +0000 Subject: [PATCH] shelley/ui: add collapsible conversations drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt: make the conversations drawer collapsible. Currently, it works differently on narrow and wide screens. Continue having it work differently, but have an affordance for collapsibility. Maybe it's the same button, but it does slightly different things on a wider screen. Fixes https://github.com/boldsoftware/shelley/issues/4 On desktop (≥768px): - Add collapse button (double chevron «) in drawer header - When collapsed, drawer is hidden and expand button (») appears in main header before title - Drawer can be expanded/collapsed by clicking the respective buttons On mobile (<768px): - Behavior unchanged - drawer slides in/out as overlay - Collapse/expand buttons are hidden on mobile (uses existing hamburger menu pattern) The collapsed state is persisted in React state and is independent of the mobile drawer open/close state. --- ui/src/App.tsx | 9 +++++++++ ui/src/components/ChatInterface.tsx | 23 +++++++++++++++++++++++ ui/src/components/ConversationDrawer.tsx | 22 +++++++++++++++++++++- ui/src/styles.css | 15 +++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 3c328b38bd86f8e66b62350b6047486cbce24d76..53519b1b83181435dfab3268f223886d2ce7bc9c 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -61,6 +61,7 @@ function App() { const [conversations, setConversations] = useState([]); const [currentConversationId, setCurrentConversationId] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); + const [drawerCollapsed, setDrawerCollapsed] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const initialSlugResolved = useRef(false); @@ -146,6 +147,10 @@ function App() { setDrawerOpen(false); }; + const toggleDrawerCollapsed = () => { + setDrawerCollapsed((prev) => !prev); + }; + const updateConversation = (updatedConversation: Conversation) => { setConversations((prev) => prev.map((conv) => @@ -229,7 +234,9 @@ function App() { {/* Conversations drawer */} setDrawerOpen(false)} + onToggleCollapse={toggleDrawerCollapsed} conversations={conversations} currentConversationId={currentConversationId} onSelectConversation={selectConversation} @@ -249,6 +256,8 @@ function App() { onConversationUpdate={updateConversation} onFirstMessage={handleFirstMessage} mostRecentCwd={mostRecentCwd} + isDrawerCollapsed={drawerCollapsed} + onToggleDrawerCollapse={toggleDrawerCollapsed} /> diff --git a/ui/src/components/ChatInterface.tsx b/ui/src/components/ChatInterface.tsx index eee3b9d76edbd9d9cb49499d96518c0794374acf..1f60c0d2c53afa3cf04a3087fc111816532aba12 100644 --- a/ui/src/components/ChatInterface.tsx +++ b/ui/src/components/ChatInterface.tsx @@ -354,6 +354,8 @@ interface ChatInterfaceProps { onConversationUpdate?: (conversation: Conversation) => void; onFirstMessage?: (message: string, model: string, cwd?: string) => Promise; mostRecentCwd?: string | null; + isDrawerCollapsed?: boolean; + onToggleDrawerCollapse?: () => void; } function ChatInterface({ @@ -364,6 +366,8 @@ function ChatInterface({ onConversationUpdate, onFirstMessage, mostRecentCwd, + isDrawerCollapsed, + onToggleDrawerCollapse, }: ChatInterfaceProps) { const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); @@ -1041,6 +1045,25 @@ function ChatInterface({ + {/* Expand drawer button - desktop only when collapsed */} + {isDrawerCollapsed && onToggleDrawerCollapse && ( + + )} +

{getDisplayTitle()}

diff --git a/ui/src/components/ConversationDrawer.tsx b/ui/src/components/ConversationDrawer.tsx index 294ad2c02056785002a16975d44db441b298ea01..4f16da4e605be88f2c7f433aa45e783b8d21b1b7 100644 --- a/ui/src/components/ConversationDrawer.tsx +++ b/ui/src/components/ConversationDrawer.tsx @@ -4,7 +4,9 @@ import { api } from "../services/api"; interface ConversationDrawerProps { isOpen: boolean; + isCollapsed: boolean; onClose: () => void; + onToggleCollapse: () => void; conversations: Conversation[]; currentConversationId: string | null; onSelectConversation: (id: string) => void; @@ -16,7 +18,9 @@ interface ConversationDrawerProps { function ConversationDrawer({ isOpen, + isCollapsed, onClose, + onToggleCollapse, conversations, currentConversationId, onSelectConversation, @@ -189,7 +193,7 @@ function ConversationDrawer({ return ( <> {/* Drawer */} -
+
{/* Header */}

{showArchived ? "Archived" : "Conversations"}

@@ -225,6 +229,22 @@ function ConversationDrawer({ /> + {/* Collapse button - desktop only */} +
diff --git a/ui/src/styles.css b/ui/src/styles.css index e3fd300f8eee5f84450bdeb6f8d7abe5786a53f7..6b8de47efd2cd6ede454cdc9404e6c3928b362e1 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -2226,15 +2226,30 @@ svg { transform: translateX(0) !important; } + .drawer.collapsed { + display: none; + } + .hide-on-desktop { display: none !important; } + .show-on-desktop-only { + display: flex !important; + } + .backdrop { display: none !important; } } +/* Hide desktop-only elements on mobile */ +@media (max-width: 767px) { + .show-on-desktop-only { + display: none !important; + } +} + /* Rotation animation for running tools */ @keyframes rotate { from {