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
This commit is contained in:
@@ -149,7 +149,8 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
animateUserOnMount = false,
|
animateUserOnMount = false,
|
||||||
onUserAnimationConsumed,
|
onUserAnimationConsumed,
|
||||||
}) => {
|
}) => {
|
||||||
const { isMobile, hasTouchInput } = useDeviceInfo();
|
const { isMobile, isTablet, hasTouchInput } = useDeviceInfo();
|
||||||
|
const alwaysShowMessageActions = isMobile || isTablet;
|
||||||
const { currentTheme } = useThemeSystem();
|
const { currentTheme } = useThemeSystem();
|
||||||
const messageContainerRef = React.useRef<HTMLDivElement | null>(null);
|
const messageContainerRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -1030,8 +1031,9 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
isMessageCompleted={isMessageCompleted}
|
isMessageCompleted={isMessageCompleted}
|
||||||
messageFinish={messageFinish}
|
messageFinish={messageFinish}
|
||||||
syntaxTheme={syntaxTheme}
|
syntaxTheme={syntaxTheme}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
hasTouchInput={hasTouchInput}
|
alwaysShowActions={alwaysShowMessageActions}
|
||||||
|
hasTouchInput={hasTouchInput}
|
||||||
copiedCode={copiedCode}
|
copiedCode={copiedCode}
|
||||||
onCopyCode={handleCopyCode}
|
onCopyCode={handleCopyCode}
|
||||||
expandedTools={expandedTools}
|
expandedTools={expandedTools}
|
||||||
@@ -1063,8 +1065,9 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
isMessageCompleted={isMessageCompleted}
|
isMessageCompleted={isMessageCompleted}
|
||||||
messageFinish={messageFinish}
|
messageFinish={messageFinish}
|
||||||
syntaxTheme={syntaxTheme}
|
syntaxTheme={syntaxTheme}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
hasTouchInput={hasTouchInput}
|
alwaysShowActions={alwaysShowMessageActions}
|
||||||
|
hasTouchInput={hasTouchInput}
|
||||||
copiedCode={copiedCode}
|
copiedCode={copiedCode}
|
||||||
onCopyCode={handleCopyCode}
|
onCopyCode={handleCopyCode}
|
||||||
expandedTools={expandedTools}
|
expandedTools={expandedTools}
|
||||||
@@ -1115,8 +1118,9 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
messageCompletedAt={messageCompletedAt ?? undefined}
|
messageCompletedAt={messageCompletedAt ?? undefined}
|
||||||
messageCreatedAt={messageCreatedAt ?? undefined}
|
messageCreatedAt={messageCreatedAt ?? undefined}
|
||||||
syntaxTheme={syntaxTheme}
|
syntaxTheme={syntaxTheme}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
hasTouchInput={hasTouchInput}
|
alwaysShowActions={alwaysShowMessageActions}
|
||||||
|
hasTouchInput={hasTouchInput}
|
||||||
copiedCode={copiedCode}
|
copiedCode={copiedCode}
|
||||||
onCopyCode={handleCopyCode}
|
onCopyCode={handleCopyCode}
|
||||||
expandedTools={effectiveExpandedTools}
|
expandedTools={effectiveExpandedTools}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip
|
|||||||
import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
|
import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
|
||||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
|
|
||||||
import type { ToolPopupContent } from './message/types';
|
import type { ToolPopupContent } from './message/types';
|
||||||
|
|
||||||
@@ -127,6 +128,8 @@ interface ImagePreviewProps {
|
|||||||
|
|
||||||
const ImagePreview = memo(({ file, onRemove }: ImagePreviewProps) => {
|
const ImagePreview = memo(({ file, onRemove }: ImagePreviewProps) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowActions = isMobile || isTablet;
|
||||||
const isLocalImagePreview =
|
const isLocalImagePreview =
|
||||||
file.source !== 'server' &&
|
file.source !== 'server' &&
|
||||||
file.mimeType.startsWith('image/') &&
|
file.mimeType.startsWith('image/') &&
|
||||||
@@ -184,7 +187,10 @@ const ImagePreview = memo(({ file, onRemove }: ImagePreviewProps) => {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
className="absolute top-0.5 right-0.5 h-4 w-4 rounded-full bg-background/80 text-foreground hover:text-destructive flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
className={cn(
|
||||||
|
"absolute top-0.5 right-0.5 h-4 w-4 rounded-full bg-background/80 text-foreground hover:text-destructive flex items-center justify-center transition-opacity focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||||
|
alwaysShowActions ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
title={t('chat.fileAttachment.actions.removeImage')}
|
title={t('chat.fileAttachment.actions.removeImage')}
|
||||||
aria-label={t('chat.fileAttachment.actions.removeNamed', { name: displayName })}
|
aria-label={t('chat.fileAttachment.actions.removeNamed', { name: displayName })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -311,10 +311,15 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement |
|
|||||||
// Table wrapper with custom controls
|
// Table wrapper with custom controls
|
||||||
const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }> = ({ children, className }) => {
|
const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }> = ({ children, className }) => {
|
||||||
const tableRef = React.useRef<HTMLDivElement>(null);
|
const tableRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowActions = isMobile || isTablet;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group my-4 flex flex-col space-y-2" data-markdown="table-wrapper" ref={tableRef}>
|
<div className="group my-4 flex flex-col space-y-2" data-markdown="table-wrapper" ref={tableRef}>
|
||||||
<div className="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className={cn(
|
||||||
|
"flex items-center justify-end gap-1 transition-opacity",
|
||||||
|
alwaysShowActions ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
||||||
|
)}>
|
||||||
<TableCopyButton tableRef={tableRef} />
|
<TableCopyButton tableRef={tableRef} />
|
||||||
<TableDownloadButton tableRef={tableRef} />
|
<TableDownloadButton tableRef={tableRef} />
|
||||||
</div>
|
</div>
|
||||||
@@ -330,7 +335,7 @@ const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }>
|
|||||||
const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ source, mode }) => {
|
const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ source, mode }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const currentTheme = useCurrentMermaidTheme();
|
const currentTheme = useCurrentMermaidTheme();
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
const [copied, setCopied] = React.useState(false);
|
const [copied, setCopied] = React.useState(false);
|
||||||
const [downloaded, setDownloaded] = React.useState(false);
|
const [downloaded, setDownloaded] = React.useState(false);
|
||||||
|
|
||||||
@@ -362,7 +367,7 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
|
|||||||
}
|
}
|
||||||
}, [mode, source]);
|
}, [mode, source]);
|
||||||
|
|
||||||
const copyVisibilityClass = isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100';
|
const copyVisibilityClass = isMobile || isTablet ? 'opacity-100' : 'opacity-0 group-hover:opacity-100';
|
||||||
|
|
||||||
const handleCopyAscii = async (asciiText: string) => {
|
const handleCopyAscii = async (asciiText: string) => {
|
||||||
if (!asciiText) return;
|
if (!asciiText) return;
|
||||||
@@ -724,6 +729,7 @@ const MarkdownCodeBlock: React.FC<{
|
|||||||
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>('code');
|
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>('code');
|
||||||
const prevCodeRef = React.useRef<string>(code);
|
const prevCodeRef = React.useRef<string>(code);
|
||||||
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
|
|
||||||
const canPreview = language === 'html' || language === 'htm';
|
const canPreview = language === 'html' || language === 'htm';
|
||||||
|
|
||||||
@@ -774,7 +780,10 @@ const MarkdownCodeBlock: React.FC<{
|
|||||||
<div data-component="markdown-code" className="my-4 group overflow-hidden rounded-2xl border border-border/80 bg-[var(--surface-elevated)]">
|
<div data-component="markdown-code" className="my-4 group overflow-hidden rounded-2xl border border-border/80 bg-[var(--surface-elevated)]">
|
||||||
<div className="flex items-center justify-between border-b border-border/70 px-3 py-1.5">
|
<div className="flex items-center justify-between border-b border-border/70 px-3 py-1.5">
|
||||||
<span className="font-mono text-[13px] text-muted-foreground">{language}</span>
|
<span className="font-mono text-[13px] text-muted-foreground">{language}</span>
|
||||||
<div className="flex items-center gap-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100">
|
<div className={cn(
|
||||||
|
"flex items-center gap-1 transition-opacity",
|
||||||
|
isMobile || isTablet ? "opacity-100" : "opacity-100 md:opacity-0 md:group-hover:opacity-100"
|
||||||
|
)}>
|
||||||
{canPreview ? (
|
{canPreview ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -426,7 +426,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false);
|
const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false);
|
||||||
const { favoriteModelsList, recentModelsList } = useModelLists();
|
const { favoriteModelsList, recentModelsList } = useModelLists();
|
||||||
|
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowHoverDetails = isMobile || isTablet;
|
||||||
const isDesktop = React.useMemo(() => isDesktopShell(), []);
|
const isDesktop = React.useMemo(() => isDesktopShell(), []);
|
||||||
const isVSCodeRuntime = useIsVSCodeRuntime();
|
const isVSCodeRuntime = useIsVSCodeRuntime();
|
||||||
// Only use mobile panels on actual mobile devices, VSCode uses desktop dropdowns
|
// Only use mobile panels on actual mobile devices, VSCode uses desktop dropdowns
|
||||||
@@ -2378,7 +2379,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
) : slides.length > 0 ? (
|
) : slides.length > 0 ? (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"items-center",
|
"items-center",
|
||||||
shouldAnimate ? "flex w-[140px] justify-end" : ((isHighlighted || isSelected) ? "flex" : "hidden group-hover:flex")
|
shouldAnimate ? "flex w-[140px] justify-end" : ((isHighlighted || isSelected || alwaysShowHoverDetails) ? "flex" : "hidden group-hover:flex")
|
||||||
)}>
|
)}>
|
||||||
{shouldAnimate ? (
|
{shouldAnimate ? (
|
||||||
<TextLoop interval={2.1} transition={{ duration: 0.25 }} trigger={shouldAnimate}>
|
<TextLoop interval={2.1} transition={{ duration: 0.25 }} trigger={shouldAnimate}>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { RiLoader4Line, RiSearchLine, RiTimeLine, RiGitBranchLine, RiArrowGoBack
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import type { Part } from '@opencode-ai/sdk/v2';
|
import type { Part } from '@opencode-ai/sdk/v2';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface TimelineDialogProps {
|
interface TimelineDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -34,6 +36,8 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
|
|||||||
const messages = useSessionMessageRecords(currentSessionId ?? '');
|
const messages = useSessionMessageRecords(currentSessionId ?? '');
|
||||||
const revertToMessage = useSessionUIStore((state) => state.revertToMessage);
|
const revertToMessage = useSessionUIStore((state) => state.revertToMessage);
|
||||||
const forkFromMessage = useSessionUIStore((state) => state.forkFromMessage);
|
const forkFromMessage = useSessionUIStore((state) => state.forkFromMessage);
|
||||||
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowActions = isMobile || isTablet;
|
||||||
|
|
||||||
const [forkingMessageId, setForkingMessageId] = React.useState<string | null>(null);
|
const [forkingMessageId, setForkingMessageId] = React.useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = React.useState('');
|
const [searchQuery, setSearchQuery] = React.useState('');
|
||||||
@@ -140,11 +144,11 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex-shrink-0 h-5 flex items-center mr-2">
|
<div className="flex-shrink-0 h-5 flex items-center mr-2">
|
||||||
<span className="typography-meta text-muted-foreground whitespace-nowrap group-hover:hidden">
|
<span className={cn("typography-meta text-muted-foreground whitespace-nowrap", alwaysShowActions ? "hidden" : "group-hover:hidden")}>
|
||||||
{relativeTime}
|
{relativeTime}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="hidden group-hover:flex gap-1">
|
<div className={cn("gap-1", alwaysShowActions ? "flex" : "hidden group-hover:flex")}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ interface MessageBodyProps {
|
|||||||
syntaxTheme: { [key: string]: React.CSSProperties };
|
syntaxTheme: { [key: string]: React.CSSProperties };
|
||||||
|
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
alwaysShowActions?: boolean;
|
||||||
hasTouchInput?: boolean;
|
hasTouchInput?: boolean;
|
||||||
copiedCode: string | null;
|
copiedCode: string | null;
|
||||||
onCopyCode: (code: string) => void;
|
onCopyCode: (code: string) => void;
|
||||||
@@ -326,10 +327,11 @@ const writeRevealedToolIds = (messageId: string, value: Set<string>): void => {
|
|||||||
revealedToolIdsByMessage.set(messageId, new Set(value));
|
revealedToolIdsByMessage.set(messageId, new Set(value));
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserMessageBody = React.memo(({ messageId, parts, isMobile, hasTouchInput, hasTextContent, onCopyMessage, copiedMessage, onShowPopup, agentMention, onRevert, onFork, userActionsMode = 'inline', stickyUserHeaderEnabled = true }: {
|
const UserMessageBody = React.memo(({ messageId, parts, isMobile, alwaysShowActions = isMobile, hasTouchInput, hasTextContent, onCopyMessage, copiedMessage, onShowPopup, agentMention, onRevert, onFork, userActionsMode = 'inline', stickyUserHeaderEnabled = true }: {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
parts: Part[];
|
parts: Part[];
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
alwaysShowActions?: boolean;
|
||||||
hasTouchInput?: boolean;
|
hasTouchInput?: boolean;
|
||||||
hasTextContent?: boolean;
|
hasTextContent?: boolean;
|
||||||
onCopyMessage?: () => void;
|
onCopyMessage?: () => void;
|
||||||
@@ -438,7 +440,7 @@ const UserMessageBody = React.memo(({ messageId, parts, isMobile, hasTouchInput,
|
|||||||
: userActionsMode === 'inline'
|
: userActionsMode === 'inline'
|
||||||
? 'translate-x-5'
|
? 'translate-x-5'
|
||||||
: 'translate-x-0',
|
: 'translate-x-0',
|
||||||
isMobile
|
alwaysShowActions
|
||||||
? 'pointer-events-auto opacity-100'
|
? 'pointer-events-auto opacity-100'
|
||||||
: 'pointer-events-none opacity-0 transition-opacity duration-150 group-hover/message:pointer-events-auto group-hover/message:opacity-100 group-hover/user-actions:pointer-events-auto group-hover/user-actions:opacity-100 group-hover/user-shell:pointer-events-auto group-hover/user-shell:opacity-100'
|
: 'pointer-events-none opacity-0 transition-opacity duration-150 group-hover/message:pointer-events-auto group-hover/message:opacity-100 group-hover/user-actions:pointer-events-auto group-hover/user-actions:opacity-100 group-hover/user-shell:pointer-events-auto group-hover/user-shell:opacity-100'
|
||||||
)}
|
)}
|
||||||
@@ -838,6 +840,7 @@ const AssistantMessageBody = React.memo(({
|
|||||||
|
|
||||||
syntaxTheme,
|
syntaxTheme,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
alwaysShowActions,
|
||||||
hasTouchInput,
|
hasTouchInput,
|
||||||
expandedTools,
|
expandedTools,
|
||||||
onToggleTool,
|
onToggleTool,
|
||||||
@@ -865,6 +868,7 @@ const AssistantMessageBody = React.memo(({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isTouchContext = Boolean(hasTouchInput ?? isMobile);
|
const isTouchContext = Boolean(hasTouchInput ?? isMobile);
|
||||||
|
const alwaysShowMessageActions = Boolean(alwaysShowActions ?? isMobile);
|
||||||
const awaitingMessageCompletion = !isMessageCompleted;
|
const awaitingMessageCompletion = !isMessageCompleted;
|
||||||
const animateActivityRows = awaitingMessageCompletion || Boolean(turnGroupingContext?.isWorking);
|
const animateActivityRows = awaitingMessageCompletion || Boolean(turnGroupingContext?.isWorking);
|
||||||
|
|
||||||
@@ -1550,6 +1554,7 @@ const AssistantMessageBody = React.memo(({
|
|||||||
part={part}
|
part={part}
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
onContentChange={onContentChange}
|
onContentChange={onContentChange}
|
||||||
|
alwaysShowActions={alwaysShowMessageActions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -1601,6 +1606,7 @@ const AssistantMessageBody = React.memo(({
|
|||||||
onToggle={onToggleTool}
|
onToggle={onToggleTool}
|
||||||
syntaxTheme={syntaxTheme}
|
syntaxTheme={syntaxTheme}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
alwaysShowActions={alwaysShowMessageActions}
|
||||||
onContentChange={onContentChange}
|
onContentChange={onContentChange}
|
||||||
onShowPopup={onShowPopup}
|
onShowPopup={onShowPopup}
|
||||||
animateTailText={animatedToolIdsLookup.has(toolPart.id)}
|
animateTailText={animatedToolIdsLookup.has(toolPart.id)}
|
||||||
@@ -1890,6 +1896,7 @@ const MessageBody = React.memo(({ isUser, ...props }: MessageBodyProps) => {
|
|||||||
messageId={props.messageId}
|
messageId={props.messageId}
|
||||||
parts={props.parts}
|
parts={props.parts}
|
||||||
isMobile={props.isMobile}
|
isMobile={props.isMobile}
|
||||||
|
alwaysShowActions={props.alwaysShowActions}
|
||||||
hasTouchInput={props.hasTouchInput}
|
hasTouchInput={props.hasTouchInput}
|
||||||
hasTextContent={props.hasTextContent}
|
hasTextContent={props.hasTextContent}
|
||||||
onCopyMessage={props.onCopyMessage}
|
onCopyMessage={props.onCopyMessage}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ type ReasoningTimelineBlockProps = {
|
|||||||
showDuration?: boolean;
|
showDuration?: boolean;
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
|
alwaysShowActions?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
|
export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
|
||||||
@@ -93,6 +94,7 @@ export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
|
|||||||
showDuration = true,
|
showDuration = true,
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
actions,
|
actions,
|
||||||
|
alwaysShowActions = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 transition-opacity',
|
'absolute inset-0 transition-opacity',
|
||||||
isExpanded && 'opacity-0',
|
isExpanded && 'opacity-0',
|
||||||
!isExpanded && 'group-hover/tool:opacity-0'
|
!isExpanded && (alwaysShowActions ? 'opacity-0' : 'group-hover/tool:opacity-0')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-3.5 w-3.5" />
|
<Icon className="h-3.5 w-3.5" />
|
||||||
@@ -135,7 +137,7 @@ export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 transition-opacity flex items-center justify-center',
|
'absolute inset-0 transition-opacity flex items-center justify-center',
|
||||||
isExpanded && 'opacity-100',
|
isExpanded && 'opacity-100',
|
||||||
!isExpanded && 'opacity-0 group-hover/tool:opacity-100'
|
!isExpanded && (alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/tool:opacity-100')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isExpanded ? <RiArrowDownSLine className="h-3.5 w-3.5" /> : <RiArrowRightSLine className="h-3.5 w-3.5" />}
|
{isExpanded ? <RiArrowDownSLine className="h-3.5 w-3.5" /> : <RiArrowRightSLine className="h-3.5 w-3.5" />}
|
||||||
@@ -200,12 +202,14 @@ type ReasoningPartProps = {
|
|||||||
part: Part;
|
part: Part;
|
||||||
onContentChange?: (reason?: ContentChangeReason) => void;
|
onContentChange?: (reason?: ContentChangeReason) => void;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
alwaysShowActions?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReasoningPart = React.memo(({
|
const ReasoningPart = React.memo(({
|
||||||
part,
|
part,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
messageId,
|
messageId,
|
||||||
|
alwaysShowActions = false,
|
||||||
}: ReasoningPartProps) => {
|
}: ReasoningPartProps) => {
|
||||||
const chatRenderMode = useUIStore((state) => state.chatRenderMode);
|
const chatRenderMode = useUIStore((state) => state.chatRenderMode);
|
||||||
const partWithText = part as PartWithText;
|
const partWithText = part as PartWithText;
|
||||||
@@ -234,6 +238,7 @@ const ReasoningPart = React.memo(({
|
|||||||
time={time}
|
time={time}
|
||||||
showDuration={chatRenderMode !== 'sorted'}
|
showDuration={chatRenderMode !== 'sorted'}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
|
alwaysShowActions={alwaysShowActions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ interface ToolPartProps {
|
|||||||
onToggle: (toolId: string) => void;
|
onToggle: (toolId: string) => void;
|
||||||
syntaxTheme: { [key: string]: React.CSSProperties };
|
syntaxTheme: { [key: string]: React.CSSProperties };
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
alwaysShowActions?: boolean;
|
||||||
onContentChange?: (reason?: ContentChangeReason) => void;
|
onContentChange?: (reason?: ContentChangeReason) => void;
|
||||||
onShowPopup?: (content: ToolPopupContent) => void;
|
onShowPopup?: (content: ToolPopupContent) => void;
|
||||||
animateTailText?: boolean;
|
animateTailText?: boolean;
|
||||||
@@ -1804,6 +1805,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
|||||||
onToggle,
|
onToggle,
|
||||||
syntaxTheme,
|
syntaxTheme,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
alwaysShowActions = isMobile,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
onShowPopup,
|
onShowPopup,
|
||||||
animateTailText = true,
|
animateTailText = true,
|
||||||
@@ -2522,7 +2524,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 transition-opacity',
|
'absolute inset-0 transition-opacity',
|
||||||
isExpanded && 'opacity-0',
|
isExpanded && 'opacity-0',
|
||||||
!isExpanded && 'group-hover/tool:opacity-0'
|
!isExpanded && (alwaysShowActions ? 'opacity-0' : 'group-hover/tool:opacity-0')
|
||||||
)}
|
)}
|
||||||
style={iconStyle}
|
style={iconStyle}
|
||||||
>
|
>
|
||||||
@@ -2533,7 +2535,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 transition-opacity flex items-center justify-center',
|
'absolute inset-0 transition-opacity flex items-center justify-center',
|
||||||
isExpanded && 'opacity-100',
|
isExpanded && 'opacity-100',
|
||||||
!isExpanded && 'opacity-0 group-hover/tool:opacity-100'
|
!isExpanded && (alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/tool:opacity-100')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isExpanded ? <RiArrowDownSLine className="h-3.5 w-3.5" /> : <RiArrowRightSLine className="h-3.5 w-3.5" />}
|
{isExpanded ? <RiArrowDownSLine className="h-3.5 w-3.5" /> : <RiArrowRightSLine className="h-3.5 w-3.5" />}
|
||||||
@@ -2661,6 +2663,7 @@ export default React.memo(ToolPart, (prev, next) => {
|
|||||||
&& prev.isExpanded === next.isExpanded
|
&& prev.isExpanded === next.isExpanded
|
||||||
&& prev.syntaxTheme === next.syntaxTheme
|
&& prev.syntaxTheme === next.syntaxTheme
|
||||||
&& prev.isMobile === next.isMobile
|
&& prev.isMobile === next.isMobile
|
||||||
|
&& prev.alwaysShowActions === next.alwaysShowActions
|
||||||
&& prev.onContentChange === next.onContentChange
|
&& prev.onContentChange === next.onContentChange
|
||||||
&& prev.onShowPopup === next.onShowPopup
|
&& prev.onShowPopup === next.onShowPopup
|
||||||
&& prev.animateTailText === next.animateTailText;
|
&& prev.animateTailText === next.animateTailText;
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export interface WorktreeSectionContentProps {
|
|||||||
|
|
||||||
export const WorktreeSectionContent: React.FC<WorktreeSectionContentProps> = ({ projectRef: projectRefProp = null }) => {
|
export const WorktreeSectionContent: React.FC<WorktreeSectionContentProps> = ({ projectRef: projectRefProp = null }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowActions = isMobile || isTablet;
|
||||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||||
|
|
||||||
const projectPath = projectRefProp?.path ?? activeProject?.path ?? null;
|
const projectPath = projectRefProp?.path ?? activeProject?.path ?? null;
|
||||||
@@ -374,7 +375,7 @@ export const WorktreeSectionContent: React.FC<WorktreeSectionContentProps> = ({
|
|||||||
onClick={() => handleDeleteWorktree(worktree)}
|
onClick={() => handleDeleteWorktree(worktree)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-shrink-0 flex h-7 w-7 items-center justify-center rounded text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50",
|
"flex-shrink-0 flex h-7 w-7 items-center justify-center rounded text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50",
|
||||||
isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
alwaysShowActions ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
||||||
)}
|
)}
|
||||||
aria-label={t('settings.openchamber.worktrees.list.deleteWorktreeAria', { name: worktree.branch || worktree.label || worktree.path })}
|
aria-label={t('settings.openchamber.worktrees.list.deleteWorktreeAria', { name: worktree.branch || worktree.label || worktree.path })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
disabledPaths,
|
disabledPaths,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
|
const showHoverActions = alwaysShowActions || isTablet;
|
||||||
const [directories, setDirectories] = React.useState<DirectoryItem[]>([]);
|
const [directories, setDirectories] = React.useState<DirectoryItem[]>([]);
|
||||||
const [expandedPaths, setExpandedPaths] = React.useState<Set<string>>(new Set());
|
const [expandedPaths, setExpandedPaths] = React.useState<Set<string>>(new Set());
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
@@ -1146,7 +1147,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
onClick={() => togglePin(path)}
|
onClick={() => togglePin(path)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-interactive-hover rounded-md transition-opacity",
|
"hover:bg-interactive-hover rounded-md transition-opacity",
|
||||||
isMobile ? "p-1.5 opacity-60" : "p-1 opacity-0 group-hover:opacity-100"
|
showHoverActions ? "p-1.5 opacity-60" : "p-1 opacity-0 group-hover:opacity-100"
|
||||||
)}
|
)}
|
||||||
title={t('directoryTree.actions.unpinDirectory')}
|
title={t('directoryTree.actions.unpinDirectory')}
|
||||||
>
|
>
|
||||||
@@ -1200,7 +1201,10 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
togglePin(path);
|
togglePin(path);
|
||||||
}}
|
}}
|
||||||
className="p-1 opacity-0 group-hover:opacity-100 hover:bg-interactive-hover rounded transition-opacity flex-shrink-0"
|
className={cn(
|
||||||
|
"hover:bg-interactive-hover rounded transition-opacity flex-shrink-0",
|
||||||
|
showHoverActions ? "p-1.5 opacity-60" : "p-1 opacity-0 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
title={t('directoryTree.actions.unpinDirectory')}
|
title={t('directoryTree.actions.unpinDirectory')}
|
||||||
>
|
>
|
||||||
<RiPushpin2Line className="h-3 w-3 text-primary" />
|
<RiPushpin2Line className="h-3 w-3 text-primary" />
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
|||||||
import { opencodeClient } from '@/lib/opencode/client';
|
import { opencodeClient } from '@/lib/opencode/client';
|
||||||
import { renderMagicPrompt } from '@/lib/magicPrompts';
|
import { renderMagicPrompt } from '@/lib/magicPrompts';
|
||||||
import { parseModelIdentifier } from '@/lib/modelIdentifier';
|
import { parseModelIdentifier } from '@/lib/modelIdentifier';
|
||||||
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { createWorktreeSessionForNewBranch } from '@/lib/worktreeSessionCreator';
|
import { createWorktreeSessionForNewBranch } from '@/lib/worktreeSessionCreator';
|
||||||
import { generateBranchSlug } from '@/lib/git/branchNameGenerator';
|
import { generateBranchSlug } from '@/lib/git/branchNameGenerator';
|
||||||
import type { GitHubIssue, GitHubIssueComment, GitHubIssuesListResult, GitHubIssueSummary, GitHubRepoSelector } from '@/lib/api/types';
|
import type { GitHubIssue, GitHubIssueComment, GitHubIssuesListResult, GitHubIssueSummary, GitHubRepoSelector } from '@/lib/api/types';
|
||||||
@@ -87,6 +88,8 @@ export function GitHubIssuePickerDialog({
|
|||||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||||
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
const { isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowActions = isMobile || isTablet;
|
||||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||||
|
|
||||||
@@ -591,7 +594,10 @@ export function GitHubIssuePickerDialog({
|
|||||||
href={issue.url}
|
href={issue.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="hidden group-hover:flex h-5 w-5 items-center justify-center text-muted-foreground hover:text-foreground transition-colors"
|
className={cn(
|
||||||
|
"h-5 w-5 items-center justify-center text-muted-foreground hover:text-foreground transition-colors",
|
||||||
|
alwaysShowActions ? "flex" : "hidden group-hover:flex"
|
||||||
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={t('session.githubIssuePicker.actions.openInGitHubAria')}
|
aria-label={t('session.githubIssuePicker.actions.openInGitHubAria')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useProjectsStore } from '@/stores/useProjectsStore';
|
|||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import { renderMagicPrompt } from '@/lib/magicPrompts';
|
import { renderMagicPrompt } from '@/lib/magicPrompts';
|
||||||
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import type { GitHubPullRequestContextResult, GitHubPullRequestSummary, GitHubPullRequestsListResult, GitHubRepoSelector } from '@/lib/api/types';
|
import type { GitHubPullRequestContextResult, GitHubPullRequestSummary, GitHubPullRequestsListResult, GitHubRepoSelector } from '@/lib/api/types';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ export function GitHubPrPickerDialog({
|
|||||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||||
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
const { isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowActions = isMobile || isTablet;
|
||||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||||
|
|
||||||
const projectDirectory = activeProject?.path ?? null;
|
const projectDirectory = activeProject?.path ?? null;
|
||||||
@@ -377,7 +380,10 @@ export function GitHubPrPickerDialog({
|
|||||||
href={pr.url}
|
href={pr.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="hidden group-hover:flex h-5 w-5 items-center justify-center text-muted-foreground hover:text-foreground transition-colors"
|
className={cn(
|
||||||
|
"h-5 w-5 items-center justify-center text-muted-foreground hover:text-foreground transition-colors",
|
||||||
|
alwaysShowActions ? "flex" : "hidden group-hover:flex"
|
||||||
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={t('session.githubPrPicker.actions.openInGitHubAria')}
|
aria-label={t('session.githubPrPicker.actions.openInGitHubAria')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface SessionFolderItemProps<TSessionNode> {
|
|||||||
groupDirectory?: string | null;
|
groupDirectory?: string | null;
|
||||||
projectId?: string | null;
|
projectId?: string | null;
|
||||||
mobileVariant?: boolean;
|
mobileVariant?: boolean;
|
||||||
|
alwaysShowActions?: boolean;
|
||||||
isRenaming?: boolean;
|
isRenaming?: boolean;
|
||||||
renameDraft?: string;
|
renameDraft?: string;
|
||||||
onRenameDraftChange?: (value: string) => void;
|
onRenameDraftChange?: (value: string) => void;
|
||||||
@@ -67,6 +68,7 @@ const SessionFolderItemBase = <TSessionNode,>({
|
|||||||
groupDirectory,
|
groupDirectory,
|
||||||
projectId,
|
projectId,
|
||||||
mobileVariant = false,
|
mobileVariant = false,
|
||||||
|
alwaysShowActions = mobileVariant,
|
||||||
isRenaming = false,
|
isRenaming = false,
|
||||||
renameDraft = '',
|
renameDraft = '',
|
||||||
onRenameDraftChange,
|
onRenameDraftChange,
|
||||||
@@ -174,7 +176,7 @@ const SessionFolderItemBase = <TSessionNode,>({
|
|||||||
<div className={cn(
|
<div className={cn(
|
||||||
'min-w-0 flex items-center gap-1.5 pl-1.5 flex-1 transition-[padding]',
|
'min-w-0 flex items-center gap-1.5 pl-1.5 flex-1 transition-[padding]',
|
||||||
archivedBucket
|
archivedBucket
|
||||||
? (mobileVariant ? 'pr-7' : 'group-hover/folder:pr-7 group-focus-within/folder:pr-7')
|
? (alwaysShowActions ? 'pr-7' : 'group-hover/folder:pr-7 group-focus-within/folder:pr-7')
|
||||||
: '',
|
: '',
|
||||||
)}>
|
)}>
|
||||||
<FolderIcon className={cn('h-3.5 w-3.5 flex-shrink-0', isDropTarget ? 'text-primary' : 'text-muted-foreground')} />
|
<FolderIcon className={cn('h-3.5 w-3.5 flex-shrink-0', isDropTarget ? 'text-primary' : 'text-muted-foreground')} />
|
||||||
@@ -257,7 +259,7 @@ const SessionFolderItemBase = <TSessionNode,>({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-0.5 transition-opacity',
|
'flex items-center gap-0.5 transition-opacity',
|
||||||
mobileVariant ? 'opacity-100' : 'opacity-0 group-hover/folder:opacity-100 group-focus-within/folder:opacity-100',
|
alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/folder:opacity-100 group-focus-within/folder:opacity-100',
|
||||||
archivedBucket && 'absolute right-0.5 top-1/2 z-10 -translate-y-1/2 px-0',
|
archivedBucket && 'absolute right-0.5 top-1/2 z-10 -translate-y-1/2 px-0',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { RiLayoutLeftLine } from '@remixicon/react';
|
|||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { isDesktopShell } from '@/lib/desktop';
|
import { isDesktopShell } from '@/lib/desktop';
|
||||||
import { isDesktopWindowFullscreen as getDesktopWindowFullscreen, onDesktopWindowResized, startDesktopWindowDrag } from '@/lib/desktopNative';
|
import { isDesktopWindowFullscreen as getDesktopWindowFullscreen, onDesktopWindowResized, startDesktopWindowDrag } from '@/lib/desktopNative';
|
||||||
import { sessionEvents } from '@/lib/sessionEvents';
|
import { sessionEvents } from '@/lib/sessionEvents';
|
||||||
@@ -437,6 +438,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
|
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
|
||||||
|
|
||||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
const { isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowSidebarActions = mobileVariant || isTablet;
|
||||||
const isMacPlatform = React.useMemo(() => {
|
const isMacPlatform = React.useMemo(() => {
|
||||||
if (typeof navigator === 'undefined') {
|
if (typeof navigator === 'undefined') {
|
||||||
return false;
|
return false;
|
||||||
@@ -1281,6 +1284,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
openContextPanelTab={openContextPanelTab}
|
openContextPanelTab={openContextPanelTab}
|
||||||
handleDeleteSession={handleDeleteSession}
|
handleDeleteSession={handleDeleteSession}
|
||||||
mobileVariant={mobileVariant}
|
mobileVariant={mobileVariant}
|
||||||
|
alwaysShowActions={alwaysShowSidebarActions}
|
||||||
renderSessionNode={renderSessionNode}
|
renderSessionNode={renderSessionNode}
|
||||||
secondaryMeta={secondaryMeta}
|
secondaryMeta={secondaryMeta}
|
||||||
renderContext={renderContext}
|
renderContext={renderContext}
|
||||||
@@ -1319,6 +1323,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
openContextPanelTab,
|
openContextPanelTab,
|
||||||
handleDeleteSession,
|
handleDeleteSession,
|
||||||
mobileVariant,
|
mobileVariant,
|
||||||
|
alwaysShowSidebarActions,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1378,6 +1383,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
lastRepoStatus={lastRepoStatusRef.current}
|
lastRepoStatus={lastRepoStatusRef.current}
|
||||||
toggleGroupSessionLimit={toggleGroupSessionLimit}
|
toggleGroupSessionLimit={toggleGroupSessionLimit}
|
||||||
mobileVariant={mobileVariant}
|
mobileVariant={mobileVariant}
|
||||||
|
alwaysShowActions={alwaysShowSidebarActions}
|
||||||
activeProjectId={activeProjectId}
|
activeProjectId={activeProjectId}
|
||||||
setActiveProjectIdOnly={setActiveProjectIdOnly}
|
setActiveProjectIdOnly={setActiveProjectIdOnly}
|
||||||
setActiveMainTab={setActiveMainTab}
|
setActiveMainTab={setActiveMainTab}
|
||||||
@@ -1413,6 +1419,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
projectRepoStatus,
|
projectRepoStatus,
|
||||||
toggleGroupSessionLimit,
|
toggleGroupSessionLimit,
|
||||||
mobileVariant,
|
mobileVariant,
|
||||||
|
alwaysShowSidebarActions,
|
||||||
activeProjectId,
|
activeProjectId,
|
||||||
setActiveProjectIdOnly,
|
setActiveProjectIdOnly,
|
||||||
setActiveMainTab,
|
setActiveMainTab,
|
||||||
@@ -1692,6 +1699,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
isDesktopShellRuntime={isDesktopShellRuntime}
|
isDesktopShellRuntime={isDesktopShellRuntime}
|
||||||
stuckProjectHeaders={stuckProjectHeaders}
|
stuckProjectHeaders={stuckProjectHeaders}
|
||||||
mobileVariant={mobileVariant}
|
mobileVariant={mobileVariant}
|
||||||
|
alwaysShowActions={alwaysShowSidebarActions}
|
||||||
toggleProject={toggleProject}
|
toggleProject={toggleProject}
|
||||||
setActiveProjectIdOnly={setActiveProjectIdOnly}
|
setActiveProjectIdOnly={setActiveProjectIdOnly}
|
||||||
setActiveMainTab={setActiveMainTab}
|
setActiveMainTab={setActiveMainTab}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type Props = {
|
|||||||
lastRepoStatus: boolean;
|
lastRepoStatus: boolean;
|
||||||
toggleGroupSessionLimit: (groupKey: string) => void;
|
toggleGroupSessionLimit: (groupKey: string) => void;
|
||||||
mobileVariant: boolean;
|
mobileVariant: boolean;
|
||||||
|
alwaysShowActions: boolean;
|
||||||
activeProjectId: string | null;
|
activeProjectId: string | null;
|
||||||
setActiveProjectIdOnly: (id: string) => void;
|
setActiveProjectIdOnly: (id: string) => void;
|
||||||
setActiveMainTab: (tab: MainTab) => void;
|
setActiveMainTab: (tab: MainTab) => void;
|
||||||
@@ -120,6 +121,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
lastRepoStatus,
|
lastRepoStatus,
|
||||||
toggleGroupSessionLimit,
|
toggleGroupSessionLimit,
|
||||||
mobileVariant,
|
mobileVariant,
|
||||||
|
alwaysShowActions,
|
||||||
activeProjectId,
|
activeProjectId,
|
||||||
setActiveProjectIdOnly,
|
setActiveProjectIdOnly,
|
||||||
setActiveMainTab,
|
setActiveMainTab,
|
||||||
@@ -405,6 +407,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
groupDirectory={group.directory}
|
groupDirectory={group.directory}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
mobileVariant={mobileVariant}
|
mobileVariant={mobileVariant}
|
||||||
|
alwaysShowActions={alwaysShowActions}
|
||||||
isRenaming={renamingFolderId === folder.id}
|
isRenaming={renamingFolderId === folder.id}
|
||||||
renameDraft={renamingFolderId === folder.id ? renameFolderDraft : undefined}
|
renameDraft={renamingFolderId === folder.id ? renameFolderDraft : undefined}
|
||||||
onRenameDraftChange={(value) => setRenameFolderDraft(value)}
|
onRenameDraftChange={(value) => setRenameFolderDraft(value)}
|
||||||
@@ -443,7 +446,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
|
|
||||||
const renderFolderItems = () => rootFolders.map(({ folder, nodes }) => renderOneFolderItem(folder, nodes, 0));
|
const renderFolderItems = () => rootFolders.map(({ folder, nodes }) => renderOneFolderItem(folder, nodes, 0));
|
||||||
const hasWorktreeDeleteAction = Boolean(!group.isMain && group.worktree);
|
const hasWorktreeDeleteAction = Boolean(!group.isMain && group.worktree);
|
||||||
const groupHeaderRightPadding = mobileVariant
|
const groupHeaderRightPadding = alwaysShowActions
|
||||||
? (hasWorktreeDeleteAction ? 'pr-14' : 'pr-7')
|
? (hasWorktreeDeleteAction ? 'pr-14' : 'pr-7')
|
||||||
: isMinimalMode
|
: isMinimalMode
|
||||||
? (hasWorktreeDeleteAction
|
? (hasWorktreeDeleteAction
|
||||||
@@ -534,10 +537,13 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
<span className="inline-flex shrink-0 items-center gap-1 leading-none align-middle">
|
<span className="inline-flex shrink-0 items-center gap-1 leading-none align-middle">
|
||||||
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||||
<RiGitBranchLine
|
<RiGitBranchLine
|
||||||
className="h-3.5 w-3.5 shrink-0 group-hover/gh:hidden"
|
className={cn('h-3.5 w-3.5 shrink-0', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
|
||||||
style={branchIconColor ? { color: branchIconColor } : undefined}
|
style={branchIconColor ? { color: branchIconColor } : undefined}
|
||||||
/>
|
/>
|
||||||
<span className="hidden text-muted-foreground group-hover/gh:inline-flex h-3.5 w-3.5 items-center justify-center">
|
<span className={cn(
|
||||||
|
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
|
||||||
|
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
|
||||||
|
)}>
|
||||||
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -587,8 +593,11 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
) : group.isArchivedBucket ? (
|
) : group.isArchivedBucket ? (
|
||||||
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
|
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
|
||||||
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||||
<RiArchiveLine className="h-3.5 w-3.5 shrink-0 text-muted-foreground group-hover/gh:hidden" />
|
<RiArchiveLine className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')} />
|
||||||
<span className="hidden text-muted-foreground group-hover/gh:inline-flex h-3.5 w-3.5 items-center justify-center">
|
<span className={cn(
|
||||||
|
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
|
||||||
|
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
|
||||||
|
)}>
|
||||||
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -598,10 +607,13 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
|
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
|
||||||
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||||
<RiGitBranchLine
|
<RiGitBranchLine
|
||||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground group-hover/gh:hidden"
|
className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
|
||||||
style={branchIconColor ? { color: branchIconColor } : undefined}
|
style={branchIconColor ? { color: branchIconColor } : undefined}
|
||||||
/>
|
/>
|
||||||
<span className="hidden text-muted-foreground group-hover/gh:inline-flex h-3.5 w-3.5 items-center justify-center">
|
<span className={cn(
|
||||||
|
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
|
||||||
|
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
|
||||||
|
)}>
|
||||||
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -671,7 +683,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{group.isArchivedBucket && allGroupSessions.length > 0 ? (
|
{group.isArchivedBucket && allGroupSessions.length > 0 ? (
|
||||||
<div className={cn('absolute right-0.5 top-1/2 -translate-y-1/2 z-10 transition-opacity', mobileVariant ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
<div className={cn('absolute right-0.5 top-1/2 -translate-y-1/2 z-10 transition-opacity', alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
@@ -694,7 +706,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{group.directory && !group.isMain && group.worktree ? (
|
{group.directory && !group.isMain && group.worktree ? (
|
||||||
<div className={cn('absolute right-7 top-1/2 -translate-y-1/2 z-10 transition-opacity', mobileVariant ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
<div className={cn('absolute right-7 top-1/2 -translate-y-1/2 z-10 transition-opacity', alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
@@ -718,7 +730,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{group.directory ? (
|
{group.directory ? (
|
||||||
<div className={cn('absolute right-0.5 top-1/2 -translate-y-1/2 z-10 transition-opacity', mobileVariant ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
<div className={cn('absolute right-0.5 top-1/2 -translate-y-1/2 z-10 transition-opacity', alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ type Props = {
|
|||||||
openContextPanelTab: (directory: string, options: { mode: 'chat'; dedupeKey: string; label: string }) => void;
|
openContextPanelTab: (directory: string, options: { mode: 'chat'; dedupeKey: string; label: string }) => void;
|
||||||
handleDeleteSession: (session: Session, source?: { archivedBucket?: boolean }) => void;
|
handleDeleteSession: (session: Session, source?: { archivedBucket?: boolean }) => void;
|
||||||
mobileVariant: boolean;
|
mobileVariant: boolean;
|
||||||
|
alwaysShowActions: boolean;
|
||||||
renderSessionNode: (node: SessionNode, depth?: number, groupDirectory?: string | null, projectId?: string | null, archivedBucket?: boolean, secondaryMeta?: SecondaryMeta | null, renderContext?: 'project' | 'recent') => React.ReactNode;
|
renderSessionNode: (node: SessionNode, depth?: number, groupDirectory?: string | null, projectId?: string | null, archivedBucket?: boolean, secondaryMeta?: SecondaryMeta | null, renderContext?: 'project' | 'recent') => React.ReactNode;
|
||||||
secondaryMeta?: SecondaryMeta | null;
|
secondaryMeta?: SecondaryMeta | null;
|
||||||
renderContext?: 'project' | 'recent';
|
renderContext?: 'project' | 'recent';
|
||||||
@@ -207,6 +208,7 @@ const areEqual = (prev: Props, next: Props): boolean => {
|
|||||||
if ((prev.secondaryMeta?.projectLabel ?? null) !== (next.secondaryMeta?.projectLabel ?? null)) return false;
|
if ((prev.secondaryMeta?.projectLabel ?? null) !== (next.secondaryMeta?.projectLabel ?? null)) return false;
|
||||||
if ((prev.secondaryMeta?.branchLabel ?? null) !== (next.secondaryMeta?.branchLabel ?? null)) return false;
|
if ((prev.secondaryMeta?.branchLabel ?? null) !== (next.secondaryMeta?.branchLabel ?? null)) return false;
|
||||||
if (prev.mobileVariant !== next.mobileVariant) return false;
|
if (prev.mobileVariant !== next.mobileVariant) return false;
|
||||||
|
if (prev.alwaysShowActions !== next.alwaysShowActions) return false;
|
||||||
if ((prev.renderContext ?? 'project') !== (next.renderContext ?? 'project')) return false;
|
if ((prev.renderContext ?? 'project') !== (next.renderContext ?? 'project')) return false;
|
||||||
if (prev.renamingFolderId !== next.renamingFolderId) return false;
|
if (prev.renamingFolderId !== next.renamingFolderId) return false;
|
||||||
|
|
||||||
@@ -253,6 +255,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
openContextPanelTab,
|
openContextPanelTab,
|
||||||
handleDeleteSession,
|
handleDeleteSession,
|
||||||
mobileVariant,
|
mobileVariant,
|
||||||
|
alwaysShowActions,
|
||||||
renderSessionNode,
|
renderSessionNode,
|
||||||
secondaryMeta,
|
secondaryMeta,
|
||||||
renderContext = 'project',
|
renderContext = 'project',
|
||||||
@@ -500,7 +503,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
'pointer-events-none absolute inline-flex h-3.5 items-center justify-center gap-0.5 transition-opacity',
|
'pointer-events-none absolute inline-flex h-3.5 items-center justify-center gap-0.5 transition-opacity',
|
||||||
isMinimalMode ? 'top-1/2 -translate-y-1/2' : 'top-[14.5px] -translate-y-1/2',
|
isMinimalMode ? 'top-1/2 -translate-y-1/2' : 'top-[14.5px] -translate-y-1/2',
|
||||||
showStatusMarker && isPinnedSession ? 'left-[-18px] w-6' : 'left-[-10px] w-3.5',
|
showStatusMarker && isPinnedSession ? 'left-[-18px] w-6' : 'left-[-10px] w-3.5',
|
||||||
hasChildren ? 'opacity-100 group-hover:opacity-0 group-focus-within:opacity-0' : '',
|
hasChildren && !alwaysShowActions ? 'opacity-100 group-hover:opacity-0 group-focus-within:opacity-0' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showStatusMarker ? statusMarkerContent : null}
|
{showStatusMarker ? statusMarkerContent : null}
|
||||||
@@ -525,7 +528,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'absolute left-[-10px] inline-flex h-3.5 w-3.5 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',
|
'absolute left-[-10px] inline-flex h-3.5 w-3.5 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',
|
||||||
isMinimalMode ? 'top-1/2 -translate-y-1/2' : 'top-[14.5px] -translate-y-1/2',
|
isMinimalMode ? 'top-1/2 -translate-y-1/2' : 'top-[14.5px] -translate-y-1/2',
|
||||||
isMinimalMode && showStatusMarker
|
isMinimalMode && showStatusMarker && !alwaysShowActions
|
||||||
? 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto'
|
? 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto'
|
||||||
: '',
|
: '',
|
||||||
)}
|
)}
|
||||||
@@ -741,15 +744,15 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex min-w-0 flex-1 cursor-pointer 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 disabled:cursor-not-allowed transition-[padding]',
|
'flex min-w-0 flex-1 cursor-pointer 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 disabled:cursor-not-allowed transition-[padding]',
|
||||||
isTouchPressed && 'bg-interactive-hover/70',
|
isTouchPressed && 'bg-interactive-hover/70',
|
||||||
mobileVariant
|
alwaysShowActions
|
||||||
? (isVSCode ? revealPaddingClass : 'pr-7')
|
? (isVSCode ? revealPaddingClass : 'pr-7')
|
||||||
: revealPaddingClass,
|
: revealPaddingClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cn('flex w-full items-center min-w-0 flex-1 overflow-hidden', isMinimalMode ? 'gap-1' : 'gap-1')}>
|
<div className={cn('flex w-full items-center min-w-0 flex-1 overflow-hidden', isMinimalMode ? 'gap-1' : 'gap-1')}>
|
||||||
<div className={cn('block min-w-0 flex-1 truncate typography-ui-label font-normal', isActive ? 'text-primary' : 'text-foreground')}>{renderHighlightedText(sessionTitle, normalizedSessionSearchQuery)}</div>
|
<div className={cn('block min-w-0 flex-1 truncate typography-ui-label font-normal', isActive ? 'text-primary' : 'text-foreground')}>{renderHighlightedText(sessionTitle, normalizedSessionSearchQuery)}</div>
|
||||||
{mobileVariant ? <span className="ml-2 flex-shrink-0 text-[0.72rem] text-muted-foreground/75">{sessionCompactUpdatedLabel}</span> : null}
|
{alwaysShowActions ? <span className="ml-2 flex-shrink-0 text-[0.72rem] text-muted-foreground/75">{sessionCompactUpdatedLabel}</span> : null}
|
||||||
{!mobileVariant ? (
|
{!alwaysShowActions ? (
|
||||||
<div className="relative ml-1 flex h-4 min-w-4 flex-shrink-0 items-center justify-end">
|
<div className="relative ml-1 flex h-4 min-w-4 flex-shrink-0 items-center justify-end">
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
'whitespace-nowrap text-right text-[0.72rem] text-muted-foreground/75 transition-opacity duration-150',
|
'whitespace-nowrap text-right text-[0.72rem] text-muted-foreground/75 transition-opacity duration-150',
|
||||||
@@ -805,7 +808,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex min-w-0 flex-1 cursor-pointer 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 disabled:cursor-not-allowed transition-[padding]',
|
'flex min-w-0 flex-1 cursor-pointer 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 disabled:cursor-not-allowed transition-[padding]',
|
||||||
isTouchPressed && 'bg-interactive-hover/70',
|
isTouchPressed && 'bg-interactive-hover/70',
|
||||||
mobileVariant
|
alwaysShowActions
|
||||||
? (isVSCode ? revealPaddingClass : 'pr-7')
|
? (isVSCode ? revealPaddingClass : 'pr-7')
|
||||||
: revealPaddingClass
|
: revealPaddingClass
|
||||||
)}
|
)}
|
||||||
@@ -844,7 +847,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
'absolute right-0 top-1/2 z-10 -translate-y-1/2 transition-opacity',
|
'absolute right-0 top-1/2 z-10 -translate-y-1/2 transition-opacity',
|
||||||
isMenuOpen
|
isMenuOpen
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: (mobileVariant && !isVSCode)
|
: (alwaysShowActions && !isVSCode)
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: cn('opacity-0', revealOnHoverClass),
|
: cn('opacity-0', revealOnHoverClass),
|
||||||
)}>
|
)}>
|
||||||
@@ -854,7 +857,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex 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',
|
'inline-flex 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',
|
||||||
isMinimalMode && !mobileVariant
|
isMinimalMode && !alwaysShowActions
|
||||||
? (isMenuOpen
|
? (isMenuOpen
|
||||||
? 'h-4 w-4 opacity-100'
|
? 'h-4 w-4 opacity-100'
|
||||||
: cn('h-4 w-4 opacity-0', revealOnHoverClass))
|
: cn('h-4 w-4 opacity-0', revealOnHoverClass))
|
||||||
@@ -866,7 +869,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
onClick={handleMenuTriggerClick}
|
onClick={handleMenuTriggerClick}
|
||||||
onKeyDown={(event) => event.stopPropagation()}
|
onKeyDown={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<RiMore2Line className={cn(isMinimalMode && !mobileVariant ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
|
<RiMore2Line className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
{sessionMenuContent}
|
{sessionMenuContent}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type Props = {
|
|||||||
isDesktopShellRuntime: boolean;
|
isDesktopShellRuntime: boolean;
|
||||||
stuckProjectHeaders: Set<string>;
|
stuckProjectHeaders: Set<string>;
|
||||||
mobileVariant: boolean;
|
mobileVariant: boolean;
|
||||||
|
alwaysShowActions: boolean;
|
||||||
toggleProject: (id: string) => void;
|
toggleProject: (id: string) => void;
|
||||||
setActiveProjectIdOnly: (id: string) => void;
|
setActiveProjectIdOnly: (id: string) => void;
|
||||||
setActiveMainTab: (tab: 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files') => 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)}
|
isStuck={props.stuckProjectHeaders.has(projectKey)}
|
||||||
hideDirectoryControls={props.hideDirectoryControls}
|
hideDirectoryControls={props.hideDirectoryControls}
|
||||||
mobileVariant={props.mobileVariant}
|
mobileVariant={props.mobileVariant}
|
||||||
|
alwaysShowActions={props.alwaysShowActions}
|
||||||
onToggle={() => props.toggleProject(projectKey)}
|
onToggle={() => props.toggleProject(projectKey)}
|
||||||
onNewSession={() => {
|
onNewSession={() => {
|
||||||
if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey);
|
if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface SortableProjectItemProps {
|
|||||||
isStuck: boolean;
|
isStuck: boolean;
|
||||||
hideDirectoryControls: boolean;
|
hideDirectoryControls: boolean;
|
||||||
mobileVariant: boolean;
|
mobileVariant: boolean;
|
||||||
|
alwaysShowActions: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
onNewSession: () => void;
|
onNewSession: () => void;
|
||||||
onNewWorktreeSession?: () => void;
|
onNewWorktreeSession?: () => void;
|
||||||
@@ -70,7 +71,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
isDesktopShell,
|
isDesktopShell,
|
||||||
isStuck,
|
isStuck,
|
||||||
hideDirectoryControls,
|
hideDirectoryControls,
|
||||||
mobileVariant,
|
alwaysShowActions,
|
||||||
onToggle,
|
onToggle,
|
||||||
onNewSession,
|
onNewSession,
|
||||||
onNewWorktreeSession,
|
onNewWorktreeSession,
|
||||||
@@ -176,17 +177,23 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
className={cn(
|
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]',
|
'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
|
isRepo && !hideDirectoryControls
|
||||||
? (mobileVariant ? 'pr-20' : 'pr-7 group-hover/project:pr-20 group-focus-within/project:pr-20')
|
? (alwaysShowActions ? '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-14' : 'pr-7 group-hover/project:pr-14 group-focus-within/project:pr-14'),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||||
<span className="hidden h-3.5 w-3.5 items-center justify-center text-muted-foreground group-hover/project:inline-flex group-focus-within/project:inline-flex">
|
<span className={cn(
|
||||||
|
'h-3.5 w-3.5 items-center justify-center text-muted-foreground',
|
||||||
|
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/project:inline-flex group-focus-within/project:inline-flex',
|
||||||
|
)}>
|
||||||
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||||
</span>
|
</span>
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
<span
|
<span
|
||||||
className="inline-flex h-3.5 w-3.5 items-center justify-center overflow-hidden rounded-[3px] group-hover/project:hidden group-focus-within/project:hidden"
|
className={cn(
|
||||||
|
'h-3.5 w-3.5 items-center justify-center overflow-hidden rounded-[3px]',
|
||||||
|
alwaysShowActions ? 'hidden' : 'inline-flex group-hover/project:hidden group-focus-within/project:hidden',
|
||||||
|
)}
|
||||||
style={projectIconBackground ? { backgroundColor: projectIconBackground } : undefined}
|
style={projectIconBackground ? { backgroundColor: projectIconBackground } : undefined}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -198,9 +205,9 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
) : ProjectIcon ? (
|
) : ProjectIcon ? (
|
||||||
<ProjectIcon className="h-3.5 w-3.5 group-hover/project:hidden group-focus-within/project:hidden" style={iconColor ? { color: iconColor } : undefined} />
|
<ProjectIcon className={cn('h-3.5 w-3.5', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
|
||||||
) : (
|
) : (
|
||||||
<RiFolderLine className="h-3.5 w-3.5 text-muted-foreground/80 group-hover/project:hidden group-focus-within/project:hidden" style={iconColor ? { color: iconColor } : undefined} />
|
<RiFolderLine className={cn('h-3.5 w-3.5 text-muted-foreground/80', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
@@ -231,7 +238,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
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',
|
'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')}
|
aria-label={t('sessions.sidebar.project.actions.newWorktree')}
|
||||||
>
|
>
|
||||||
@@ -255,7 +262,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
'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',
|
'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
|
isMenuOpen
|
||||||
? 'opacity-100 pointer-events-auto'
|
? 'opacity-100 pointer-events-auto'
|
||||||
: mobileVariant
|
: alwaysShowActions
|
||||||
? 'opacity-100'
|
? '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',
|
: '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<SortableProjectItemProps> = ({
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
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',
|
'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
|
aria-label={isRepo
|
||||||
? t('sessions.sidebar.project.actions.newDraftSession')
|
? t('sessions.sidebar.project.actions.newDraftSession')
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { RiCloseLine } from '@remixicon/react';
|
|||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
|
|
||||||
export type SortableTabsStripItem = {
|
export type SortableTabsStripItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -99,6 +100,8 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
const { isTablet } = useDeviceInfo();
|
||||||
|
const alwaysShowCloseControls = isMobile || isTablet;
|
||||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [overflow, setOverflow] = React.useState<{ left: boolean; right: boolean }>({ left: false, right: false });
|
const [overflow, setOverflow] = React.useState<{ left: boolean; right: boolean }>({ left: false, right: false });
|
||||||
const itemIDs = React.useMemo(() => items.map((item) => item.id), [items]);
|
const itemIDs = React.useMemo(() => items.map((item) => item.id), [items]);
|
||||||
@@ -435,12 +438,12 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
|||||||
<>
|
<>
|
||||||
{item.icon ? (
|
{item.icon ? (
|
||||||
<span className="relative flex h-4 w-4 shrink-0 items-center justify-center">
|
<span className="relative flex h-4 w-4 shrink-0 items-center justify-center">
|
||||||
<span className={cn('flex items-center justify-center transition-opacity', closeReplacesIcon && (isMobile ? 'opacity-0' : 'group-hover:opacity-0'))}>{item.icon}</span>
|
<span className={cn('flex items-center justify-center transition-opacity', closeReplacesIcon && (alwaysShowCloseControls ? 'opacity-0' : 'group-hover:opacity-0'))}>{item.icon}</span>
|
||||||
{closeReplacesIcon ? (
|
{closeReplacesIcon ? (
|
||||||
<span
|
<span
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={cn('absolute inset-0 z-20 flex items-center justify-center rounded-sm text-muted-foreground transition-opacity hover:text-foreground', isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100')}
|
className={cn('absolute inset-0 z-20 flex items-center justify-center rounded-sm text-muted-foreground transition-opacity hover:text-foreground', alwaysShowCloseControls ? 'opacity-100' : 'opacity-0 group-hover:opacity-100')}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
@@ -467,12 +470,12 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
|||||||
isActive ? 'text-[var(--primary-base)]' : 'text-muted-foreground'
|
isActive ? 'text-[var(--primary-base)]' : 'text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={cn('flex items-center justify-center transition-opacity', closeReplacesIcon && (isMobile ? 'opacity-0' : 'group-hover:opacity-0'))}>{item.icon}</span>
|
<span className={cn('flex items-center justify-center transition-opacity', closeReplacesIcon && (alwaysShowCloseControls ? 'opacity-0' : 'group-hover:opacity-0'))}>{item.icon}</span>
|
||||||
{closeReplacesIcon ? (
|
{closeReplacesIcon ? (
|
||||||
<span
|
<span
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={cn('absolute inset-0 z-20 flex items-center justify-center rounded-sm text-muted-foreground transition-opacity hover:text-foreground', isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100')}
|
className={cn('absolute inset-0 z-20 flex items-center justify-center rounded-sm text-muted-foreground transition-opacity hover:text-foreground', alwaysShowCloseControls ? 'opacity-100' : 'opacity-0 group-hover:opacity-100')}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ interface FileRowProps {
|
|||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
alwaysShowActions: boolean;
|
||||||
status?: FileStatus | null;
|
status?: FileStatus | null;
|
||||||
badge?: { modified: number; added: number } | null;
|
badge?: { modified: number; added: number } | null;
|
||||||
permissions: {
|
permissions: {
|
||||||
@@ -319,6 +320,7 @@ const FileRow: React.FC<FileRowProps> = ({
|
|||||||
isExpanded,
|
isExpanded,
|
||||||
isActive,
|
isActive,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
alwaysShowActions,
|
||||||
status,
|
status,
|
||||||
badge,
|
badge,
|
||||||
permissions,
|
permissions,
|
||||||
@@ -395,8 +397,7 @@ const FileRow: React.FC<FileRowProps> = ({
|
|||||||
{(canRename || canCreateFile || canCreateFolder || canDelete || canReveal) && (
|
{(canRename || canCreateFile || canCreateFolder || canDelete || canReveal) && (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute right-1 top-1/2 -translate-y-1/2",
|
"absolute right-1 top-1/2 -translate-y-1/2",
|
||||||
!isMobile && "opacity-0 focus-within:opacity-100 group-hover:opacity-100",
|
alwaysShowActions ? "opacity-100" : "opacity-0 focus-within:opacity-100 group-hover:opacity-100"
|
||||||
isMobile && "opacity-100"
|
|
||||||
)}>
|
)}>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={contextMenuPath === node.path}
|
open={contextMenuPath === node.path}
|
||||||
@@ -498,7 +499,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { files, runtime } = useRuntimeAPIs();
|
const { files, runtime } = useRuntimeAPIs();
|
||||||
const { currentTheme, availableThemes, lightThemeId, darkThemeId } = useThemeSystem();
|
const { currentTheme, availableThemes, lightThemeId, darkThemeId } = useThemeSystem();
|
||||||
const { isMobile, screenWidth } = useDeviceInfo();
|
const { isMobile, isTablet, screenWidth } = useDeviceInfo();
|
||||||
|
const alwaysShowActions = isMobile || isTablet;
|
||||||
const showHidden = useDirectoryShowHidden();
|
const showHidden = useDirectoryShowHidden();
|
||||||
const showGitignored = useFilesViewShowGitignored();
|
const showGitignored = useFilesViewShowGitignored();
|
||||||
|
|
||||||
@@ -1815,6 +1817,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
alwaysShowActions={alwaysShowActions}
|
||||||
status={!isDir ? getFileStatus(node.path) : undefined}
|
status={!isDir ? getFileStatus(node.path) : undefined}
|
||||||
badge={isDir ? getFolderBadge(node.path) : undefined}
|
badge={isDir ? getFolderBadge(node.path) : undefined}
|
||||||
permissions={fileRowPermissions}
|
permissions={fileRowPermissions}
|
||||||
@@ -2886,7 +2889,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-sm p-0.5 text-[var(--surface-muted-foreground)] hover:text-[var(--surface-foreground)]',
|
'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 })}
|
aria-label={t('filesView.editor.closeFileAria', { name: file.name })}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user