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 c71c20251d..f26683ca72 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, useState } 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 { @@ -36,34 +29,25 @@ function AppContent(): React.JSX.Element { clearError, } = useApp() - // New session dialog state - const [showNewSession, setShowNewSession] = useState(false) - const [newSessionCwd, setNewSessionCwd] = useState('') - const [selectedAgentId, setSelectedAgentId] = useState('opencode') + const openModal = useModalStore((s) => s.openModal) - // Settings dialog state - const [showSettings, setShowSettings] = useState(false) + // Default agent for new sessions + const [defaultAgentId, setDefaultAgentId] = useState('opencode') // Auto-show new session dialog when no sessions exist useEffect(() => { if (!currentSession && sessions.length === 0) { - setNewSessionCwd('') - setShowNewSession(true) + openModal('newSession') } - }, [currentSession, sessions.length]) + }, [currentSession, sessions.length, openModal]) const handleNewSession = () => { - setNewSessionCwd('') - setShowNewSession(true) + openModal('newSession') } - const handleCreateSession = async () => { - if (!newSessionCwd.trim()) return - - // Create session with selected agent (agent starts automatically) - await createSession(newSessionCwd.trim(), selectedAgentId) - setShowNewSession(false) - setNewSessionCwd('') + const handleCreateSession = async (cwd: string) => { + // Create session with default agent (agent starts automatically) + await createSession(cwd, defaultAgentId) } const handleSelectSession = async (sessionId: string) => { @@ -77,7 +61,7 @@ function AppContent(): React.JSX.Element { : false return ( -
Recent
+ {/* Recent label */} +Recent
- {/* Session list */} -+
{hasSession ? 'Start a conversation with your coding agent' : 'Create a session to start chatting'} @@ -54,7 +55,7 @@ export function ChatView({ updates, isProcessing, hasSession, onNewSession }: Ch ))} {isProcessing && ( -
+
{children}
)
}
return (
-
+
{children}
)
},
pre: ({ children }) => (
-
+
{children}
),
a: ({ href, children }) => (
-
+
{children}
),
blockquote: ({ children }) => (
-
+
{children}
),
- hr: () =>
,
+ hr: () =>
,
strong: ({ children }) => {children},
em: ({ children }) => {children},
}}
@@ -369,157 +361,6 @@ function ThoughtBlock({ text }: { text: string }) {
)
}
-// Tool call line - inline display
-function ToolCallLine({ toolCall }: { toolCall: ToolCall }) {
- const { icon, action, detail } = parseToolCall(toolCall)
-
- const isRunning = toolCall.status === 'running' || toolCall.status === 'in_progress' || toolCall.status === 'pending'
- const isFailed = toolCall.status === 'failed'
-
- return (
-
- {/* Icon */}
- {icon}
-
- {/* Action */}
- {action}
-
- {/* Detail in code pill */}
- {detail && (
-
- {detail}
-
- )}
-
- {/* Running indicator */}
- {isRunning && }
-
- )
-}
-
-interface ParsedToolCall {
- icon: string
- action: string
- detail?: string
-}
-
-function parseToolCall(toolCall: ToolCall): ParsedToolCall {
- const title = toolCall.title?.toLowerCase() || ''
- const kind = toolCall.kind?.toLowerCase() || ''
-
- // Try to extract file path from input
- let filePath: string | undefined
- if (toolCall.input) {
- try {
- const parsed = JSON.parse(toolCall.input)
- filePath = parsed.file_path || parsed.path || parsed.filePath || parsed.pattern
- } catch {
- // Not JSON, might be a direct path
- if (toolCall.input.startsWith('/') || toolCall.input.includes('.')) {
- filePath = toolCall.input.split('\n')[0].trim()
- }
- }
- }
-
- // Determine icon and action based on kind/title
- if (kind === 'search' || title.includes('glob') || title.includes('search') || title.includes('grep')) {
- return {
- icon: '◎',
- action: title.includes('glob') ? 'Search files' : 'Search',
- detail: filePath || extractPathFromTitle(toolCall.title),
- }
- }
-
- if (title.includes('list') || title.startsWith('ls')) {
- return {
- icon: '▤',
- action: 'List',
- detail: extractPathFromTitle(toolCall.title),
- }
- }
-
- if (title.includes('read')) {
- return {
- icon: '◔',
- action: 'Read',
- detail: filePath || extractPathFromTitle(toolCall.title),
- }
- }
-
- if (title.includes('write') || title.includes('create')) {
- // Try to get line count from output
- let lineCount = ''
- if (toolCall.output) {
- const lines = toolCall.output.split('\n').length
- if (lines > 1) lineCount = `${lines} lines`
- }
- return {
- icon: '▤',
- action: lineCount ? `Write ${lineCount}` : 'Write',
- detail: filePath || extractPathFromTitle(toolCall.title),
- }
- }
-
- if (title.includes('edit') || title.includes('replace')) {
- return {
- icon: '✎',
- action: 'Edit',
- detail: filePath || extractPathFromTitle(toolCall.title),
- }
- }
-
- if (title.startsWith('run') || kind === 'bash' || kind === 'shell') {
- const cmd = extractCommandFromTitle(toolCall.title)
- return {
- icon: '>_',
- action: getCommandDescription(toolCall.title),
- detail: cmd,
- }
- }
-
- // Default
- return {
- icon: '◇',
- action: toolCall.title || toolCall.kind || 'Tool',
- detail: filePath,
- }
-}
-
-function extractPathFromTitle(title?: string): string | undefined {
- if (!title) return undefined
- // Look for path-like strings
- const match = title.match(/\/[\w\-./]+/)
- return match ? match[0] : undefined
-}
-
-function extractCommandFromTitle(title?: string): string | undefined {
- if (!title) return undefined
- // Remove "Run " prefix and get the command
- if (title.toLowerCase().startsWith('run ')) {
- const cmd = title.slice(4).trim()
- // Truncate if too long
- return cmd.length > 50 ? cmd.slice(0, 47) + '...' : cmd
- }
- return undefined
-}
-
-function getCommandDescription(title?: string): string {
- if (!title) return 'Run command'
- const lower = title.toLowerCase()
-
- if (lower.includes('mkdir')) return 'Create directory'
- if (lower.includes('rm ')) return 'Remove'
- if (lower.includes('mv ')) return 'Move'
- if (lower.includes('cp ')) return 'Copy'
- if (lower.includes('npm') || lower.includes('pnpm') || lower.includes('yarn')) return 'Package manager'
- if (lower.includes('git')) return 'Git'
- if (lower.includes('python')) return 'Run Python'
- if (lower.includes('node')) return 'Run Node'
- if (lower.includes('perl')) return 'Run Perl'
-
- return 'Run command'
-}
-
function LoadingDots() {
return (
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 (
-
+
{isProcessing ? (
@@ -77,7 +77,7 @@ export function MessageInput({
{/* Hint */}
-
+
Press Enter to send, Shift+Enter for new line
diff --git a/src/renderer/src/components/Modals.tsx b/src/renderer/src/components/Modals.tsx
new file mode 100644
index 0000000000..370db7c9c8
--- /dev/null
+++ b/src/renderer/src/components/Modals.tsx
@@ -0,0 +1,189 @@
+/**
+ * Global modals registry
+ * All app modals are rendered here and controlled via modalStore
+ */
+import { useState } from 'react'
+import { useModalStore, useModal } from '../stores/modalStore'
+import { Settings } from './Settings'
+import type { MulticaSession } from '../../../shared/types'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+
+interface ModalsProps {
+ // Settings props
+ defaultAgentId: string
+ onSetDefaultAgent: (agentId: string) => void
+ // NewSession props
+ onCreateSession: (cwd: string) => Promise
+ // DeleteSession props
+ onDeleteSession: (sessionId: string) => void
+}
+
+export function Modals({
+ defaultAgentId,
+ onSetDefaultAgent,
+ onCreateSession,
+ onDeleteSession,
+}: ModalsProps) {
+ const closeModal = useModalStore((s) => s.closeModal)
+
+ return (
+ <>
+ closeModal('settings')}
+ />
+ closeModal('newSession')}
+ />
+ closeModal('deleteSession')}
+ />
+ >
+ )
+}
+
+// Settings Modal
+interface SettingsModalProps {
+ defaultAgentId: string
+ onSetDefaultAgent: (agentId: string) => void
+ onClose: () => void
+}
+
+function SettingsModal({ defaultAgentId, onSetDefaultAgent, onClose }: SettingsModalProps) {
+ const { isOpen } = useModal('settings')
+
+ return (
+
+ )
+}
+
+// New Session Modal
+interface NewSessionModalProps {
+ onCreateSession: (cwd: string) => Promise
+ onClose: () => void
+}
+
+function NewSessionModal({ onCreateSession, onClose }: NewSessionModalProps) {
+ const { isOpen } = useModal('newSession')
+ const [cwd, setCwd] = useState('')
+
+ const handleCreate = async () => {
+ if (!cwd.trim()) return
+ await onCreateSession(cwd.trim())
+ setCwd('')
+ onClose()
+ }
+
+ const handleOpenChange = (open: boolean) => {
+ if (!open) {
+ setCwd('')
+ onClose()
+ }
+ }
+
+ return (
+
+ )
+}
+
+// Delete Session Modal
+interface DeleteSessionModalProps {
+ onDeleteSession: (sessionId: string) => void
+ onClose: () => void
+}
+
+function DeleteSessionModal({ onDeleteSession, onClose }: DeleteSessionModalProps) {
+ const { isOpen, data: session } = useModal('deleteSession')
+
+ const handleConfirm = () => {
+ if (session) {
+ onDeleteSession(session.id)
+ onClose()
+ }
+ }
+
+ const getSessionTitle = (s: MulticaSession): string => {
+ if (s.title) return s.title
+ const parts = s.workingDirectory.split('/')
+ return parts[parts.length - 1] || s.workingDirectory
+ }
+
+ return (
+
+ )
+}
diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx
index 87dd5eecc6..c4f675d9f0 100644
--- a/src/renderer/src/components/Settings.tsx
+++ b/src/renderer/src/components/Settings.tsx
@@ -1,6 +1,6 @@
/**
- * Agent setup / Settings component
- * Using shadcn/ui components
+ * Settings component - simplified agent selector
+ * Using Linear-style design: minimal UI, direct interactions
*/
import { useState, useEffect } from 'react'
import type { AgentCheckResult } from '../../../shared/electron-api'
@@ -10,13 +10,10 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
- DialogFooter,
} from '@/components/ui/dialog'
-import { Button } from '@/components/ui/button'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
-import { Card } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
-import { Sun, Moon, Monitor, Check, Loader2, RefreshCw } from 'lucide-react'
+import { Sun, Moon, Monitor, ChevronRight, ChevronDown, Loader2 } from 'lucide-react'
+import { cn } from '@/lib/utils'
interface SettingsProps {
isOpen: boolean
@@ -25,28 +22,18 @@ interface SettingsProps {
onSetDefaultAgent: (agentId: string) => void
}
-// Agent icons mapping
-const AGENT_ICONS: Record = {
- 'claude-code': '◉',
- opencode: '⌘',
- codex: '◈',
- gemini: '✦',
-}
-
type ThemeMode = 'light' | 'dark' | 'system'
export function Settings({ isOpen, onClose, defaultAgentId, onSetDefaultAgent }: SettingsProps) {
const [agents, setAgents] = useState([])
const [loading, setLoading] = useState(true)
- const [selectedAgent, setSelectedAgent] = useState(defaultAgentId)
const { mode, setMode } = useTheme()
useEffect(() => {
if (isOpen) {
- setSelectedAgent(defaultAgentId)
loadAgents()
}
- }, [isOpen, defaultAgentId])
+ }, [isOpen])
async function loadAgents() {
setLoading(true)
@@ -60,20 +47,18 @@ export function Settings({ isOpen, onClose, defaultAgentId, onSetDefaultAgent }:
}
}
- function handleDone() {
- if (selectedAgent !== defaultAgentId) {
- onSetDefaultAgent(selectedAgent)
+ // Direct selection - Linear style, no confirm button needed
+ function handleSelectAgent(agentId: string) {
+ if (agentId !== defaultAgentId) {
+ onSetDefaultAgent(agentId)
}
- onClose()
}
- const installedCount = agents.filter(a => a.installed).length
-
return (
+ {/* Separator */}
+
+
{/* Agent Section */}
- Default Agent
-
- Select the default agent for new sessions. Each session runs its own agent process.
-
+ AI Agent
{loading ? (
-
-
+
+
) : (
-
+
{agents.map((agent) => (
- agent.installed && setSelectedAgent(agent.id)}
+ isSelected={agent.id === defaultAgentId}
+ onSelect={handleSelectAgent}
/>
))}
)}
-
- {/* Footer */}
-
-
-
-
-
-
-
-
)
}
-interface AgentCardProps {
+interface AgentItemProps {
agent: AgentCheckResult
isSelected: boolean
- onSelect: () => void
+ onSelect: (agentId: string) => void
}
-function AgentCard({ agent, isSelected, onSelect }: AgentCardProps) {
- const icon = AGENT_ICONS[agent.id] || '◇'
+function AgentItem({ agent, isSelected, onSelect }: AgentItemProps) {
+ const [expanded, setExpanded] = useState(false)
+
+ const status = !agent.installed ? 'setup'
+ : isSelected ? 'selected'
+ : 'ready'
+
+ // Click row to expand/collapse only
+ const handleRowClick = () => {
+ setExpanded(!expanded)
+ }
+
+ const handleSelectClick = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ onSelect(agent.id)
+ }
return (
-
- {/* Icon */}
-
- {icon}
-
-
- {/* Content */}
-
-
- {agent.name}
- {agent.installed && (
-
-
- installed
-
+ {/* Main row - click to expand */}
+
+
+ {expanded ? (
+
+ ) : (
+
)}
-
-
- {getAgentDescription(agent.id)}
-
- {!agent.installed && agent.installHint && (
-
- {agent.installHint}
-
+
+
+ {agent.name}
+
+ {/* Right side: status or button */}
+ {status === 'selected' ? (
+ Default
+ ) : status === 'ready' ? (
+
+ ) : (
+ Setup required
)}
- {/* Selection indicator */}
-
- {isSelected && }
-
-
+ {/* Expanded content */}
+ {expanded && (
+
+ {status === 'setup' && agent.installHint ? (
+
+ To install, run in Terminal: {agent.installHint}
+
+ ) : (
+ {getAgentDescription(agent.id)}
+ )}
+
+ )}
+
)
}
function getAgentDescription(agentId: string): string {
const descriptions: Record = {
- 'claude-code': 'Anthropic\'s Claude Code via ACP',
- opencode: 'Terminal-based coding assistant',
- codex: 'OpenAI\'s Codex CLI via ACP',
- gemini: 'Google\'s Gemini CLI agent',
+ 'claude-code': 'Best for complex reasoning tasks. By Anthropic.',
+ opencode: 'Fast and lightweight. Open source.',
+ codex: 'OpenAI\'s code assistant. By OpenAI.',
+ gemini: 'Google\'s AI assistant. By Google.',
}
- return descriptions[agentId] || 'Coding assistant'
+ return descriptions[agentId] || 'AI coding assistant'
}
diff --git a/src/renderer/src/components/StatusBar.tsx b/src/renderer/src/components/StatusBar.tsx
index 97f36be369..33caf25bda 100644
--- a/src/renderer/src/components/StatusBar.tsx
+++ b/src/renderer/src/components/StatusBar.tsx
@@ -2,23 +2,19 @@
* Status bar component - shows session info and running status
*/
import type { MulticaSession } from '../../../shared/types'
-import { Button } from '@/components/ui/button'
import { SidebarTrigger, useSidebar } from '@/components/ui/sidebar'
import { cn } from '@/lib/utils'
-import { Settings } from 'lucide-react'
interface StatusBarProps {
runningSessionsCount: number
currentSession: MulticaSession | null
isCurrentSessionRunning: boolean
- onOpenSettings: () => void
}
export function StatusBar({
runningSessionsCount,
currentSession,
isCurrentSessionRunning,
- onOpenSettings,
}: StatusBarProps) {
const { state, isMobile } = useSidebar()
@@ -38,25 +34,21 @@ export function StatusBar({
{currentSession.title || currentSession.workingDirectory.split('/').pop()}
-
+
{currentSession.workingDirectory}
>
) : (
- No session selected
+ No session selected
)}
- {/* Right: Status + Settings */}
+ {/* Right: Status */}
-
-
)
@@ -78,7 +70,7 @@ function SessionStatusBadge({ isRunning, runningCount }: SessionStatusBadgeProps
return (
- {text}
+ {text}
)
}
diff --git a/src/renderer/src/components/ToolCallItem.tsx b/src/renderer/src/components/ToolCallItem.tsx
new file mode 100644
index 0000000000..e0b453a9f2
--- /dev/null
+++ b/src/renderer/src/components/ToolCallItem.tsx
@@ -0,0 +1,135 @@
+/**
+ * Tool call item component - displays tool calls with expandable details
+ */
+import { useState } from 'react'
+import { ChevronRight } from 'lucide-react'
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
+import { cn } from '@/lib/utils'
+
+export interface ToolCall {
+ id: string
+ title: string
+ status: string
+ kind?: string
+ input?: string
+ output?: string
+}
+
+// Status dot component - displays tool call status with color and animation
+function StatusDot({ status }: { status: string }) {
+ const statusStyles: Record = {
+ pending: 'bg-[var(--tool-pending)]',
+ running: 'bg-[var(--tool-running)] animate-[glow-pulse_2s_ease-in-out_infinite]',
+ in_progress: 'bg-[var(--tool-running)] animate-[glow-pulse_2s_ease-in-out_infinite]',
+ completed: 'bg-[var(--tool-success)]',
+ failed: 'bg-[var(--tool-error)]',
+ }
+
+ return (
+
+ )
+}
+
+// Tool call details - shows input and output separated by a line
+function ToolCallDetails({ toolCall }: { toolCall: ToolCall }) {
+ return (
+
+ {/* Input */}
+ {toolCall.input && (
+
+
+ {formatJson(toolCall.input)}
+
+
+ )}
+
+ {/* Separator */}
+ {toolCall.input && toolCall.output && (
+
+ )}
+
+ {/* Output */}
+ {toolCall.output && (
+
+
+ {toolCall.output}
+
+
+ )}
+
+ )
+}
+
+// Format JSON string for display
+function formatJson(input: string): string {
+ try {
+ const parsed = JSON.parse(input)
+ return JSON.stringify(parsed, null, 2)
+ } catch {
+ return input
+ }
+}
+
+// Tool call item - expandable display with input/output details
+export function ToolCallItem({ toolCall }: { toolCall: ToolCall }) {
+ const [isOpen, setIsOpen] = useState(false)
+
+ const hasDetails = toolCall.input || toolCall.output
+ const isFailed = toolCall.status === 'failed'
+ const kind = toolCall.kind || 'tool'
+
+ return (
+
+
+ {/* Status dot */}
+
+
+ {/* Kind */}
+
+ {kind}
+
+
+ {/* Title (file path etc) - truncate */}
+ {toolCall.title && (
+
+ {toolCall.title}
+
+ )}
+
+ {/* Expand indicator */}
+ {hasDetails && (
+
+ )}
+
+
+ {hasDetails && (
+
+
+
+ )}
+
+ )
+}
diff --git a/src/renderer/src/stores/modalStore.ts b/src/renderer/src/stores/modalStore.ts
new file mode 100644
index 0000000000..159bd0c0bb
--- /dev/null
+++ b/src/renderer/src/stores/modalStore.ts
@@ -0,0 +1,57 @@
+/**
+ * Global modal state management using Zustand
+ */
+import { create } from 'zustand'
+import type { MulticaSession } from '../../../shared/types'
+
+// Modal types
+export type ModalType = 'settings' | 'newSession' | 'deleteSession'
+
+// Modal data types
+interface ModalDataMap {
+ settings: undefined
+ newSession: undefined
+ deleteSession: MulticaSession
+}
+
+interface ModalState {
+ isOpen: boolean
+ data?: ModalDataMap[T]
+}
+
+interface ModalStore {
+ modals: {
+ [K in ModalType]: ModalState
+ }
+ openModal: (type: T, data?: ModalDataMap[T]) => void
+ closeModal: (type: ModalType) => void
+}
+
+export const useModalStore = create((set) => ({
+ modals: {
+ settings: { isOpen: false },
+ newSession: { isOpen: false },
+ deleteSession: { isOpen: false },
+ },
+ openModal: (type, data) =>
+ set((state) => ({
+ modals: {
+ ...state.modals,
+ [type]: { isOpen: true, data },
+ },
+ })),
+ closeModal: (type) =>
+ set((state) => ({
+ modals: {
+ ...state.modals,
+ [type]: { isOpen: false, data: undefined },
+ },
+ })),
+}))
+
+// Convenience selectors
+export const useModal = (type: T) =>
+ useModalStore((state) => state.modals[type] as ModalState)
+
+export const useOpenModal = () => useModalStore((state) => state.openModal)
+export const useCloseModal = () => useModalStore((state) => state.closeModal)
diff --git a/src/renderer/src/styles/index.css b/src/renderer/src/styles/index.css
index 45f922c799..175547256c 100644
--- a/src/renderer/src/styles/index.css
+++ b/src/renderer/src/styles/index.css
@@ -3,45 +3,12 @@
@custom-variant dark (&:is(.dark *));
-/* Dark theme (default) */
-:root,
-.dark {
- --color-primary: #292929;
- --color-primary-dark: #363636;
- --color-primary-text: #ffffff;
- --color-accent: #22c55e;
- --color-accent-muted: rgba(34, 197, 94, 0.15);
- --color-background: #0f0f0f;
- --color-surface: #1a1a1a;
- --color-surface-hover: #252525;
- --color-border: #2e2e2e;
- --color-text: #ffffff;
- --color-text-muted: #a1a1aa;
-}
-
-/* Light theme */
-.light {
- --color-primary: #f5f5f5;
- --color-primary-dark: #e5e5e5;
- --color-primary-text: #171717;
- --color-accent: #16a34a;
- --color-accent-muted: rgba(22, 163, 74, 0.1);
- --color-background: #ffffff;
- --color-surface: #f5f5f5;
- --color-surface-hover: #e5e5e5;
- --color-border: #e5e5e5;
- --color-text: #171717;
- --color-text-muted: #525252;
-}
-
/* Base styles */
* {
box-sizing: border-box;
}
body {
- background-color: var(--color-background);
- color: var(--color-text);
-webkit-font-smoothing: antialiased;
font-family:
-apple-system,
@@ -73,12 +40,12 @@ body {
}
::-webkit-scrollbar-thumb {
- background-color: var(--color-border);
+ background-color: var(--border);
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
- background-color: var(--color-text-muted);
+ background-color: var(--muted-foreground);
}
@theme inline {
@@ -155,6 +122,12 @@ body {
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
+
+ /* Tool call status colors */
+ --tool-pending: oklch(0.55 0.05 250);
+ --tool-running: oklch(0.6 0.18 250);
+ --tool-success: oklch(0.72 0.12 145);
+ --tool-error: oklch(0.65 0.2 25);
}
.dark {
@@ -189,6 +162,12 @@ body {
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
+
+ /* Tool call status colors */
+ --tool-pending: oklch(0.5 0.08 250);
+ --tool-running: oklch(0.65 0.2 250);
+ --tool-success: oklch(0.65 0.15 145);
+ --tool-error: oklch(0.7 0.2 22);
}
@layer base {
@@ -199,3 +178,13 @@ body {
@apply bg-background text-foreground;
}
}
+
+/* Tool call status glow animation */
+@keyframes glow-pulse {
+ 0%, 100% {
+ box-shadow: 0 0 0 0 var(--tool-running);
+ }
+ 50% {
+ box-shadow: 0 0 0 3px oklch(0.6 0.2 250 / 0);
+ }
+}