From 6595322f1a6ebf17c46a7372d297d6656e9a79d7 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 3 May 2026 17:19:55 +0300 Subject: [PATCH] fix: show tablet actions without hover Makes hover-only controls visible on tablets Preserves existing mobile and desktop layouts Covers chat, session sidebar, file, tab, and picker actions --- .../ui/src/components/chat/ChatMessage.tsx | 18 +++++++---- .../ui/src/components/chat/FileAttachment.tsx | 8 ++++- .../components/chat/MarkdownRendererImpl.tsx | 17 +++++++--- .../ui/src/components/chat/ModelControls.tsx | 5 +-- .../ui/src/components/chat/TimelineDialog.tsx | 8 +++-- .../components/chat/message/MessageBody.tsx | 11 +++++-- .../chat/message/parts/ReasoningPart.tsx | 9 ++++-- .../chat/message/parts/ToolPart.tsx | 7 ++-- .../openchamber/WorktreeSectionContent.tsx | 5 +-- .../src/components/session/DirectoryTree.tsx | 10 ++++-- .../session/GitHubIssuePickerDialog.tsx | 8 ++++- .../session/GitHubPrPickerDialog.tsx | 8 ++++- .../components/session/SessionFolderItem.tsx | 6 ++-- .../src/components/session/SessionSidebar.tsx | 8 +++++ .../session/sidebar/SessionGroupSection.tsx | 32 +++++++++++++------ .../session/sidebar/SessionNodeItem.tsx | 21 ++++++------ .../session/sidebar/SidebarProjectsList.tsx | 2 ++ .../session/sidebar/sortableItems.tsx | 27 ++++++++++------ .../src/components/ui/sortable-tabs-strip.tsx | 11 ++++--- .../ui/src/components/views/FilesView.tsx | 11 ++++--- 20 files changed, 164 insertions(+), 68 deletions(-) diff --git a/packages/ui/src/components/chat/ChatMessage.tsx b/packages/ui/src/components/chat/ChatMessage.tsx index 4283febd..5f222faf 100644 --- a/packages/ui/src/components/chat/ChatMessage.tsx +++ b/packages/ui/src/components/chat/ChatMessage.tsx @@ -149,7 +149,8 @@ const ChatMessage: React.FC = ({ animateUserOnMount = false, onUserAnimationConsumed, }) => { - const { isMobile, hasTouchInput } = useDeviceInfo(); + const { isMobile, isTablet, hasTouchInput } = useDeviceInfo(); + const alwaysShowMessageActions = isMobile || isTablet; const { currentTheme } = useThemeSystem(); const messageContainerRef = React.useRef(null); @@ -1030,8 +1031,9 @@ const ChatMessage: React.FC = ({ isMessageCompleted={isMessageCompleted} messageFinish={messageFinish} syntaxTheme={syntaxTheme} - isMobile={isMobile} - hasTouchInput={hasTouchInput} + isMobile={isMobile} + alwaysShowActions={alwaysShowMessageActions} + hasTouchInput={hasTouchInput} copiedCode={copiedCode} onCopyCode={handleCopyCode} expandedTools={expandedTools} @@ -1063,8 +1065,9 @@ const ChatMessage: React.FC = ({ isMessageCompleted={isMessageCompleted} messageFinish={messageFinish} syntaxTheme={syntaxTheme} - isMobile={isMobile} - hasTouchInput={hasTouchInput} + isMobile={isMobile} + alwaysShowActions={alwaysShowMessageActions} + hasTouchInput={hasTouchInput} copiedCode={copiedCode} onCopyCode={handleCopyCode} expandedTools={expandedTools} @@ -1115,8 +1118,9 @@ const ChatMessage: React.FC = ({ messageCompletedAt={messageCompletedAt ?? undefined} messageCreatedAt={messageCreatedAt ?? undefined} syntaxTheme={syntaxTheme} - isMobile={isMobile} - hasTouchInput={hasTouchInput} + isMobile={isMobile} + alwaysShowActions={alwaysShowMessageActions} + hasTouchInput={hasTouchInput} copiedCode={copiedCode} onCopyCode={handleCopyCode} expandedTools={effectiveExpandedTools} diff --git a/packages/ui/src/components/chat/FileAttachment.tsx b/packages/ui/src/components/chat/FileAttachment.tsx index c6f91450..9d7f38c9 100644 --- a/packages/ui/src/components/chat/FileAttachment.tsx +++ b/packages/ui/src/components/chat/FileAttachment.tsx @@ -10,6 +10,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; import { useI18n } from '@/lib/i18n'; +import { useDeviceInfo } from '@/lib/device'; import type { ToolPopupContent } from './message/types'; @@ -127,6 +128,8 @@ interface ImagePreviewProps { const ImagePreview = memo(({ file, onRemove }: ImagePreviewProps) => { const { t } = useI18n(); + const { isMobile, isTablet } = useDeviceInfo(); + const alwaysShowActions = isMobile || isTablet; const isLocalImagePreview = file.source !== 'server' && file.mimeType.startsWith('image/') && @@ -184,7 +187,10 @@ const ImagePreview = memo(({ file, onRemove }: ImagePreviewProps) => { /> {sessionMenuContent} diff --git a/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx b/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx index 3b51b18a..955a9646 100644 --- a/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx @@ -47,6 +47,7 @@ type Props = { isDesktopShellRuntime: boolean; stuckProjectHeaders: Set; mobileVariant: boolean; + alwaysShowActions: boolean; toggleProject: (id: string) => void; setActiveProjectIdOnly: (id: string) => void; setActiveMainTab: (tab: 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files') => void; @@ -168,6 +169,7 @@ export function SidebarProjectsList(props: Props): React.ReactNode { isStuck={props.stuckProjectHeaders.has(projectKey)} hideDirectoryControls={props.hideDirectoryControls} mobileVariant={props.mobileVariant} + alwaysShowActions={props.alwaysShowActions} onToggle={() => props.toggleProject(projectKey)} onNewSession={() => { if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey); diff --git a/packages/ui/src/components/session/sidebar/sortableItems.tsx b/packages/ui/src/components/session/sidebar/sortableItems.tsx index 33aea845..45f0f5bd 100644 --- a/packages/ui/src/components/session/sidebar/sortableItems.tsx +++ b/packages/ui/src/components/session/sidebar/sortableItems.tsx @@ -38,6 +38,7 @@ export interface SortableProjectItemProps { isStuck: boolean; hideDirectoryControls: boolean; mobileVariant: boolean; + alwaysShowActions: boolean; onToggle: () => void; onNewSession: () => void; onNewWorktreeSession?: () => void; @@ -70,7 +71,7 @@ export const SortableProjectItem: React.FC = ({ isDesktopShell, isStuck, hideDirectoryControls, - mobileVariant, + alwaysShowActions, onToggle, onNewSession, onNewWorktreeSession, @@ -176,17 +177,23 @@ export const SortableProjectItem: React.FC = ({ className={cn( 'flex-1 min-w-0 flex items-center gap-1.5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 rounded-md cursor-grab active:cursor-grabbing transition-[padding]', isRepo && !hideDirectoryControls - ? (mobileVariant ? 'pr-20' : 'pr-7 group-hover/project:pr-20 group-focus-within/project:pr-20') - : (mobileVariant ? 'pr-14' : 'pr-7 group-hover/project:pr-14 group-focus-within/project:pr-14'), + ? (alwaysShowActions ? 'pr-20' : 'pr-7 group-hover/project:pr-20 group-focus-within/project:pr-20') + : (alwaysShowActions ? 'pr-14' : 'pr-7 group-hover/project:pr-14 group-focus-within/project:pr-14'), )} > - + {isCollapsed ? : } {imageUrl ? ( = ({ /> ) : ProjectIcon ? ( - + ) : ( - + )} = ({ }} className={cn( 'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground transition-opacity', - mobileVariant ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto', + alwaysShowActions ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto', )} aria-label={t('sessions.sidebar.project.actions.newWorktree')} > @@ -255,7 +262,7 @@ export const SortableProjectItem: React.FC = ({ 'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground', isMenuOpen ? 'opacity-100 pointer-events-auto' - : mobileVariant + : alwaysShowActions ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto', )} @@ -301,7 +308,7 @@ export const SortableProjectItem: React.FC = ({ }} className={cn( 'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-opacity', - mobileVariant ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto', + alwaysShowActions ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto', )} aria-label={isRepo ? t('sessions.sidebar.project.actions.newDraftSession') diff --git a/packages/ui/src/components/ui/sortable-tabs-strip.tsx b/packages/ui/src/components/ui/sortable-tabs-strip.tsx index cdee1d1f..6fd71ebf 100644 --- a/packages/ui/src/components/ui/sortable-tabs-strip.tsx +++ b/packages/ui/src/components/ui/sortable-tabs-strip.tsx @@ -20,6 +20,7 @@ import { RiCloseLine } from '@remixicon/react'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/useUIStore'; +import { useDeviceInfo } from '@/lib/device'; export type SortableTabsStripItem = { id: string; @@ -99,6 +100,8 @@ export const SortableTabsStrip: React.FC = ({ }) => { const { t } = useI18n(); const isMobile = useUIStore((state) => state.isMobile); + const { isTablet } = useDeviceInfo(); + const alwaysShowCloseControls = isMobile || isTablet; const scrollRef = React.useRef(null); const [overflow, setOverflow] = React.useState<{ left: boolean; right: boolean }>({ left: false, right: false }); const itemIDs = React.useMemo(() => items.map((item) => item.id), [items]); @@ -435,12 +438,12 @@ export const SortableTabsStrip: React.FC = ({ <> {item.icon ? ( - {item.icon} + {item.icon} {closeReplacesIcon ? ( { event.stopPropagation(); }} @@ -467,12 +470,12 @@ export const SortableTabsStrip: React.FC = ({ isActive ? 'text-[var(--primary-base)]' : 'text-muted-foreground' )} > - {item.icon} + {item.icon} {closeReplacesIcon ? ( { event.stopPropagation(); }} diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 475dcabd..6678cfc4 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -295,6 +295,7 @@ interface FileRowProps { isExpanded: boolean; isActive: boolean; isMobile: boolean; + alwaysShowActions: boolean; status?: FileStatus | null; badge?: { modified: number; added: number } | null; permissions: { @@ -319,6 +320,7 @@ const FileRow: React.FC = ({ isExpanded, isActive, isMobile, + alwaysShowActions, status, badge, permissions, @@ -395,8 +397,7 @@ const FileRow: React.FC = ({ {(canRename || canCreateFile || canCreateFolder || canDelete || canReveal) && (
= ({ mode = 'full' }) => { const { t } = useI18n(); const { files, runtime } = useRuntimeAPIs(); const { currentTheme, availableThemes, lightThemeId, darkThemeId } = useThemeSystem(); - const { isMobile, screenWidth } = useDeviceInfo(); + const { isMobile, isTablet, screenWidth } = useDeviceInfo(); + const alwaysShowActions = isMobile || isTablet; const showHidden = useDirectoryShowHidden(); const showGitignored = useFilesViewShowGitignored(); @@ -1815,6 +1817,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { isExpanded={isExpanded} isActive={isActive} isMobile={isMobile} + alwaysShowActions={alwaysShowActions} status={!isDir ? getFileStatus(node.path) : undefined} badge={isDir ? getFolderBadge(node.path) : undefined} permissions={fileRowPermissions} @@ -2886,7 +2889,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }} className={cn( 'rounded-sm p-0.5 text-[var(--surface-muted-foreground)] hover:text-[var(--surface-foreground)]', - !isActive && 'opacity-0 group-hover:opacity-100' + !isActive && !alwaysShowActions && 'opacity-0 group-hover:opacity-100' )} aria-label={t('filesView.editor.closeFileAria', { name: file.name })} >