diff --git a/desktop/src/components/layout/TabBar.test.tsx b/desktop/src/components/layout/TabBar.test.tsx
index edbe4ed8..1239d821 100644
--- a/desktop/src/components/layout/TabBar.test.tsx
+++ b/desktop/src/components/layout/TabBar.test.tsx
@@ -6,6 +6,9 @@ const startDraggingMock = vi.hoisted(() => vi.fn(() => Promise.resolve()))
const getCurrentWindowMock = vi.hoisted(() => vi.fn(() => ({
startDragging: startDraggingMock,
})))
+const windowControlsMock = vi.hoisted(() => ({
+ show: true,
+}))
vi.mock('@tauri-apps/api/window', () => ({
getCurrentWindow: getCurrentWindowMock,
@@ -34,8 +37,10 @@ vi.mock('../../i18n', () => ({
}))
vi.mock('./WindowControls', () => ({
- WindowControls: () =>
({
+ createSession: vi.fn(),
+ listSessions: vi.fn(),
+ getMessages: vi.fn(),
+ getSlashCommands: vi.fn(),
+ listSkills: vi.fn(),
+ getTasksForList: vi.fn(),
+ resetTaskList: vi.fn(),
+ wsClearHandlers: vi.fn(),
+ wsConnect: vi.fn(),
+ wsOnMessage: vi.fn(),
+ wsSend: vi.fn(),
+ wsDisconnect: vi.fn(),
+}))
+
+vi.mock('../api/sessions', () => ({
+ sessionsApi: {
+ create: mocks.createSession,
+ list: mocks.listSessions,
+ getMessages: mocks.getMessages,
+ getSlashCommands: mocks.getSlashCommands,
+ },
+}))
+
+vi.mock('../api/skills', () => ({
+ skillsApi: {
+ list: mocks.listSkills,
+ },
+}))
+
+vi.mock('../api/cliTasks', () => ({
+ cliTasksApi: {
+ getTasksForList: mocks.getTasksForList,
+ resetTaskList: mocks.resetTaskList,
+ },
+}))
+
+vi.mock('../api/websocket', () => ({
+ wsManager: {
+ clearHandlers: mocks.wsClearHandlers,
+ connect: mocks.wsConnect,
+ onMessage: mocks.wsOnMessage,
+ send: mocks.wsSend,
+ disconnect: mocks.wsDisconnect,
+ },
+}))
+
+vi.mock('../components/shared/DirectoryPicker', () => ({
+ DirectoryPicker: ({ value, onChange }: { value: string; onChange: (path: string) => void }) => (
+
+ ),
+}))
+
+vi.mock('../components/controls/PermissionModeSelector', () => ({
+ PermissionModeSelector: () =>
,
+}))
+
+vi.mock('../components/controls/ModelSelector', () => ({
+ ModelSelector: () =>
,
+}))
+
+import { EmptySession } from './EmptySession'
+import { useChatStore } from '../stores/chatStore'
+import { useSessionRuntimeStore } from '../stores/sessionRuntimeStore'
+import { useSessionStore } from '../stores/sessionStore'
+import { useSettingsStore } from '../stores/settingsStore'
+import { useTabStore } from '../stores/tabStore'
+import { useUIStore } from '../stores/uiStore'
+
+describe('EmptySession', () => {
+ const initialSessionState = useSessionStore.getInitialState()
+ const initialChatState = useChatStore.getInitialState()
+ const initialTabState = useTabStore.getInitialState()
+ const initialRuntimeState = useSessionRuntimeStore.getInitialState()
+ const initialUiState = useUIStore.getInitialState()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ useSettingsStore.setState({ locale: 'en', activeProviderName: null })
+ useSessionStore.setState(initialSessionState, true)
+ useChatStore.setState(initialChatState, true)
+ useTabStore.setState(initialTabState, true)
+ useSessionRuntimeStore.setState(initialRuntimeState, true)
+ useUIStore.setState(initialUiState, true)
+
+ mocks.createSession.mockResolvedValue({ sessionId: 'draft-session' })
+ mocks.listSessions.mockResolvedValue({
+ sessions: [{
+ id: 'draft-session',
+ title: 'New Session',
+ createdAt: '2026-05-01T00:00:00.000Z',
+ modifiedAt: '2026-05-01T00:00:00.000Z',
+ messageCount: 0,
+ projectPath: '/workspace/project',
+ workDir: '/workspace/project',
+ workDirExists: true,
+ }],
+ total: 1,
+ })
+ mocks.getMessages.mockResolvedValue({ messages: [] })
+ mocks.getSlashCommands.mockResolvedValue({ commands: [] })
+ mocks.listSkills.mockResolvedValue({ skills: [] })
+ mocks.getTasksForList.mockResolvedValue({ tasks: [] })
+ mocks.resetTaskList.mockResolvedValue(undefined)
+ })
+
+ afterEach(() => {
+ useSessionStore.setState(initialSessionState, true)
+ useChatStore.setState(initialChatState, true)
+ useTabStore.setState(initialTabState, true)
+ useSessionRuntimeStore.setState(initialRuntimeState, true)
+ useUIStore.setState(initialUiState, true)
+ })
+
+ it('creates an empty session as soon as a project is selected', async () => {
+ render(
)
+
+ fireEvent.change(screen.getByRole('textbox'), {
+ target: { value: 'draft question', selectionStart: 14 },
+ })
+ fireEvent.click(screen.getByRole('button', { name: 'Pick project' }))
+
+ await waitFor(() => {
+ expect(mocks.createSession).toHaveBeenCalledWith('/workspace/project')
+ })
+
+ expect(useTabStore.getState().activeTabId).toBe('draft-session')
+ expect(useTabStore.getState().tabs).toEqual([
+ { sessionId: 'draft-session', title: 'New Session', type: 'session', status: 'idle' },
+ ])
+ expect(useSessionStore.getState().sessions[0]).toMatchObject({
+ id: 'draft-session',
+ workDir: '/workspace/project',
+ })
+ expect(useChatStore.getState().sessions['draft-session']?.composerPrefill?.text).toBe('draft question')
+ expect(mocks.wsConnect).toHaveBeenCalledWith('draft-session')
+ })
+})
diff --git a/desktop/src/pages/EmptySession.tsx b/desktop/src/pages/EmptySession.tsx
index 34c0f065..696e26e1 100644
--- a/desktop/src/pages/EmptySession.tsx
+++ b/desktop/src/pages/EmptySession.tsx
@@ -23,7 +23,7 @@ import {
replaceSlashCommand,
resolveSlashUiAction,
} from '../components/chat/composerUtils'
-import type { AttachmentRef } from '../types/chat'
+import type { AttachmentRef, UIAttachment } from '../types/chat'
import type { SlashCommandOption } from '../components/chat/composerUtils'
type Attachment = {
@@ -39,6 +39,7 @@ export function EmptySession() {
const t = useTranslation()
const [input, setInput] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isCreatingSession, setIsCreatingSession] = useState(false)
const [workDir, setWorkDir] = useState('')
const [attachments, setAttachments] = useState
([])
const [plusMenuOpen, setPlusMenuOpen] = useState(false)
@@ -157,6 +158,73 @@ export function EmptySession() {
[slashCommands],
)
+ const resolveDraftRuntimeSelection = async () => {
+ const settings = useSettingsStore.getState()
+ let providerState = useProviderStore.getState()
+ if (
+ settings.activeProviderName &&
+ providerState.providers.length === 0 &&
+ !providerState.isLoading
+ ) {
+ await providerState.fetchProviders()
+ providerState = useProviderStore.getState()
+ }
+ const inferredProviderId = providerState.activeId ?? (
+ settings.activeProviderName
+ ? providerState.providers.find((provider) => provider.name === settings.activeProviderName)?.id ?? null
+ : null
+ )
+ return (
+ useSessionRuntimeStore.getState().selections[DRAFT_RUNTIME_SELECTION_KEY]
+ ?? {
+ providerId: inferredProviderId,
+ modelId: settings.currentModel?.id ?? OFFICIAL_DEFAULT_MODEL_ID,
+ }
+ )
+ }
+
+ const openDraftSessionForWorkDir = async (newWorkDir: string) => {
+ setWorkDir(newWorkDir)
+ if (!newWorkDir || isCreatingSession || isSubmitting) return
+
+ setIsCreatingSession(true)
+ try {
+ const draftSelection = await resolveDraftRuntimeSelection()
+ const sessionId = await createSession(newWorkDir)
+ useSessionRuntimeStore.getState().setSelection(sessionId, draftSelection)
+ useSessionRuntimeStore.getState().clearSelection(DRAFT_RUNTIME_SELECTION_KEY)
+ setActiveView('code')
+ useTabStore.getState().openTab(sessionId, 'New Session')
+ connectToSession(sessionId)
+
+ const draftAttachments: UIAttachment[] = attachments.map((attachment) => ({
+ type: attachment.type,
+ name: attachment.name,
+ data: attachment.data,
+ mimeType: attachment.mimeType,
+ }))
+ if (input.trim() || draftAttachments.length > 0) {
+ useChatStore.getState().queueComposerPrefill(sessionId, {
+ text: input,
+ attachments: draftAttachments,
+ })
+ }
+ setInput('')
+ setAttachments([])
+ setFileSearchOpen(false)
+ setSlashMenuOpen(false)
+ setPlusMenuOpen(false)
+ setLocalSlashPanel(null)
+ } catch (error) {
+ addToast({
+ type: 'error',
+ message: error instanceof Error ? error.message : t('empty.failedToCreate'),
+ })
+ } finally {
+ setIsCreatingSession(false)
+ }
+ }
+
const filteredCommands = useMemo(() => {
const source = allSlashCommands
if (!slashFilter) return source
@@ -210,27 +278,7 @@ export function EmptySession() {
setIsSubmitting(true)
try {
- const settings = useSettingsStore.getState()
- let providerState = useProviderStore.getState()
- if (
- settings.activeProviderName &&
- providerState.providers.length === 0 &&
- !providerState.isLoading
- ) {
- await providerState.fetchProviders()
- providerState = useProviderStore.getState()
- }
- const inferredProviderId = providerState.activeId ?? (
- settings.activeProviderName
- ? providerState.providers.find((provider) => provider.name === settings.activeProviderName)?.id ?? null
- : null
- )
- const draftSelection =
- useSessionRuntimeStore.getState().selections[DRAFT_RUNTIME_SELECTION_KEY]
- ?? {
- providerId: inferredProviderId,
- modelId: settings.currentModel?.id ?? OFFICIAL_DEFAULT_MODEL_ID,
- }
+ const draftSelection = await resolveDraftRuntimeSelection()
const sessionId = await createSession(workDir || undefined)
useSessionRuntimeStore.getState().setSelection(sessionId, draftSelection)
useSessionRuntimeStore.getState().clearSelection(DRAFT_RUNTIME_SELECTION_KEY)
@@ -590,10 +638,10 @@ export function EmptySession() {