diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx index 5a3f7479..4a1780dc 100644 --- a/desktop/src/__tests__/pages.test.tsx +++ b/desktop/src/__tests__/pages.test.tsx @@ -38,7 +38,7 @@ describe('Content-only pages render without errors', () => { it('ActiveSession renders with chat components', () => { const SESSION_ID = 'test-active-session' - useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', status: 'idle' }], activeTabId: SESSION_ID }) + useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID }) useChatStore.setState({ sessions: { [SESSION_ID]: { @@ -71,7 +71,7 @@ describe('Content-only pages render without errors', () => { }) it('ActiveSession shows a single primary action button while a turn is active', () => { - useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', status: 'idle' }] }) + useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] }) useChatStore.setState({ sessions: { 'active-tab': { diff --git a/desktop/src/components/chat/ChatInput.tsx b/desktop/src/components/chat/ChatInput.tsx index e89447c7..cf64ccf5 100644 --- a/desktop/src/components/chat/ChatInput.tsx +++ b/desktop/src/components/chat/ChatInput.tsx @@ -9,6 +9,7 @@ import { ModelSelector } from '../controls/ModelSelector' import type { AttachmentRef } from '../../types/chat' import { AttachmentGallery } from './AttachmentGallery' import { ProjectContextChip } from '../shared/ProjectContextChip' +import { DirectoryPicker } from '../shared/DirectoryPicker' import { FileSearchMenu, type FileSearchMenuHandle } from './FileSearchMenu' import { FALLBACK_SLASH_COMMANDS, @@ -49,9 +50,10 @@ export function ChatInput() { const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined) const chatState = sessionState?.chatState ?? 'idle' const slashCommands = sessionState?.slashCommands ?? [] - const activeSessionId = useSessionStore((state) => state.activeSessionId) - const activeSession = useSessionStore((state) => state.sessions.find((session) => session.id === state.activeSessionId) ?? null) + const activeSession = useSessionStore((state) => activeTabId ? state.sessions.find((session) => session.id === activeTabId) ?? null : null) const [gitInfo, setGitInfo] = useState(null) + const messages = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.messages ?? [] : []) + const hasMessages = messages.length > 0 const isActive = chatState !== 'idle' const isWorkspaceMissing = activeSession?.workDirExists === false @@ -62,12 +64,12 @@ export function ChatInput() { }, [isActive]) useEffect(() => { - if (!activeSessionId) { + if (!activeTabId) { setGitInfo(null) return } - sessionsApi.getGitInfo(activeSessionId).then(setGitInfo).catch(() => setGitInfo(null)) - }, [activeSessionId]) + sessionsApi.getGitInfo(activeTabId).then(setGitInfo).catch(() => setGitInfo(null)) + }, [activeTabId]) useEffect(() => { const el = textareaRef.current @@ -518,11 +520,30 @@ export function ChatInput() {
- + {hasMessages ? ( + + ) : ( + { + if (!activeTabId) return + // Recreate session with new workDir + const { deleteSession, createSession } = useSessionStore.getState() + const { closeTab, openTab } = useTabStore.getState() + const { disconnectSession, connectToSession } = useChatStore.getState() + disconnectSession(activeTabId) + closeTab(activeTabId) + await deleteSession(activeTabId) + const newId = await createSession(newWorkDir) + openTab(newId, t('sidebar.newSession')) + connectToSession(newId) + }} + /> + )}
diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 064b9d29..5041b722 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -30,7 +30,7 @@ function makeSessionState(overrides: Partial = {}): PerSessionS describe('MessageList nested tool calls', () => { beforeEach(() => { - useTabStore.setState({ activeTabId: ACTIVE_TAB, tabs: [{ sessionId: ACTIVE_TAB, title: 'Test', status: 'idle' }] }) + useTabStore.setState({ activeTabId: ACTIVE_TAB, tabs: [{ sessionId: ACTIVE_TAB, title: 'Test', type: 'session' as const, status: 'idle' }] }) useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState() } }) }) diff --git a/desktop/src/components/chat/chatBlocks.test.tsx b/desktop/src/components/chat/chatBlocks.test.tsx index 1b5d13fa..efbc4251 100644 --- a/desktop/src/components/chat/chatBlocks.test.tsx +++ b/desktop/src/components/chat/chatBlocks.test.tsx @@ -8,7 +8,7 @@ import { useTabStore } from '../../stores/tabStore' describe('chat blocks', () => { beforeEach(() => { - useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', status: 'idle' }] }) + useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] }) useChatStore.setState({ sessions: {} }) }) diff --git a/desktop/src/components/layout/AppShell.tsx b/desktop/src/components/layout/AppShell.tsx index beebfccc..642806be 100644 --- a/desktop/src/components/layout/AppShell.tsx +++ b/desktop/src/components/layout/AppShell.tsx @@ -6,6 +6,7 @@ import { useSettingsStore } from '../../stores/settingsStore' import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts' import { initializeDesktopServerUrl } from '../../lib/desktopRuntime' import { TabBar } from './TabBar' +import { StatusBar } from './StatusBar' import { useTabStore } from '../../stores/tabStore' import { useChatStore } from '../../stores/chatStore' @@ -103,6 +104,7 @@ export function AppShell() {
+
diff --git a/desktop/src/components/layout/ContentRouter.tsx b/desktop/src/components/layout/ContentRouter.tsx index b72be1ef..41f4387a 100644 --- a/desktop/src/components/layout/ContentRouter.tsx +++ b/desktop/src/components/layout/ContentRouter.tsx @@ -1,4 +1,3 @@ -import { useUIStore } from '../../stores/uiStore' import { useTabStore } from '../../stores/tabStore' import { useTeamStore } from '../../stores/teamStore' import { EmptySession } from '../../pages/EmptySession' @@ -8,62 +7,28 @@ import { Settings } from '../../pages/Settings' import { AgentTranscript } from '../../pages/AgentTranscript' export function ContentRouter() { - const activeView = useUIStore((s) => s.activeView) const activeTabId = useTabStore((s) => s.activeTabId) + const activeTab = useTabStore((s) => s.tabs.find((t) => t.sessionId === s.activeTabId)) const viewingAgentId = useTeamStore((s) => s.viewingAgentId) - if (activeView === 'settings') { - return - } - - if (activeView === 'scheduled') { - return - } - - if (activeView === 'terminal') { - return - } - - if (activeView === 'history') { - // If viewing an agent transcript, show that - if (viewingAgentId) { - return - } - return - } - - // Code view - if (!activeTabId) { + // No tabs open โ€” show empty session + if (!activeTabId || !activeTab) { return } + // Special tabs + if (activeTab.type === 'settings') { + return + } + + if (activeTab.type === 'scheduled') { + return + } + + // Session tab โ€” show agent transcript or active session if (viewingAgentId) { return } return } - -function TerminalPlaceholder() { - return ( -
- terminal -

Terminal

-

- Integrated terminal coming soon. Use the Code tab to interact with Claude. -

-
- ) -} - -function HistoryPlaceholder() { - return ( -
- history -

History

-

- View session history and agent transcripts. Select a session from the sidebar to view its history. -

-
- ) -} diff --git a/desktop/src/components/layout/Sidebar.tsx b/desktop/src/components/layout/Sidebar.tsx index e7e4ff30..ba356f5a 100644 --- a/desktop/src/components/layout/Sidebar.tsx +++ b/desktop/src/components/layout/Sidebar.tsx @@ -1,10 +1,9 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react' import { useSessionStore } from '../../stores/sessionStore' -import { useUIStore } from '../../stores/uiStore' import { useTranslation } from '../../i18n' import { ProjectFilter } from './ProjectFilter' import type { SessionListItem } from '../../types/session' -import { useTabStore } from '../../stores/tabStore' +import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID } from '../../stores/tabStore' import { useChatStore } from '../../stores/chatStore' const isTauri = typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window) @@ -16,15 +15,13 @@ const TIME_GROUP_ORDER: TimeGroup[] = ['today', 'yesterday', 'last7days', 'last3 export function Sidebar() { const { sessions, - activeSessionId, selectedProjects, error, - setActiveSession, fetchSessions, deleteSession, renameSession, } = useSessionStore() - const { activeView, setActiveView } = useUIStore() + const activeTabId = useTabStore((s) => s.activeTabId) const [searchQuery, setSearchQuery] = useState('') const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null) const [renamingId, setRenamingId] = useState(null) @@ -132,15 +129,29 @@ export function Sidebar() { {/* Navigation */}
{ setActiveView('code'); setActiveSession(null) }} + active={false} + onClick={async () => { + try { + // Use current active session's workDir as default for new session + const currentTabId = useTabStore.getState().activeTabId + const currentSession = currentTabId + ? useSessionStore.getState().sessions.find((s) => s.id === currentTabId) + : null + const workDir = currentSession?.workDir || undefined + const sessionId = await useSessionStore.getState().createSession(workDir) + useTabStore.getState().openTab(sessionId, t('sidebar.newSession')) + useChatStore.getState().connectToSession(sessionId) + } catch { + // Session creation failed โ€” no tab opened + } + }} icon={} > {t('sidebar.newSession')} setActiveView('scheduled')} + active={activeTabId === SCHEDULED_TAB_ID} + onClick={() => useTabStore.getState().openTab(SCHEDULED_TAB_ID, t('sidebar.scheduled'), 'scheduled')} icon={} > {t('sidebar.scheduled')} @@ -208,22 +219,21 @@ export function Sidebar() { ) : ( -

{t('settings.title')}

-
-
{/* Tab navigation */}
diff --git a/desktop/src/stores/tabStore.ts b/desktop/src/stores/tabStore.ts index 3c531e6d..970e1aa9 100644 --- a/desktop/src/stores/tabStore.ts +++ b/desktop/src/stores/tabStore.ts @@ -3,14 +3,20 @@ import { sessionsApi } from '../api/sessions' const TAB_STORAGE_KEY = 'cc-haha-open-tabs' +export const SETTINGS_TAB_ID = '__settings__' +export const SCHEDULED_TAB_ID = '__scheduled__' + +export type TabType = 'session' | 'settings' | 'scheduled' + export type Tab = { sessionId: string title: string + type: TabType status: 'idle' | 'running' | 'error' } type TabPersistence = { - openTabs: Array<{ sessionId: string; title: string }> + openTabs: Array<{ sessionId: string; title: string; type?: TabType }> activeTabId: string | null } @@ -18,7 +24,7 @@ type TabStore = { tabs: Tab[] activeTabId: string | null - openTab: (sessionId: string, title: string) => void + openTab: (sessionId: string, title: string, type?: TabType) => void closeTab: (sessionId: string) => void setActiveTab: (sessionId: string) => void updateTabTitle: (sessionId: string, title: string) => void @@ -32,14 +38,14 @@ export const useTabStore = create((set, get) => ({ tabs: [], activeTabId: null, - openTab: (sessionId, title) => { + openTab: (sessionId, title, type = 'session') => { const { tabs } = get() const existing = tabs.find((t) => t.sessionId === sessionId) if (existing) { set({ activeTabId: sessionId }) } else { set({ - tabs: [...tabs, { sessionId, title, status: 'idle' }], + tabs: [...tabs, { sessionId, title, type, status: 'idle' }], activeTabId: sessionId, }) } @@ -89,7 +95,7 @@ export const useTabStore = create((set, get) => ({ saveTabs: () => { const { tabs, activeTabId } = get() const data: TabPersistence = { - openTabs: tabs.map((t) => ({ sessionId: t.sessionId, title: t.title })), + openTabs: tabs.map((t) => ({ sessionId: t.sessionId, title: t.title, type: t.type })), activeTabId, } try { @@ -109,12 +115,23 @@ export const useTabStore = create((set, get) => ({ const existingIds = new Set(sessions.map((s) => s.id)) const validTabs: Tab[] = data.openTabs - .filter((t) => existingIds.has(t.sessionId)) - .map((t) => ({ - sessionId: t.sessionId, - title: sessions.find((s) => s.id === t.sessionId)?.title || t.title, - status: 'idle' as const, - })) + .filter((t) => { + // Special tabs are always valid + if (t.type === 'settings' || t.type === 'scheduled') return true + // Session tabs must exist on server + return existingIds.has(t.sessionId) + }) + .map((t) => { + if (t.type === 'settings' || t.type === 'scheduled') { + return { sessionId: t.sessionId, title: t.title, type: t.type, status: 'idle' as const } + } + return { + sessionId: t.sessionId, + title: sessions.find((s) => s.id === t.sessionId)?.title || t.title, + type: 'session' as const, + status: 'idle' as const, + } + }) if (validTabs.length === 0) return