fix(chat): fix session return button on mobile. add double-click to switch to chat and auto-focus draft (#384)

This commit is contained in:
gsxdsm
2026-02-11 20:08:15 +02:00
committed by GitHub
parent 3afe0bb45d
commit eaf19cc62f
5 changed files with 90 additions and 47 deletions
@@ -224,6 +224,23 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
} }
}, [currentSessionId, persistChatDraft, message]); }, [currentSessionId, persistChatDraft, message]);
// Focus textarea when new session draft is opened
const prevNewSessionDraftOpenRef = React.useRef(newSessionDraftOpen);
React.useEffect(() => {
if (!prevNewSessionDraftOpenRef.current && newSessionDraftOpen) {
// New session draft just opened - focus the textarea
requestAnimationFrame(() => {
if (isMobile) {
// On mobile, use preventScroll to avoid viewport jumping
textareaRef.current?.focus({ preventScroll: true });
} else {
textareaRef.current?.focus();
}
});
}
prevNewSessionDraftOpenRef.current = newSessionDraftOpen;
}, [newSessionDraftOpen, isMobile]);
// Persist chat input draft to localStorage (only if setting enabled) // Persist chat input draft to localStorage (only if setting enabled)
React.useEffect(() => { React.useEffect(() => {
if (!persistChatDraft) { if (!persistChatDraft) {
@@ -183,6 +183,7 @@ function SessionItem({
getSessionAgentName, getSessionAgentName,
getSessionTitle, getSessionTitle,
onClick, onClick,
onDoubleClick,
needsAttention needsAttention
}: { }: {
session: SessionWithStatus; session: SessionWithStatus;
@@ -190,6 +191,7 @@ function SessionItem({
getSessionAgentName: (s: Session) => string; getSessionAgentName: (s: Session) => string;
getSessionTitle: (s: Session) => string; getSessionTitle: (s: Session) => string;
onClick: () => void; onClick: () => void;
onDoubleClick?: () => void;
needsAttention: (sessionId: string) => boolean; needsAttention: (sessionId: string) => boolean;
}) { }) {
const agentName = getSessionAgentName(session); const agentName = getSessionAgentName(session);
@@ -200,6 +202,10 @@ function SessionItem({
<button <button
type="button" type="button"
onClick={onClick} onClick={onClick}
onDoubleClick={(e) => {
e.stopPropagation();
onDoubleClick?.();
}}
className={cn( className={cn(
"flex items-center gap-0.5 px-1.5 py-px text-left transition-colors", "flex items-center gap-0.5 px-1.5 py-px text-left transition-colors",
"hover:bg-[var(--interactive-hover)] active:bg-[var(--interactive-selection)]", "hover:bg-[var(--interactive-hover)] active:bg-[var(--interactive-selection)]",
@@ -339,6 +345,7 @@ function ExpandedView({
onToggleExpand, onToggleExpand,
onNewSession, onNewSession,
onSessionClick, onSessionClick,
onSessionDoubleClick,
getSessionAgentName, getSessionAgentName,
getSessionTitle, getSessionTitle,
needsAttention needsAttention
@@ -353,6 +360,7 @@ function ExpandedView({
onToggleExpand: () => void; onToggleExpand: () => void;
onNewSession: () => void; onNewSession: () => void;
onSessionClick: (id: string) => void; onSessionClick: (id: string) => void;
onSessionDoubleClick?: () => void;
getSessionAgentName: (s: Session) => string; getSessionAgentName: (s: Session) => string;
getSessionTitle: (s: Session) => string; getSessionTitle: (s: Session) => string;
needsAttention: (sessionId: string) => boolean; needsAttention: (sessionId: string) => boolean;
@@ -419,6 +427,7 @@ function ExpandedView({
getSessionAgentName={getSessionAgentName} getSessionAgentName={getSessionAgentName}
getSessionTitle={getSessionTitle} getSessionTitle={getSessionTitle}
onClick={() => onSessionClick(session.id)} onClick={() => onSessionClick(session.id)}
onDoubleClick={onSessionDoubleClick}
needsAttention={needsAttention} needsAttention={needsAttention}
/> />
))} ))}
@@ -438,6 +447,7 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
const createSession = useSessionStore((state) => state.createSession); const createSession = useSessionStore((state) => state.createSession);
const agents = useConfigStore((state) => state.agents); const agents = useConfigStore((state) => state.agents);
const { isMobile, isMobileSessionStatusBarCollapsed, setIsMobileSessionStatusBarCollapsed } = useUIStore(); const { isMobile, isMobileSessionStatusBarCollapsed, setIsMobileSessionStatusBarCollapsed } = useUIStore();
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
const [isExpanded, setIsExpanded] = React.useState(false); const [isExpanded, setIsExpanded] = React.useState(false);
const { sessions: sortedSessions, totalRunning, totalUnread, totalCount } = useSessionGrouping(sessions, sessionStatus, sessionAttentionStates); const { sessions: sortedSessions, totalRunning, totalUnread, totalCount } = useSessionGrouping(sessions, sessionStatus, sessionAttentionStates);
@@ -456,6 +466,11 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
setIsExpanded(false); setIsExpanded(false);
}; };
const handleSessionDoubleClick = () => {
// On double-tap, switch to the Chat tab
setActiveMainTab('chat');
};
const handleCreateSession = async () => { const handleCreateSession = async () => {
const newSession = await createSession(); const newSession = await createSession();
if (newSession) { if (newSession) {
@@ -491,6 +506,7 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
onToggleExpand={() => setIsExpanded(!isExpanded)} onToggleExpand={() => setIsExpanded(!isExpanded)}
onNewSession={handleCreateSession} onNewSession={handleCreateSession}
onSessionClick={handleSessionClick} onSessionClick={handleSessionClick}
onSessionDoubleClick={handleSessionDoubleClick}
getSessionAgentName={getSessionAgentName} getSessionAgentName={getSessionAgentName}
getSessionTitle={getSessionTitle} getSessionTitle={getSessionTitle}
needsAttention={needsAttention} needsAttention={needsAttention}
@@ -430,22 +430,19 @@ export const MainLayout: React.FC = () => {
style={{ paddingTop: 'var(--oc-header-height, 56px)' }} style={{ paddingTop: 'var(--oc-header-height, 56px)' }}
> >
{/* Mobile drill-down: show sessions sidebar OR main content */} {/* Mobile drill-down: show sessions sidebar OR main content */}
{isSessionSwitcherOpen ? ( <div className={cn('flex-1 overflow-hidden bg-sidebar', !isSessionSwitcherOpen && 'hidden')}>
<div className="flex-1 overflow-hidden bg-sidebar"> <ErrorBoundary><SessionSidebar mobileVariant /></ErrorBoundary>
<ErrorBoundary><SessionSidebar mobileVariant /></ErrorBoundary> </div>
<main className={cn('flex-1 overflow-hidden bg-background relative', isSessionSwitcherOpen && 'hidden')}>
<div className={cn('absolute inset-0', !isChatActive && 'invisible')}>
<ErrorBoundary><ChatView /></ErrorBoundary>
</div> </div>
) : ( {secondaryView && (
<main className="flex-1 overflow-hidden bg-background relative"> <div className="absolute inset-0">
<div className={cn('absolute inset-0', !isChatActive && 'invisible')}> <ErrorBoundary>{secondaryView}</ErrorBoundary>
<ErrorBoundary><ChatView /></ErrorBoundary>
</div> </div>
{secondaryView && ( )}
<div className="absolute inset-0"> </main>
<ErrorBoundary>{secondaryView}</ErrorBoundary>
</div>
)}
</main>
)}
</div> </div>
{/* Mobile multi-run launcher: full screen */} {/* Mobile multi-run launcher: full screen */}
@@ -6,6 +6,7 @@ import { useSessionStore } from '@/stores/useSessionStore';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
import { McpDropdown } from '@/components/mcp/McpDropdown'; import { McpDropdown } from '@/components/mcp/McpDropdown';
import { cn } from '@/lib/utils';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -377,41 +378,43 @@ export const VSCodeLayout: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
) : currentView === 'sessions' ? (
// Compact layout: sessions list (drill-down)
<div className="flex flex-col h-full">
<VSCodeHeader
title="Sessions"
/>
<div className="flex-1 overflow-hidden">
<SessionSidebar
mobileVariant
allowReselect
onSessionSelected={() => setCurrentView('chat')}
hideDirectoryControls
showOnlyMainWorkspace
/>
</div>
</div>
) : ( ) : (
// Compact layout: chat view (drill-down) // Compact layout: drill-down between sessions list and chat
<div className="flex flex-col h-full"> <>
<VSCodeHeader {/* Sessions list view */}
title={newSessionDraftOpen && !currentSessionId <div className={cn('flex flex-col h-full', currentView !== 'sessions' && 'hidden')}>
? 'New session' <VSCodeHeader
: sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'} title="Sessions"
showBack />
onBack={handleBackToSessions} <div className="flex-1 overflow-hidden">
showMcp <SessionSidebar
showContextUsage mobileVariant
showRateLimits allowReselect
/> onSessionSelected={() => setCurrentView('chat')}
<div className="flex-1 overflow-hidden"> hideDirectoryControls
<ErrorBoundary> showOnlyMainWorkspace
<ChatView /> />
</ErrorBoundary> </div>
</div> </div>
</div> {/* Chat view */}
<div className={cn('flex flex-col h-full', currentView !== 'chat' && 'hidden')}>
<VSCodeHeader
title={newSessionDraftOpen && !currentSessionId
? 'New session'
: sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'}
showBack
onBack={handleBackToSessions}
showMcp
showContextUsage
showRateLimits
/>
<div className="flex-1 overflow-hidden">
<ErrorBoundary>
<ChatView />
</ErrorBoundary>
</div>
</div>
</>
)} )}
</div> </div>
); );
@@ -908,6 +908,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
], ],
); );
const handleSessionDoubleClick = React.useCallback(() => {
// On double-click/tap, switch to the Chat tab
setActiveMainTab('chat');
}, [setActiveMainTab]);
const handleSaveEdit = React.useCallback(async () => { const handleSaveEdit = React.useCallback(async () => {
if (editingId && editTitle.trim()) { if (editingId && editTitle.trim()) {
await updateSessionTitle(editingId, editTitle.trim()); await updateSessionTitle(editingId, editTitle.trim());
@@ -1788,6 +1793,10 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
type="button" type="button"
disabled={isMissingDirectory} disabled={isMissingDirectory}
onClick={() => handleSessionSelect(session.id, sessionDirectory, isMissingDirectory, projectId)} onClick={() => handleSessionSelect(session.id, sessionDirectory, isMissingDirectory, projectId)}
onDoubleClick={(e) => {
e.stopPropagation();
handleSessionDoubleClick();
}}
className={cn( className={cn(
'flex min-w-0 flex-1 flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none', 'flex min-w-0 flex-1 flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none',
)} )}
@@ -1981,6 +1990,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
handleCancelEdit, handleCancelEdit,
toggleParent, toggleParent,
handleSessionSelect, handleSessionSelect,
handleSessionDoubleClick,
handleShareSession, handleShareSession,
handleCopyShareUrl, handleCopyShareUrl,
handleUnshareSession, handleUnshareSession,