From 71ab27d02ab8945c154ff79501e95d89b5e506aa Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:45:40 +0800 Subject: [PATCH] feat: add zustand modal store and clean up theme variables - Add zustand for global modal state management - Create modalStore with settings/newSession/deleteSession modals - Create Modals.tsx component for centralized modal rendering - Move Settings button to sidebar footer - Clean up old --color-* CSS variables, use shadcn standard variables - Delete unused base.css and main.css files - Update components to use Tailwind classes (bg-background, text-foreground, etc.) Co-Authored-By: Claude Opus 4.5 --- package.json | 3 +- pnpm-lock.yaml | 26 +++ src/renderer/src/App.tsx | 96 ++-------- src/renderer/src/assets/base.css | 67 ------- src/renderer/src/assets/main.css | 171 ----------------- src/renderer/src/components/AppSidebar.tsx | 119 +++++------- src/renderer/src/components/ChatView.tsx | 22 +-- src/renderer/src/components/MessageInput.tsx | 6 +- src/renderer/src/components/Modals.tsx | 189 +++++++++++++++++++ src/renderer/src/components/Settings.tsx | 2 +- src/renderer/src/components/StatusBar.tsx | 13 +- src/renderer/src/stores/modalStore.ts | 57 ++++++ src/renderer/src/styles/index.css | 37 +--- 13 files changed, 353 insertions(+), 455 deletions(-) delete mode 100644 src/renderer/src/assets/base.css delete mode 100644 src/renderer/src/assets/main.css create mode 100644 src/renderer/src/components/Modals.tsx create mode 100644 src/renderer/src/stores/modalStore.ts diff --git a/package.json b/package.json index b86b2bc697..6c045b72d9 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "clsx": "^2.1.1", "lucide-react": "^0.562.0", "react-markdown": "^10.1.0", - "tailwind-merge": "^3.4.0" + "tailwind-merge": "^3.4.0", + "zustand": "^5.0.10" }, "devDependencies": { "@electron-toolkit/eslint-config-prettier": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7736c156e1..741ea3025e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: tailwind-merge: specifier: ^3.4.0 version: 3.4.0 + zustand: + specifier: ^5.0.10 + version: 5.0.10(@types/react@19.2.8)(react@19.2.3) devDependencies: '@electron-toolkit/eslint-config-prettier': specifier: ^3.0.0 @@ -3735,6 +3738,24 @@ packages: zod@4.3.5: resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + zustand@5.0.10: + resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -7793,4 +7814,9 @@ snapshots: zod@4.3.5: {} + zustand@5.0.10(@types/react@19.2.8)(react@19.2.3): + optionalDependencies: + '@types/react': 19.2.8 + react: 19.2.3 + zwitch@2.0.4: {} diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 97e33513a7..0b80e5d507 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,21 +1,14 @@ /** * Main App component */ -import { useState, useEffect } from 'react' +import { useEffect } from 'react' import { useApp } from './hooks/useApp' -import { ChatView, MessageInput, StatusBar, Settings } from './components' +import { ChatView, MessageInput, StatusBar } from './components' import { AppSidebar } from './components/AppSidebar' +import { Modals } from './components/Modals' import { ThemeProvider } from './contexts/ThemeContext' import { SidebarProvider } from '@/components/ui/sidebar' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' +import { useModalStore } from './stores/modalStore' function AppContent(): React.JSX.Element { const { @@ -39,37 +32,25 @@ function AppContent(): React.JSX.Element { clearError, } = useApp() - // New session dialog state - const [showNewSession, setShowNewSession] = useState(false) - const [newSessionCwd, setNewSessionCwd] = useState('') - - // Settings dialog state - const [showSettings, setShowSettings] = useState(false) + const openModal = useModalStore((s) => s.openModal) // Auto-show new session dialog when agent is running but no session useEffect(() => { if (agentStatus.state === 'running' && !currentSession && sessions.length === 0) { - setNewSessionCwd('') - setShowNewSession(true) + openModal('newSession') } - }, [agentStatus.state, currentSession, sessions.length]) + }, [agentStatus.state, currentSession, sessions.length, openModal]) const handleNewSession = () => { - setNewSessionCwd('') - setShowNewSession(true) + openModal('newSession') } - const handleCreateSession = async () => { - if (!newSessionCwd.trim()) return - + const handleCreateSession = async (cwd: string) => { // Ensure agent is running if (agentStatus.state !== 'running') { await startAgent('opencode') } - - await createSession(newSessionCwd.trim()) - setShowNewSession(false) - setNewSessionCwd('') + await createSession(cwd) } const handleSelectSession = async (sessionId: string) => { @@ -84,7 +65,7 @@ function AppContent(): React.JSX.Element { } return ( -
+
{/* Error banner */} {error && (
@@ -102,7 +83,6 @@ function AppContent(): React.JSX.Element { sessions={sessions} currentSessionId={currentSession?.id ?? null} onSelect={handleSelectSession} - onDelete={deleteSession} onNewSession={handleNewSession} /> @@ -114,7 +94,6 @@ function AppContent(): React.JSX.Element { currentSession={currentSession} onStartAgent={() => startAgent('opencode')} onStopAgent={stopAgent} - onOpenSettings={() => setShowSettings(true)} /> {/* Chat view */} @@ -135,57 +114,12 @@ function AppContent(): React.JSX.Element { - {/* New session dialog */} - - - - New Session - - -
-
- -
- setNewSessionCwd(e.target.value)} - placeholder="Select a directory..." - onKeyDown={(e) => { - if (e.key === 'Enter') handleCreateSession() - }} - /> - -
-
-
- - - - - -
-
- - {/* Settings dialog */} - setShowSettings(false)} + {/* Global modals */} +
) diff --git a/src/renderer/src/assets/base.css b/src/renderer/src/assets/base.css deleted file mode 100644 index 5ed6406a34..0000000000 --- a/src/renderer/src/assets/base.css +++ /dev/null @@ -1,67 +0,0 @@ -:root { - --ev-c-white: #ffffff; - --ev-c-white-soft: #f8f8f8; - --ev-c-white-mute: #f2f2f2; - - --ev-c-black: #1b1b1f; - --ev-c-black-soft: #222222; - --ev-c-black-mute: #282828; - - --ev-c-gray-1: #515c67; - --ev-c-gray-2: #414853; - --ev-c-gray-3: #32363f; - - --ev-c-text-1: rgba(255, 255, 245, 0.86); - --ev-c-text-2: rgba(235, 235, 245, 0.6); - --ev-c-text-3: rgba(235, 235, 245, 0.38); - - --ev-button-alt-border: transparent; - --ev-button-alt-text: var(--ev-c-text-1); - --ev-button-alt-bg: var(--ev-c-gray-3); - --ev-button-alt-hover-border: transparent; - --ev-button-alt-hover-text: var(--ev-c-text-1); - --ev-button-alt-hover-bg: var(--ev-c-gray-2); -} - -:root { - --color-background: var(--ev-c-black); - --color-background-soft: var(--ev-c-black-soft); - --color-background-mute: var(--ev-c-black-mute); - - --color-text: var(--ev-c-text-1); -} - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - font-weight: normal; -} - -ul { - list-style: none; -} - -body { - min-height: 100vh; - color: var(--color-text); - background: var(--color-background); - line-height: 1.6; - font-family: - Inter, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - Oxygen, - Ubuntu, - Cantarell, - 'Fira Sans', - 'Droid Sans', - 'Helvetica Neue', - sans-serif; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css deleted file mode 100644 index 0179fc4c27..0000000000 --- a/src/renderer/src/assets/main.css +++ /dev/null @@ -1,171 +0,0 @@ -@import './base.css'; - -body { - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - background-image: url('./wavy-lines.svg'); - background-size: cover; - user-select: none; -} - -code { - font-weight: 600; - padding: 3px 5px; - border-radius: 2px; - background-color: var(--color-background-mute); - font-family: - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace; - font-size: 85%; -} - -#root { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - margin-bottom: 80px; -} - -.logo { - margin-bottom: 20px; - -webkit-user-drag: none; - height: 128px; - width: 128px; - will-change: filter; - transition: filter 300ms; -} - -.logo:hover { - filter: drop-shadow(0 0 1.2em #6988e6aa); -} - -.creator { - font-size: 14px; - line-height: 16px; - color: var(--ev-c-text-2); - font-weight: 600; - margin-bottom: 10px; -} - -.text { - font-size: 28px; - color: var(--ev-c-text-1); - font-weight: 700; - line-height: 32px; - text-align: center; - margin: 0 10px; - padding: 16px 0; -} - -.tip { - font-size: 16px; - line-height: 24px; - color: var(--ev-c-text-2); - font-weight: 600; -} - -.react { - background: -webkit-linear-gradient(315deg, #087ea4 55%, #7c93ee); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-weight: 700; -} - -.ts { - background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-weight: 700; -} - -.actions { - display: flex; - padding-top: 32px; - margin: -6px; - flex-wrap: wrap; - justify-content: flex-start; -} - -.action { - flex-shrink: 0; - padding: 6px; -} - -.action a { - cursor: pointer; - text-decoration: none; - display: inline-block; - border: 1px solid transparent; - text-align: center; - font-weight: 600; - white-space: nowrap; - border-radius: 20px; - padding: 0 20px; - line-height: 38px; - font-size: 14px; - border-color: var(--ev-button-alt-border); - color: var(--ev-button-alt-text); - background-color: var(--ev-button-alt-bg); -} - -.action a:hover { - border-color: var(--ev-button-alt-hover-border); - color: var(--ev-button-alt-hover-text); - background-color: var(--ev-button-alt-hover-bg); -} - -.versions { - position: absolute; - bottom: 30px; - margin: 0 auto; - padding: 15px 0; - font-family: 'Menlo', 'Lucida Console', monospace; - display: inline-flex; - overflow: hidden; - align-items: center; - border-radius: 22px; - background-color: #202127; - backdrop-filter: blur(24px); -} - -.versions li { - display: block; - float: left; - border-right: 1px solid var(--ev-c-gray-1); - padding: 0 20px; - font-size: 14px; - line-height: 14px; - opacity: 0.8; - &:last-child { - border: none; - } -} - -@media (max-width: 720px) { - .text { - font-size: 20px; - } -} - -@media (max-width: 620px) { - .versions { - display: none; - } -} - -@media (max-width: 350px) { - .tip, - .actions { - display: none; - } -} diff --git a/src/renderer/src/components/AppSidebar.tsx b/src/renderer/src/components/AppSidebar.tsx index 63b5060f2b..c1ba1c851f 100644 --- a/src/renderer/src/components/AppSidebar.tsx +++ b/src/renderer/src/components/AppSidebar.tsx @@ -6,33 +6,26 @@ import type { MulticaSession } from '../../../shared/types' import { Sidebar, SidebarContent, + SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, } from '@/components/ui/sidebar' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' -import { Plus, Trash2 } from 'lucide-react' +import { Plus, Settings, Trash2 } from 'lucide-react' +import { useModalStore } from '../stores/modalStore' interface AppSidebarProps { sessions: MulticaSession[] currentSessionId: string | null onSelect: (sessionId: string) => void - onDelete: (sessionId: string) => void onNewSession: () => void } @@ -73,27 +66,21 @@ function SessionItem({ session, isActive, onSelect, onDelete }: SessionItemProps onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - + - {/* Status indicator */} - + {/* Error indicator - only show on error */} + {session.status === 'error' && ( + + )} {/* Content */}
@@ -162,67 +149,49 @@ export function AppSidebar({ sessions, currentSessionId, onSelect, - onDelete, onNewSession, }: AppSidebarProps) { - const [deleteSession, setDeleteSession] = useState(null) - - const handleConfirmDelete = () => { - if (deleteSession) { - onDelete(deleteSession.id) - setDeleteSession(null) - } - } + const openModal = useModalStore((s) => s.openModal) return ( - <> - - {/* Header - just for traffic lights spacing */} - + + {/* Header - just for traffic lights spacing */} + - - {/* New task button */} - + + {/* New task button */} + - {/* Recent label */} -

Recent

+ {/* Recent label */} +

Recent

- {/* Session list */} - -
-
+ {/* Session list */} + openModal('deleteSession', session)} + /> + - {/* Delete confirmation dialog */} - !open && setDeleteSession(null)}> - - - Delete Task - - Are you sure you want to delete "{deleteSession && getSessionTitle(deleteSession)}"? This action cannot be undone. - - - - - - - - - + + + +
) } diff --git a/src/renderer/src/components/ChatView.tsx b/src/renderer/src/components/ChatView.tsx index a7358ccbb5..59693328b1 100644 --- a/src/renderer/src/components/ChatView.tsx +++ b/src/renderer/src/components/ChatView.tsx @@ -31,7 +31,7 @@ export function ChatView({ updates, isProcessing, hasSession, onNewSession }: Ch

Multica

-

+

{hasSession ? 'Start a conversation with your coding agent' : 'Create a session to start chatting'} @@ -54,7 +54,7 @@ export function ChatView({ updates, isProcessing, hasSession, onNewSession }: Ch ))} {isProcessing && ( -

+
Agent is thinking...
@@ -220,7 +220,7 @@ function MessageBubble({ message }: MessageBubbleProps) { if (isUser) { return (
-
+
{message.content}
@@ -257,33 +257,33 @@ function MessageBubble({ message }: MessageBubbleProps) { const isBlock = className?.includes('language-') if (isBlock) { return ( - + {children} ) } return ( - + {children} ) }, pre: ({ children }) => ( -
+                
                   {children}
                 
), a: ({ href, children }) => ( - + {children} ), blockquote: ({ children }) => ( -
+
{children}
), - hr: () =>
, + hr: () =>
, strong: ({ children }) => {children}, em: ({ children }) => {children}, }} @@ -377,7 +377,7 @@ function ToolCallLine({ toolCall }: { toolCall: ToolCall }) { const isFailed = toolCall.status === 'failed' return ( -
+
{/* Icon */} {icon} @@ -386,7 +386,7 @@ function ToolCallLine({ toolCall }: { toolCall: ToolCall }) { {/* Detail in code pill */} {detail && ( - + {detail} )} diff --git a/src/renderer/src/components/MessageInput.tsx b/src/renderer/src/components/MessageInput.tsx index 558e8732b1..14dbc4ddae 100644 --- a/src/renderer/src/components/MessageInput.tsx +++ b/src/renderer/src/components/MessageInput.tsx @@ -52,7 +52,7 @@ export function MessageInput({ } return ( -
+