event.preventDefault()}
onDrop={handleDrop}
>
diff --git a/desktop/src/components/layout/AppShell.test.tsx b/desktop/src/components/layout/AppShell.test.tsx
index ba252a8d..223cb14c 100644
--- a/desktop/src/components/layout/AppShell.test.tsx
+++ b/desktop/src/components/layout/AppShell.test.tsx
@@ -2,6 +2,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useUIStore } from '../../stores/uiStore'
+import { useSessionStore } from '../../stores/sessionStore'
const mocks = vi.hoisted(() => ({
initializeDesktopServerUrl: vi.fn(),
@@ -120,6 +121,7 @@ describe('AppShell boot flow', () => {
})
mocks.tabState.activeTabId = null
mocks.tabState.tabs = []
+ useSessionStore.setState({ sessions: [], activeSessionId: null, isLoading: false, error: null })
useUIStore.setState({ sidebarOpen: true })
})
@@ -271,6 +273,39 @@ describe('AppShell boot flow', () => {
expect(screen.getByTestId('sidebar-shell')).toHaveAttribute('data-state', 'closed')
})
+ it('shares the mobile drawer row with the active session title', async () => {
+ mocks.isMobile = true
+ mocks.tabState.activeTabId = 'session-mobile'
+ mocks.tabState.tabs = [
+ { sessionId: 'session-mobile', title: 'Fallback tab title', type: 'session', status: 'running' },
+ ]
+ useSessionStore.setState({
+ sessions: [{
+ id: 'session-mobile',
+ title: 'Analyze recent commits',
+ createdAt: '2026-05-10T00:00:00.000Z',
+ modifiedAt: new Date().toISOString(),
+ messageCount: 7,
+ projectPath: '/tmp/project',
+ workDir: '/tmp/project',
+ workDirExists: true,
+ }],
+ activeSessionId: 'session-mobile',
+ isLoading: false,
+ error: null,
+ })
+
+ render(
)
+
+ await screen.findByText('content loaded')
+
+ const header = screen.getByTestId('mobile-session-header')
+ expect(header).toHaveTextContent('Analyze recent commits')
+ expect(header).toHaveTextContent('session.active')
+ expect(header).toHaveTextContent('session.messages')
+ expect(screen.getByTestId('mobile-sidebar-toggle')).toHaveClass('h-10', 'w-10')
+ })
+
it('keeps browser H5 mobile on chat tabs when settings was restored as active', async () => {
mocks.isMobile = true
mocks.tabState.activeTabId = '__settings__'
diff --git a/desktop/src/components/layout/AppShell.tsx b/desktop/src/components/layout/AppShell.tsx
index 94007261..c9e578ab 100644
--- a/desktop/src/components/layout/AppShell.tsx
+++ b/desktop/src/components/layout/AppShell.tsx
@@ -16,6 +16,7 @@ import { TabBar } from './TabBar'
import { StartupErrorView } from './StartupErrorView'
import { useTabStore, SETTINGS_TAB_ID } from '../../stores/tabStore'
import { useChatStore } from '../../stores/chatStore'
+import { useSessionStore } from '../../stores/sessionStore'
import { useTranslation } from '../../i18n'
import { H5ConnectionView } from './H5ConnectionView'
import { useMobileViewport } from '../../hooks/useMobileViewport'
@@ -41,8 +42,22 @@ export function AppShell() {
const tabs = useTabStore((s) => s.tabs)
const activeTabId = useTabStore((s) => s.activeTabId)
const setActiveTab = useTabStore((s) => s.setActiveTab)
+ const activeSession = useSessionStore((s) =>
+ activeTabId ? s.sessions.find((session) => session.id === activeTabId) ?? null : null,
+ )
const wasMobileShellRef = useRef(false)
const effectiveSidebarOpen = isMobileShell ? mobileSidebarOpen : sidebarOpen
+ const activeTab = tabs.find((tab) => tab.sessionId === activeTabId)
+ const isActiveChatTab = isChatTab(activeTab)
+ const mobileSessionTitle = activeSession?.title || activeTab?.title || t('session.untitled')
+ const mobileSessionUpdated = (() => {
+ if (!activeSession?.modifiedAt) return ''
+ const diff = Date.now() - new Date(activeSession.modifiedAt).getTime()
+ if (diff < 60000) return t('session.timeJustNow')
+ if (diff < 3600000) return t('session.timeMinutes', { n: Math.floor(diff / 60000) })
+ if (diff < 86400000) return t('session.timeHours', { n: Math.floor(diff / 3600000) })
+ return t('session.timeDays', { n: Math.floor(diff / 86400000) })
+ })()
const sidebarHiddenProps: HTMLAttributes
& { inert?: '' } =
isMobileShell && !effectiveSidebarOpen
? { 'aria-hidden': true, inert: '' }
@@ -130,7 +145,6 @@ export function AppShell() {
useEffect(() => {
if (!ready || !isMobileShell) return
- const activeTab = tabs.find((tab) => tab.sessionId === activeTabId)
if (isChatTab(activeTab) || (!activeTab && !activeTabId)) return
const nextChatTab = tabs.find(isChatTab)
if (nextChatTab) {
@@ -138,7 +152,7 @@ export function AppShell() {
return
}
useTabStore.setState({ activeTabId: null })
- }, [activeTabId, isMobileShell, ready, setActiveTab, tabs])
+ }, [activeTab, activeTabId, isMobileShell, ready, setActiveTab, tabs])
const setEffectiveSidebarOpen = (open: boolean) => {
if (isMobileShell) {
@@ -208,7 +222,10 @@ export function AppShell() {
className={`min-w-0 flex-1 flex flex-col overflow-hidden${isMobileShell ? ' app-shell-main--mobile' : ''}`}
>
{isMobileShell ? (
-
+
+ {isActiveChatTab ? (
+
+
+ {mobileSessionTitle}
+
+
+ {activeTab?.status === 'running' ? (
+
+
+ {t('session.active')}
+
+ ) : null}
+ {activeSession?.messageCount !== undefined && activeSession.messageCount > 0 ? (
+ <>
+ {activeTab?.status === 'running' ? · : null}
+ {t('session.messages', { count: activeSession.messageCount })}
+ >
+ ) : null}
+ {mobileSessionUpdated ? (
+ <>
+ {(activeTab?.status === 'running') || ((activeSession?.messageCount ?? 0) > 0) ? · : null}
+ {t('session.lastUpdated', { time: mobileSessionUpdated })}
+ >
+ ) : null}
+
+
+ ) : null}
) : null}
{!isMobileShell ?
: null}
diff --git a/desktop/src/pages/ActiveSession.test.tsx b/desktop/src/pages/ActiveSession.test.tsx
index 24f92d1e..5eeda9a2 100644
--- a/desktop/src/pages/ActiveSession.test.tsx
+++ b/desktop/src/pages/ActiveSession.test.tsx
@@ -509,6 +509,7 @@ describe('ActiveSession task polling', () => {
expect(screen.getByTestId('active-session-chat-column')).toHaveClass('min-w-0')
expect(screen.getByTestId('message-list')).toHaveAttribute('data-compact', 'false')
expect(screen.getByTestId('chat-input')).toHaveAttribute('data-compact', 'false')
+ expect(screen.queryByRole('heading', { name: 'Mobile Session' })).not.toBeInTheDocument()
expect(screen.queryByTestId('workspace-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('workspace-resize-handle')).not.toBeInTheDocument()
expect(screen.queryByTestId('session-terminal-panel')).not.toBeInTheDocument()
diff --git a/desktop/src/pages/ActiveSession.tsx b/desktop/src/pages/ActiveSession.tsx
index baf2537b..60198917 100644
--- a/desktop/src/pages/ActiveSession.tsx
+++ b/desktop/src/pages/ActiveSession.tsx
@@ -358,12 +358,12 @@ export function ActiveSession() {
) : (
<>
- {!isMemberSession && (
+ {!isMemberSession && !isMobileLayout && (