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:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user