From 7699aab123efd25e3dd4aa28462297443dd87bc0 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 21 Dec 2025 20:18:51 +0200 Subject: [PATCH] feat(chat): delay session creation until first message The app now opens to a new chat by default on startup New chats no longer create a session until you send your first message Fixed mobile and VSCode sessions handling for better consistency --- CHANGELOG.md | 4 + packages/ui/src/App.tsx | 5 +- .../ui/src/components/chat/ChatContainer.tsx | 24 +++- packages/ui/src/components/chat/ChatInput.tsx | 25 ++-- .../ui/src/components/layout/VSCodeLayout.tsx | 81 +++--------- .../src/components/session/SessionSidebar.tsx | 108 +++++++++------- .../ui/src/components/ui/CommandPalette.tsx | 12 +- packages/ui/src/hooks/useKeyboardShortcuts.ts | 16 +-- packages/ui/src/hooks/useMenuActions.ts | 18 ++- packages/ui/src/stores/sessionStore.ts | 2 +- packages/ui/src/stores/types/sessionTypes.ts | 13 ++ packages/ui/src/stores/useSessionStore.ts | 117 +++++++++++++++++- 12 files changed, 273 insertions(+), 152 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ec2bea..ecf4ea88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- New chats no longer create a session until you send your first message. +- The app opens to a new chat by default. +- Fixed mobile and VSCode sessions handling + ## [1.3.0] - 2025-12-21 diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 4e77adcb..c0497a16 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -42,7 +42,7 @@ type AppProps = { }; function App({ apis }: AppProps) { - const { initializeApp, loadProviders, isInitialized } = useConfigStore(); + const { initializeApp, isInitialized } = useConfigStore(); const { error, clearError, loadSessions } = useSessionStore(); const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory); @@ -120,11 +120,10 @@ function App({ apis }: AppProps) { React.useEffect(() => { const init = async () => { await initializeApp(); - await loadProviders(); }; init(); - }, [initializeApp, loadProviders]); + }, [initializeApp]); React.useEffect(() => { if (isSwitchingDirectory) { diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index 41c983b7..6cfcfe70 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -27,6 +27,7 @@ export const ChatContainer: React.FC = () => { messageStreamStates, trimToViewportWindow, sessionActivityPhase, + newSessionDraft, } = useSessionStore(); const streamingMessageId = React.useMemo(() => { @@ -35,6 +36,7 @@ export const ChatContainer: React.FC = () => { }, [currentSessionId, streamingMessageIds]); const { isMobile } = useDeviceInfo(); + const draftOpen = Boolean(newSessionDraft?.open); const sessionMessages = React.useMemo(() => { @@ -143,7 +145,7 @@ export const ChatContainer: React.FC = () => { void load(); }, [currentSessionId, loadMessages, messages, scrollToBottom]); - if (!currentSessionId) { + if (!currentSessionId && !draftOpen) { return (
{ ); } + if (!currentSessionId && draftOpen) { + return ( +
+
+ +
+
+ +
+
+ ); + } + + if (!currentSessionId) { + return null; + } + if (isLoading && sessionMessages.length === 0 && !streamingMessageId) { const hasMessagesEntry = messages.has(currentSessionId); if (!hasMessagesEntry) { diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 27b24a6f..3e875947 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -61,6 +61,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const sendMessage = useSessionStore((state) => state.sendMessage); const currentSessionId = useSessionStore((state) => state.currentSessionId); + const newSessionDraftOpen = useSessionStore((state) => state.newSessionDraft?.open); const abortCurrentOperation = useSessionStore((state) => state.abortCurrentOperation); const acknowledgeSessionAbort = useSessionStore((state) => state.acknowledgeSessionAbort); const abortPromptSessionId = useSessionStore((state) => state.abortPromptSessionId); @@ -229,7 +230,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); - if (!hasContent || !currentSessionId) return; + if (!hasContent || (!currentSessionId && !newSessionDraftOpen)) return; const messageToSend = message.replace(/^\n+|\n+$/g, ''); @@ -542,7 +543,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo return; } - if (!currentSessionId) { + if (!currentSessionId && !newSessionDraftOpen) { return; } @@ -572,7 +573,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (attachedCount > 0) { toast.success(`Attached ${attachedCount} image${attachedCount > 1 ? 's' : ''} from clipboard`); } - }, [addAttachedFile, currentSessionId, insertTextAtSelection]); + }, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection]); const handleFileSelect = (file: { name: string; path: string }) => { @@ -660,7 +661,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - if (currentSessionId && !isDragging) { + if ((currentSessionId || newSessionDraftOpen) && !isDragging) { setIsDragging(true); } }; @@ -678,7 +679,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo e.stopPropagation(); setIsDragging(false); - if (!currentSessionId) return; + if (!currentSessionId && !newSessionDraftOpen) return; const files = Array.from(e.dataTransfer.files); let attachedCount = 0; @@ -838,13 +839,13 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo ) : ( + ) : null} + {isExpanded && totalSessions > maxVisible ? ( + + ) : null} + + ); + })()}
) : ( groupedSessions.map((group) => ( @@ -1054,19 +1082,15 @@ export const SessionSidebar: React.FC = ({ { - if (isCreatingSession) return; e.stopPropagation(); handleCreateSessionInGroup(group.directory); }} onKeyDown={(e) => { - if (isCreatingSession) return; if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); handleCreateSessionInGroup(group.directory); @@ -1084,7 +1108,7 @@ export const SessionSidebar: React.FC = ({
{(() => { const isExpanded = expandedSessionGroups.has(group.id); - const maxVisible = 7; + const maxVisible = hideDirectoryControls ? 10 : 7; const totalSessions = group.sessions.length; const visibleSessions = isExpanded ? group.sessions : group.sessions.slice(0, maxVisible); const remainingCount = totalSessions - visibleSessions.length; diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx index 80960db6..525455ad 100644 --- a/packages/ui/src/components/ui/CommandPalette.tsx +++ b/packages/ui/src/components/ui/CommandPalette.tsx @@ -12,7 +12,6 @@ import { import { useUIStore } from '@/stores/useUIStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; -import { useConfigStore } from '@/stores/useConfigStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useDeviceInfo } from '@/lib/device'; import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react'; @@ -31,14 +30,12 @@ export const CommandPalette: React.FC = () => { } = useUIStore(); const { - createSession, + openNewSessionDraft, setCurrentSession, getSessionsByDirectory, - initializeNewOpenChamberSession, } = useSessionStore(); const { currentDirectory } = useDirectoryStore(); - const { agents } = useConfigStore(); const { themeMode, setThemeMode } = useThemeSystem(); const handleClose = () => { @@ -46,10 +43,9 @@ export const CommandPalette: React.FC = () => { }; const handleCreateSession = async () => { - const session = await createSession(); - if (session) { - initializeNewOpenChamberSession(session.id, agents); - } + setActiveMainTab('chat'); + setSessionSwitcherOpen(false); + openNewSessionDraft(); handleClose(); }; diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index 7aed46b8..e0c0ae8a 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -1,13 +1,12 @@ import React from 'react'; import { useSessionStore } from '@/stores/useSessionStore'; import { useUIStore } from '@/stores/useUIStore'; -import { useConfigStore } from '@/stores/useConfigStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useAssistantStatus } from '@/hooks/useAssistantStatus'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; export const useKeyboardShortcuts = () => { - const { createSession, abortCurrentOperation, initializeNewOpenChamberSession, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore(); + const { openNewSessionDraft, abortCurrentOperation, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore(); const { toggleCommandPalette, toggleHelpDialog, @@ -17,7 +16,6 @@ export const useKeyboardShortcuts = () => { setActiveMainTab, setSettingsDialogOpen, } = useUIStore(); - const { agents } = useConfigStore(); const { themeMode, setThemeMode } = useThemeSystem(); const { working } = useAssistantStatus(); const abortPrimedUntilRef = React.useRef(null); @@ -86,11 +84,9 @@ export const useKeyboardShortcuts = () => { return; } - createSession().then(session => { - if (session) { - initializeNewOpenChamberSession(session.id, agents); - } - }); + setActiveMainTab('chat'); + setSessionSwitcherOpen(false); + openNewSessionDraft(); } if ((e.metaKey || e.ctrlKey) && e.key === '/') { @@ -189,7 +185,7 @@ export const useKeyboardShortcuts = () => { window.removeEventListener('keydown', handleKeyDown); }; }, [ - createSession, + openNewSessionDraft, abortCurrentOperation, toggleCommandPalette, toggleHelpDialog, @@ -200,8 +196,6 @@ export const useKeyboardShortcuts = () => { setSettingsDialogOpen, setThemeMode, themeMode, - initializeNewOpenChamberSession, - agents, working, armAbortPrompt, resetAbortPriming, diff --git a/packages/ui/src/hooks/useMenuActions.ts b/packages/ui/src/hooks/useMenuActions.ts index 8213f028..e40d72b0 100644 --- a/packages/ui/src/hooks/useMenuActions.ts +++ b/packages/ui/src/hooks/useMenuActions.ts @@ -2,7 +2,6 @@ import React from 'react'; import { toast } from 'sonner'; import { useSessionStore } from '@/stores/useSessionStore'; import { useUIStore } from '@/stores/useUIStore'; -import { useConfigStore } from '@/stores/useConfigStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; @@ -32,17 +31,17 @@ type MenuAction = export const useMenuActions = ( onToggleMemoryDebug?: () => void ) => { - const { createSession, initializeNewOpenChamberSession } = useSessionStore(); + const { openNewSessionDraft } = useSessionStore(); const { toggleCommandPalette, toggleHelpDialog, toggleSidebar, + setSessionSwitcherOpen, setSessionCreateDialogOpen, setActiveMainTab, setSettingsDialogOpen, setAboutDialogOpen, } = useUIStore(); - const { agents } = useConfigStore(); const { setDirectory } = useDirectoryStore(); const { setThemeMode } = useThemeSystem(); const isDownloadingLogsRef = React.useRef(false); @@ -87,11 +86,9 @@ export const useMenuActions = ( break; case 'new-session': - createSession().then(session => { - if (session) { - initializeNewOpenChamberSession(session.id, agents); - } - }); + setActiveMainTab('chat'); + setSessionSwitcherOpen(false); + openNewSessionDraft(); break; case 'worktree-creator': @@ -183,17 +180,16 @@ export const useMenuActions = ( window.addEventListener(MENU_ACTION_EVENT, handleMenuAction); return () => window.removeEventListener(MENU_ACTION_EVENT, handleMenuAction); }, [ - createSession, - initializeNewOpenChamberSession, + openNewSessionDraft, toggleCommandPalette, toggleHelpDialog, toggleSidebar, + setSessionSwitcherOpen, setSessionCreateDialogOpen, setActiveMainTab, setSettingsDialogOpen, setAboutDialogOpen, setThemeMode, - agents, onToggleMemoryDebug, handleChangeWorkspace, ]); diff --git a/packages/ui/src/stores/sessionStore.ts b/packages/ui/src/stores/sessionStore.ts index a849b060..fe84bb6a 100644 --- a/packages/ui/src/stores/sessionStore.ts +++ b/packages/ui/src/stores/sessionStore.ts @@ -721,7 +721,7 @@ export const useSessionStore = create()( set((state) => { const filteredSessions = state.sessions.filter((session) => !deletedSet.has(session.id)); if (state.currentSessionId && deletedSet.has(state.currentSessionId)) { - nextCurrentId = filteredSessions.length > 0 ? filteredSessions[0].id : null; + nextCurrentId = null; } else { nextCurrentId = state.currentSessionId; } diff --git a/packages/ui/src/stores/types/sessionTypes.ts b/packages/ui/src/stores/types/sessionTypes.ts index a7295519..ce9b56f1 100644 --- a/packages/ui/src/stores/types/sessionTypes.ts +++ b/packages/ui/src/stores/types/sessionTypes.ts @@ -56,6 +56,13 @@ export const MEMORY_LIMITS = { export const ACTIVE_SESSION_WINDOW = 180; +export type NewSessionDraftState = { + open: boolean; + directoryOverride: string | null; + parentID: string | null; + title?: string; +}; + export interface SessionStore { sessions: Session[]; @@ -98,10 +105,16 @@ export interface SessionStore { pendingInputText: string | null; + newSessionDraft: NewSessionDraftState; + getSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => EditPermissionMode; toggleSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => void; setSessionAgentEditMode: (sessionId: string, agentName: string | undefined, mode: EditPermissionMode, defaultMode?: EditPermissionMode) => void; loadSessions: () => Promise; + + openNewSessionDraft: (options?: { directoryOverride?: string | null; parentID?: string | null; title?: string }) => void; + closeNewSessionDraft: () => void; + createSession: (title?: string, directoryOverride?: string | null, parentID?: string | null) => Promise; createSessionFromAssistantMessage: (sourceMessageId: string) => Promise; diff --git a/packages/ui/src/stores/useSessionStore.ts b/packages/ui/src/stores/useSessionStore.ts index d5bc77fd..7dad6306 100644 --- a/packages/ui/src/stores/useSessionStore.ts +++ b/packages/ui/src/stores/useSessionStore.ts @@ -95,6 +95,7 @@ export const useSessionStore = create()( sessionActivityPhase: new Map(), userSummaryTitles: new Map(), pendingInputText: null, + newSessionDraft: { open: true, directoryOverride: null, parentID: null }, getSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => { return useContextStore.getState().getSessionAgentEditMode(sessionId, agentName, defaultMode); @@ -109,7 +110,46 @@ export const useSessionStore = create()( }, loadSessions: () => useSessionManagementStore.getState().loadSessions(), + + openNewSessionDraft: (options) => { + set({ + newSessionDraft: { + open: true, + directoryOverride: options?.directoryOverride ?? null, + parentID: options?.parentID ?? null, + title: options?.title, + }, + currentSessionId: null, + error: null, + }); + + try { + const configState = useConfigStore.getState(); + const visibleAgents = configState.getVisibleAgents(); + const agentName = + configState.currentAgentName || + visibleAgents.find((agent) => agent.name === 'build')?.name || + visibleAgents[0]?.name; + + if (agentName) { + configState.setAgent(agentName); + } + } catch { + // ignored + } + }, + + closeNewSessionDraft: () => { + const realCurrentSessionId = useSessionManagementStore.getState().currentSessionId; + set({ + newSessionDraft: { open: false, directoryOverride: null, parentID: null, title: undefined }, + currentSessionId: realCurrentSessionId, + }); + }, + createSession: async (title?: string, directoryOverride?: string | null, parentID?: string | null) => { + get().closeNewSessionDraft(); + const result = await useSessionManagementStore.getState().createSession(title, directoryOverride, parentID); if (result?.id) { @@ -179,7 +219,11 @@ export const useSessionStore = create()( shareSession: (id: string) => useSessionManagementStore.getState().shareSession(id), unshareSession: (id: string) => useSessionManagementStore.getState().unshareSession(id), setCurrentSession: async (id: string | null) => { - const previousSessionId = get().currentSessionId; + if (id) { + get().closeNewSessionDraft(); + } + + const previousSessionId = useSessionManagementStore.getState().currentSessionId; const sessionDirectory = resolveSessionDirectory( useSessionManagementStore.getState().sessions, @@ -224,7 +268,64 @@ export const useSessionStore = create()( get().evictLeastRecentlyUsed(); }, loadMessages: (sessionId: string) => useMessageStore.getState().loadMessages(sessionId), - sendMessage: (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string) => { + sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string) => { + const draft = get().newSessionDraft; + + if (draft?.open) { + const created = await useSessionManagementStore + .getState() + .createSession(draft.title, draft.directoryOverride ?? null, draft.parentID ?? null); + + if (!created?.id) { + throw new Error('Failed to create session'); + } + + const configState = useConfigStore.getState(); + const draftAgentName = configState.currentAgentName; + const draftProviderId = configState.currentProviderId; + const draftModelId = configState.currentModelId; + + if (draftProviderId && draftModelId) { + try { + useContextStore.getState().saveSessionModelSelection(created.id, draftProviderId, draftModelId); + } catch { + // ignored + } + } + + if (draftAgentName) { + try { + useContextStore.getState().saveSessionAgentSelection(created.id, draftAgentName); + } catch { + // ignored + } + + if (draftProviderId && draftModelId) { + try { + useContextStore + .getState() + .saveAgentModelForSession(created.id, draftAgentName, draftProviderId, draftModelId); + } catch { + // ignored + } + } + } + + try { + useSessionManagementStore + .getState() + .initializeNewOpenChamberSession(created.id, configState.agents); + } catch { + // ignored + } + + get().closeNewSessionDraft(); + + return useMessageStore + .getState() + .sendMessage(content, providerID, modelID, agent, created.id, attachments, agentMentionName); + } + const currentSessionId = useSessionManagementStore.getState().currentSessionId; return useMessageStore.getState().sendMessage(content, providerID, modelID, agent, currentSessionId || undefined, attachments, agentMentionName); }, @@ -309,6 +410,10 @@ export const useSessionStore = create()( setSessionDirectory: (sessionId: string, directory: string | null) => useSessionManagementStore.getState().setSessionDirectory(sessionId, directory), getWorktreeMetadata: (sessionId: string) => useSessionManagementStore.getState().getWorktreeMetadata(sessionId), getContextUsage: (contextLimit: number, outputLimit: number) => { + if (get().newSessionDraft?.open) { + return null; + } + const currentSessionId = useSessionManagementStore.getState().currentSessionId; if (!currentSessionId) return null; const messages = useMessageStore.getState().messages; @@ -420,9 +525,11 @@ useSessionManagementStore.subscribe((state, prevState) => { return; } + const draftOpen = useSessionStore.getState().newSessionDraft?.open; + useSessionStore.setState({ sessions: state.sessions, - currentSessionId: state.currentSessionId, + currentSessionId: draftOpen ? null : state.currentSessionId, lastLoadedDirectory: state.lastLoadedDirectory, isLoading: state.isLoading, error: state.error, @@ -535,9 +642,11 @@ usePermissionStore.subscribe((state, prevState) => { }); }); +const bootDraftOpen = useSessionStore.getState().newSessionDraft?.open; + useSessionStore.setState({ sessions: useSessionManagementStore.getState().sessions, - currentSessionId: useSessionManagementStore.getState().currentSessionId, + currentSessionId: bootDraftOpen ? null : useSessionManagementStore.getState().currentSessionId, lastLoadedDirectory: useSessionManagementStore.getState().lastLoadedDirectory, isLoading: useSessionManagementStore.getState().isLoading, error: useSessionManagementStore.getState().error,