perf(ui): migrate icons to SVG sprite system

Replace @remixicon/react with a shared Icon component that renders
via <use href> references to a single hidden SVG sprite. This reduces
DOM node count by replacing inline SVGs with lightweight references.

- Create Icon component with sprite injection (packages/ui/src/components/icon/)
- Migrate all 164 files from @remixicon/react to Icon component
- Auto-generate sprite data from remixicon bundle (scripts/generate-icon-sprite.mjs)
- Add bun run icons:generate to package.json
- Move @remixicon/react to devDependencies
- Add icon usage instructions to theme-system skill
This commit is contained in:
Bohdan Triapitsyn
2026-05-13 13:26:35 +03:00
parent b4cbd7f0d6
commit 14357257ae
173 changed files with 2342 additions and 2227 deletions
@@ -9,16 +9,7 @@ import {
import { Input } from '@/components/ui/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { toast } from '@/components/ui';
import {
RiCheckLine,
RiCloseLine,
RiDeleteBinLine,
RiGitBranchLine,
RiLoader4Line,
RiPencilLine,
RiSearchLine,
RiSplitCellsHorizontal,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { deleteGitBranch, getGitBranches, git, renameBranch } from '@/lib/gitApi';
import type { GitBranch, GitWorktreeInfo } from '@/lib/api/types';
@@ -304,7 +295,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col overflow-hidden gap-3">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<RiGitBranchLine className="h-5 w-5" />
<Icon name="git-branch" className="h-5 w-5" />
{t('branchPickerDialog.title')}
</DialogTitle>
<DialogDescription>
@@ -313,7 +304,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
</DialogHeader>
<div className="relative flex-shrink-0">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Icon name="search" className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('branchPickerDialog.search.placeholder')}
value={searchQuery}
@@ -371,7 +362,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
key={branchName}
className="flex items-center gap-2 px-2.5 py-1.5 hover:bg-interactive-hover/30 rounded-md overflow-hidden"
>
<RiGitBranchLine className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<Icon name="git-branch" className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-1.5 min-w-0">
@@ -445,9 +436,9 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
aria-label={t('branchPickerDialog.actions.createWorktreeAria')}
>
{isCreatingWorktree ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<RiSplitCellsHorizontal className="h-4 w-4" />
<Icon name="split-cells-horizontal" className="h-4 w-4" />
)}
</button>
</TooltipTrigger>
@@ -465,7 +456,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
aria-label={t('branchPickerDialog.actions.renameAria')}
>
<RiPencilLine className="h-4 w-4" />
<Icon name="pencil" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="left">
@@ -489,9 +480,9 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
aria-label={hasAttachedWorktree ? t('branchPickerDialog.actions.deleteWorktreeAria') : t('branchPickerDialog.actions.deleteAria')}
>
{isDeleting ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
)}
</button>
</TooltipTrigger>
@@ -520,9 +511,9 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
aria-label={t('branchPickerDialog.actions.confirmRenameAria')}
>
{isRenaming ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<RiCheckLine className="h-4 w-4" />
<Icon name="check" className="h-4 w-4" />
)}
</button>
<button
@@ -531,7 +522,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors"
aria-label={t('branchPickerDialog.actions.cancelRenameAria')}
>
<RiCloseLine className="h-4 w-4" />
<Icon name="close" className="h-4 w-4" />
</button>
</div>
) : null}
@@ -557,9 +548,9 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
aria-label={t('branchPickerDialog.actions.confirmDeleteAria')}
>
{isDeleting ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<RiCheckLine className="h-4 w-4" />
<Icon name="check" className="h-4 w-4" />
)}
</button>
<button
@@ -568,7 +559,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors"
aria-label={t('branchPickerDialog.actions.cancelDeleteAria')}
>
<RiCloseLine className="h-4 w-4" />
<Icon name="close" className="h-4 w-4" />
</button>
</div>
) : null}
@@ -1,8 +1,8 @@
import React from 'react';
import { RiFolderLine, RiRefreshLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { opencodeClient, type FilesystemEntry } from '@/lib/opencode/client';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { Icon } from "@/components/icon/Icon";
interface DirectoryAutocompleteProps {
inputValue: string;
@@ -280,7 +280,7 @@ export const DirectoryAutocomplete = React.forwardRef<DirectoryAutocompleteHandl
>
{loading ? (
<div className="flex items-center justify-center py-3">
<RiRefreshLine className="h-4 w-4 animate-spin text-muted-foreground" />
<Icon name="refresh" className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<div className="overflow-y-auto py-1">
@@ -297,7 +297,7 @@ export const DirectoryAutocomplete = React.forwardRef<DirectoryAutocompleteHandl
onClick={() => { handleSelectSuggestion(entry); onClose(); }}
onMouseEnter={() => setSelectedIndex(index)}
>
<RiFolderLine className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<Icon name="folder" className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<span className="truncate">{entry.name}</span>
</div>
);
@@ -16,18 +16,9 @@ import { useFileSystemAccess } from '@/hooks/useFileSystemAccess';
import { cn } from '@/lib/utils';
import { toast } from '@/components/ui';
import { IdentityDropdown } from '@/components/views/git/GitHeader';
import {
RiArrowDownSLine,
RiArrowLeftSLine,
RiArrowUpSLine,
RiCheckboxBlankLine,
RiCheckboxLine,
RiCornerDownLeftLine,
RiFolder6Line,
RiFolderAddLine,
} from '@remixicon/react';
import { useDeviceInfo } from '@/lib/device';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { Icon } from "@/components/icon/Icon";
import { opencodeClient } from '@/lib/opencode/client';
import {
setDirectoryShowHidden,
@@ -523,7 +514,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
onClick={() => setDirectoryShowHidden(!showHidden)}
className="flex flex-shrink-0 items-center gap-2 rounded-lg px-2 py-1 typography-meta text-muted-foreground transition-colors hover:bg-interactive-hover/40"
>
{showHidden ? <RiCheckboxLine className="h-4 w-4 text-primary" /> : <RiCheckboxBlankLine className="h-4 w-4" />}
{showHidden ? <Icon name="checkbox" className="h-4 w-4 text-primary" /> : <Icon name="checkbox-blank" className="h-4 w-4" />}
{t('directoryExplorerDialog.toggle.showHidden')}
</button>
);
@@ -552,7 +543,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
</div>
) : null}
<div className="relative">
<RiFolderAddLine className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/80" />
<Icon name="folder-add" className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/80" />
<Input
ref={inputRef}
value={query}
@@ -626,9 +617,9 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
)}
>
{row.type === 'up' ? (
<RiArrowLeftSLine className="h-4 w-4 flex-shrink-0 text-muted-foreground/80" />
<Icon name="arrow-left-s" className="h-4 w-4 flex-shrink-0 text-muted-foreground/80" />
) : (
<RiFolder6Line className="h-4 w-4 flex-shrink-0 text-muted-foreground/80" />
<Icon name="folder-6" className="h-4 w-4 flex-shrink-0 text-muted-foreground/80" />
)}
<span className="flex min-w-0 flex-1 items-center gap-1.5">
<span className="truncate typography-ui-label text-foreground">{row.name}</span>
@@ -657,17 +648,17 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
const footerHints = (
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 typography-micro text-muted-foreground">
<span className="inline-flex items-center gap-1">
<RiArrowUpSLine className="h-3.5 w-3.5" />
<RiArrowDownSLine className="-ml-1 h-3.5 w-3.5" />
<Icon name="arrow-up-s" className="h-3.5 w-3.5" />
<Icon name="arrow-down-s" className="-ml-1 h-3.5 w-3.5" />
{t('directoryExplorerDialog.footer.navigate')}
</span>
<span className="inline-flex items-center gap-1">
<RiCornerDownLeftLine className="h-3.5 w-3.5" />
<Icon name="corner-down-left" className="h-3.5 w-3.5" />
{t('directoryExplorerDialog.footer.select')}
</span>
<span className="inline-flex items-center gap-1">
<span>{submitModifierLabel}</span>
<RiCornerDownLeftLine className="h-3.5 w-3.5" />
<Icon name="corner-down-left" className="h-3.5 w-3.5" />
{t('directoryExplorerDialog.footer.add')}
</span>
</div>
@@ -8,7 +8,6 @@ import {
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import { RiAddLine, RiArrowDownSLine, RiArrowRightSLine, RiCheckLine, RiCloseLine, RiFolder6Line, RiPushpin2Line, RiPushpinLine } from '@remixicon/react';
import { cn, formatPathForDisplay } from '@/lib/utils';
import { opencodeClient } from '@/lib/opencode/client';
import { useDeviceInfo } from '@/lib/device';
@@ -16,6 +15,7 @@ import type { DesktopSettings } from '@/lib/desktop';
import { updateDesktopSettings } from '@/lib/persistence';
import { useFileSystemAccess } from '@/hooks/useFileSystemAccess';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
interface DirectoryItem {
@@ -695,9 +695,9 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className={cn("hover:bg-interactive-hover rounded", isMobile ? "p-0.5" : "p-0.5")}
>
{isExpanded ? (
<RiArrowDownSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
<Icon name="arrow-down-s" className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
) : (
<RiArrowRightSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
<Icon name="arrow-right-s" className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
)}
</button>
)}
@@ -728,7 +728,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
isInlineVariant ? (isSelected ? 'text-primary' : 'text-foreground') : 'text-foreground'
)}
>
<RiFolder6Line
<Icon name="folder-6"
className={cn(
'text-muted-foreground flex-shrink-0',
isMobile ? 'h-4 w-4' : 'h-3.5 w-3.5',
@@ -760,7 +760,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
)}
title={t('directoryTree.actions.createNewDirectory')}
>
<RiAddLine className={cn("text-muted-foreground", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
<Icon name="add" className={cn("text-muted-foreground", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
</button>
<button
@@ -776,9 +776,9 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
title={isPinned ? t('directoryTree.actions.unpinDirectory') : t('directoryTree.actions.pinDirectory')}
>
{isPinned ? (
<RiPushpin2Line className={cn("text-primary", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
<Icon name="pushpin-2" className={cn("text-primary", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
) : (
<RiPushpinLine className={cn("text-muted-foreground", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
<Icon name="pushpin" className={cn("text-muted-foreground", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
)}
</button>
</>
@@ -809,7 +809,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
style={{ paddingLeft: `${(level + 1) * 14 + 6}px` }}
>
<div className="w-4" />
<RiFolder6Line className="h-3.5 w-3.5 text-muted-foreground" />
<Icon name="folder-6" className="h-3.5 w-3.5 text-muted-foreground" />
<Input
ref={inputRef}
value={newDirName}
@@ -838,7 +838,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className="p-1 hover:bg-interactive-hover rounded"
title={t('directoryTree.actions.createDirectory')}
>
<RiCheckLine className="h-3 w-3 text-green-600" />
<Icon name="check" className="h-3 w-3 text-green-600" />
</button>
<button
onClick={(e) => {
@@ -849,7 +849,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className="p-1 hover:bg-interactive-hover rounded"
title={t('directoryTree.actions.cancel')}
>
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
<Icon name="close" className="h-3 w-3 text-muted-foreground" />
</button>
</div>
)}
@@ -881,9 +881,9 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className="p-0.5 hover:bg-interactive-hover rounded"
>
{isExpanded ? (
<RiArrowDownSLine className="h-3 w-3" />
<Icon name="arrow-down-s" className="h-3 w-3" />
) : (
<RiArrowRightSLine className="h-3 w-3" />
<Icon name="arrow-right-s" className="h-3 w-3" />
)}
</button>
)}
@@ -898,7 +898,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
style={{ paddingLeft: `${(level + 1) * 12 + 8}px` }}
>
<div className="w-4" />
<RiFolder6Line className="h-3.5 w-3.5 text-muted-foreground" />
<Icon name="folder-6" className="h-3.5 w-3.5 text-muted-foreground" />
<Input
ref={inputRef}
value={newDirName}
@@ -927,7 +927,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className="p-1 hover:bg-interactive-hover rounded"
title={t('directoryTree.actions.createDirectory')}
>
<RiCheckLine className="h-3 w-3 text-green-600" />
<Icon name="check" className="h-3 w-3 text-green-600" />
</button>
<button
onClick={(e) => {
@@ -938,7 +938,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className="p-1 hover:bg-interactive-hover rounded"
title={t('directoryTree.actions.cancel')}
>
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
<Icon name="close" className="h-3 w-3 text-muted-foreground" />
</button>
</div>
)}
@@ -968,9 +968,9 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className={cn("hover:bg-interactive-hover rounded", isMobile ? "p-0.5" : "p-0.5")}
>
{isExpanded ? (
<RiArrowDownSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
<Icon name="arrow-down-s" className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
) : (
<RiArrowRightSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
<Icon name="arrow-right-s" className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
)}
</button>
)}
@@ -996,7 +996,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
isInlineVariant ? (isSelected ? 'text-primary' : 'text-foreground') : 'text-foreground'
)}
>
<RiFolder6Line
<Icon name="folder-6"
className={cn(
'text-muted-foreground flex-shrink-0',
isMobile ? 'h-4 w-4' : 'h-3.5 w-3.5',
@@ -1027,9 +1027,9 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
title={isPinned ? t('directoryTree.actions.unpinDirectory') : t('directoryTree.actions.pinDirectory')}
>
{isPinned ? (
<RiPushpin2Line className={cn("text-primary", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
<Icon name="pushpin-2" className={cn("text-primary", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
) : (
<RiPushpinLine className={cn("text-muted-foreground", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
<Icon name="pushpin" className={cn("text-muted-foreground", isMobile ? "h-3.5 w-3.5" : "h-3 w-3")} />
)}
</button>
</>
@@ -1105,9 +1105,9 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className={cn("hover:bg-interactive-hover rounded flex-shrink-0", isMobile ? "p-0.5" : "p-0.5")}
>
{isExpanded ? (
<RiArrowDownSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
<Icon name="arrow-down-s" className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
) : (
<RiArrowRightSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
<Icon name="arrow-right-s" className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
)}
</button>
@@ -1124,7 +1124,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
isSelected ? 'text-primary' : 'text-foreground'
)}
>
<RiFolder6Line
<Icon name="folder-6"
className={cn(
'flex-shrink-0',
isMobile ? 'h-4 w-4' : 'h-3.5 w-3.5',
@@ -1151,7 +1151,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
)}
title={t('directoryTree.actions.unpinDirectory')}
>
<RiPushpin2Line className={cn("text-primary", isMobile ? "h-3.5 w-3.5" : "h-3.5 w-3.5")} />
<Icon name="pushpin-2" className={cn("text-primary", isMobile ? "h-3.5 w-3.5" : "h-3.5 w-3.5")} />
</button>
</div>
{isExpanded && children && children.map((child) => renderPinnedTreeItem(child, 1))}
@@ -1183,12 +1183,12 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
className="p-0.5 hover:bg-interactive-hover rounded flex-shrink-0"
>
{isExpanded ? (
<RiArrowDownSLine className="h-3 w-3" />
<Icon name="arrow-down-s" className="h-3 w-3" />
) : (
<RiArrowRightSLine className="h-3 w-3" />
<Icon name="arrow-right-s" className="h-3 w-3" />
)}
</button>
<RiFolder6Line className="h-3.5 w-3.5 text-muted-foreground mt-0.5 flex-shrink-0" />
<Icon name="folder-6" className="h-3.5 w-3.5 text-muted-foreground mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="typography-ui-label font-medium">{name}</div>
<div className="typography-meta text-muted-foreground">
@@ -1207,7 +1207,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
)}
title={t('directoryTree.actions.unpinDirectory')}
>
<RiPushpin2Line className="h-3 w-3 text-primary" />
<Icon name="pushpin-2" className="h-3 w-3 text-primary" />
</button>
</DropdownMenuItem>
{isExpanded && children && (
@@ -1238,9 +1238,9 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
)}
>
{isPinnedExpanded ? (
<RiArrowDownSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
<Icon name="arrow-down-s" className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
) : (
<RiArrowRightSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
<Icon name="arrow-right-s" className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
)}
<span>{t('directoryTree.section.pinned')}</span>
<span className="ml-auto typography-micro text-muted-foreground/60 normal-case tracking-normal">
@@ -1302,12 +1302,12 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
aria-label={t('directoryTree.actions.selectWorkingDirectoryAria')}
>
<span className="flex items-center gap-1.5 min-w-0 flex-1">
<RiFolder6Line className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
<Icon name="folder-6" className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
<span className="truncate" title={currentPath}>
{formatPathForDisplay(currentPath, homeDirectory)}
</span>
</span>
<RiArrowDownSLine className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[350px]">
@@ -9,16 +9,6 @@ import {
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
RiGithubLine,
RiLoader4Line,
RiSearchLine,
RiErrorWarningLine,
RiCheckLine,
RiGitPullRequestLine,
RiGitBranchLine,
RiCloseLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useProjectsStore } from '@/stores/useProjectsStore';
@@ -27,6 +17,7 @@ import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import { validateWorktreeCreate } from '@/lib/worktrees/worktreeManager';
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { Icon } from "@/components/icon/Icon";
import type {
GitHubIssue,
GitHubIssueSummary,
@@ -299,7 +290,7 @@ export function GitHubIntegrationDialog({
<>
{!isGitHubConnected ? (
<div className="flex-1 flex flex-col items-center justify-center p-8 gap-4">
<RiGithubLine className="h-12 w-12 text-muted-foreground" />
<Icon name="github" className="h-12 w-12 text-muted-foreground" />
<div className="text-center">
<p className="typography-ui-label text-foreground">{t('session.githubIntegration.connect.title')}</p>
<p className="typography-small text-muted-foreground mt-1">
@@ -312,7 +303,7 @@ export function GitHubIntegrationDialog({
<>
{/* Search */}
<div className="relative mt-2">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Icon name="search" className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
@@ -329,7 +320,7 @@ export function GitHubIntegrationDialog({
{/* Loading */}
{loading && (
<div className="flex items-center justify-center h-full">
<RiLoader4Line className="h-5 w-5 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
)}
@@ -337,7 +328,7 @@ export function GitHubIntegrationDialog({
{error && (
<div className="flex items-center justify-center h-full">
<div className="flex items-center gap-2 p-2 rounded-md bg-destructive/10 text-destructive">
<RiErrorWarningLine className="h-4 w-4" />
<Icon name="error-warning" className="h-4 w-4" />
<span className="typography-small">{error}</span>
</div>
</div>
@@ -391,7 +382,7 @@ export function GitHubIntegrationDialog({
)}
{loadingMore && (
<div className="flex items-center justify-center py-2">
<RiLoader4Line className="h-4 w-4 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -463,7 +454,7 @@ export function GitHubIntegrationDialog({
)}
{loadingMore && (
<div className="flex items-center justify-center py-2">
<RiLoader4Line className="h-4 w-4 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -489,7 +480,7 @@ export function GitHubIntegrationDialog({
{/* Selected Issue/PR display - hidden on mobile (shown in header instead) */}
{!isMobile && (selectedIssue || selectedPr) && (
<div className="flex items-center gap-2 px-2 h-8 rounded-md bg-muted/50 border border-border/50">
<RiCheckLine className="h-3.5 w-3.5 text-status-success shrink-0" />
<Icon name="check" className="h-3.5 w-3.5 text-status-success shrink-0" />
<span className="typography-small truncate max-w-[150px]">
{selectedIssue
? t('session.githubIntegration.selected.issueNumber', { number: selectedIssue.number })
@@ -499,7 +490,7 @@ export function GitHubIntegrationDialog({
onClick={handleClear}
className="text-muted-foreground hover:text-foreground shrink-0 p-0.5 rounded hover:bg-muted transition-colors"
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
</div>
)}
@@ -562,8 +553,8 @@ export function GitHubIntegrationDialog({
<div className="w-full">
<SortableTabsStrip
items={[
{ id: 'issues', label: t('session.githubIntegration.tabs.issues'), icon: <RiGitBranchLine className="h-3.5 w-3.5" /> },
{ id: 'prs', label: t('session.githubIntegration.tabs.pullRequests'), icon: <RiGitPullRequestLine className="h-3.5 w-3.5" /> },
{ id: 'issues', label: t('session.githubIntegration.tabs.issues'), icon: <Icon name="git-branch" className="h-3.5 w-3.5" /> },
{ id: 'prs', label: t('session.githubIntegration.tabs.pullRequests'), icon: <Icon name="git-pull-request" className="h-3.5 w-3.5" /> },
]}
activeId={activeTab}
onSelect={(id) => {
@@ -578,7 +569,7 @@ export function GitHubIntegrationDialog({
{/* Selected Item Inline Display */}
{(selectedIssue || selectedPr) && (
<div className="flex items-center gap-2 px-2 py-1 rounded-md bg-muted/50 border border-border/50">
<RiCheckLine className="h-3.5 w-3.5 text-status-success shrink-0" />
<Icon name="check" className="h-3.5 w-3.5 text-status-success shrink-0" />
<span className="typography-small truncate flex-1">
{selectedIssue
? t('session.githubIntegration.selected.issueNumber', { number: selectedIssue.number })
@@ -588,7 +579,7 @@ export function GitHubIntegrationDialog({
onClick={handleClear}
className="text-muted-foreground hover:text-foreground shrink-0 p-0.5 rounded hover:bg-muted transition-colors"
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
</div>
)}
@@ -603,7 +594,7 @@ export function GitHubIntegrationDialog({
<DialogHeader className="flex flex-row items-center justify-between">
<div className="flex items-center gap-3">
<DialogTitle className="flex items-center gap-2 shrink-0">
<RiGithubLine className="h-5 w-5" />
<Icon name="github" className="h-5 w-5" />
{t('session.githubIntegration.title')}
</DialogTitle>
@@ -611,8 +602,8 @@ export function GitHubIntegrationDialog({
<div className="w-[220px]">
<SortableTabsStrip
items={[
{ id: 'issues', label: t('session.githubIntegration.tabs.issues'), icon: <RiGitBranchLine className="h-3.5 w-3.5" /> },
{ id: 'prs', label: t('session.githubIntegration.tabs.pullRequests'), icon: <RiGitPullRequestLine className="h-3.5 w-3.5" /> },
{ id: 'issues', label: t('session.githubIntegration.tabs.issues'), icon: <Icon name="git-branch" className="h-3.5 w-3.5" /> },
{ id: 'prs', label: t('session.githubIntegration.tabs.pullRequests'), icon: <Icon name="git-pull-request" className="h-3.5 w-3.5" /> },
]}
activeId={activeTab}
onSelect={(id) => {
@@ -10,14 +10,7 @@ import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { toast } from '@/components/ui';
import {
RiCheckboxBlankLine,
RiCheckboxLine,
RiExternalLinkLine,
RiGithubLine,
RiLoader4Line,
RiSearchLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useProjectsStore } from '@/stores/useProjectsStore';
@@ -499,7 +492,7 @@ export function GitHubIssuePickerDialog({
const content = (
<>
<div className="relative mt-2">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Icon name="search" className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('session.githubIssuePicker.searchPlaceholder')}
value={query}
@@ -519,7 +512,7 @@ export function GitHubIssuePickerDialog({
{isLoading ? (
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
{t('session.githubIssuePicker.loading.issues')}
</div>
) : null}
@@ -553,7 +546,7 @@ export function GitHubIssuePickerDialog({
</p>
<div className="flex-shrink-0 h-5 flex items-center mr-2">
{startingIssueNumber === directNumber ? (
<RiLoader4Line className="h-4 w-4 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="h-4 w-4 animate-spin text-muted-foreground" />
) : null}
</div>
</div>
@@ -588,7 +581,7 @@ export function GitHubIssuePickerDialog({
<div className="flex-shrink-0 h-5 flex items-center mr-2">
{startingIssueNumber === issue.number ? (
<RiLoader4Line className="h-4 w-4 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="h-4 w-4 animate-spin text-muted-foreground" />
) : (
<a
href={issue.url}
@@ -601,7 +594,7 @@ export function GitHubIssuePickerDialog({
onClick={(e) => e.stopPropagation()}
aria-label={t('session.githubIssuePicker.actions.openInGitHubAria')}
>
<RiExternalLinkLine className="h-4 w-4" />
<Icon name="external-link" className="h-4 w-4" />
</a>
)}
</div>
@@ -621,7 +614,7 @@ export function GitHubIssuePickerDialog({
>
{isLoadingMore ? (
<span className="inline-flex items-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
{t('session.githubIssuePicker.loading.more')}
</span>
) : (
@@ -660,9 +653,9 @@ export function GitHubIssuePickerDialog({
className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
{createInWorktree ? (
<RiCheckboxLine className="h-4 w-4 text-primary" />
<Icon name="checkbox" className="h-4 w-4 text-primary" />
) : (
<RiCheckboxBlankLine className="h-4 w-4" />
<Icon name="checkbox-blank" className="h-4 w-4" />
)}
</button>
<span className="typography-meta text-muted-foreground">{t('session.githubIssuePicker.actions.createInWorktree')}</span>
@@ -673,7 +666,7 @@ export function GitHubIssuePickerDialog({
{repoUrl ? (
<Button variant="outline" size="sm" asChild>
<a href={repoUrl} target="_blank" rel="noopener noreferrer">
<RiExternalLinkLine className="size-4" />
<Icon name="external-link" className="size-4" />
{t('session.githubIssuePicker.actions.openRepo')}
</a>
</Button>
@@ -714,7 +707,7 @@ export function GitHubIssuePickerDialog({
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<RiGithubLine className="h-5 w-5" />
<Icon name="github" className="h-5 w-5" />
{title}
</DialogTitle>
<DialogDescription>
@@ -11,12 +11,7 @@ import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { toast } from '@/components/ui';
import {
RiGithubLine,
RiLoader4Line,
RiSearchLine,
RiExternalLinkLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useProjectsStore } from '@/stores/useProjectsStore';
@@ -269,7 +264,7 @@ export function GitHubPrPickerDialog({
<>
<div className="mt-2 flex items-center gap-3">
<div className="relative flex-1 min-w-0">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Icon name="search" className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('session.githubPrPicker.searchPlaceholder')}
value={query}
@@ -306,7 +301,7 @@ export function GitHubPrPickerDialog({
{isLoading ? (
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
{t('session.githubPrPicker.loading.pullRequests')}
</div>
) : null}
@@ -340,7 +335,7 @@ export function GitHubPrPickerDialog({
</p>
<div className="flex-shrink-0 h-5 flex items-center mr-2">
{loadingPrNumber === directNumber ? (
<RiLoader4Line className="h-4 w-4 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="h-4 w-4 animate-spin text-muted-foreground" />
) : null}
</div>
</div>
@@ -374,7 +369,7 @@ export function GitHubPrPickerDialog({
<div className="flex-shrink-0 h-5 flex items-center mr-2">
{loadingPrNumber === pr.number ? (
<RiLoader4Line className="h-4 w-4 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="h-4 w-4 animate-spin text-muted-foreground" />
) : (
<a
href={pr.url}
@@ -387,7 +382,7 @@ export function GitHubPrPickerDialog({
onClick={(e) => e.stopPropagation()}
aria-label={t('session.githubPrPicker.actions.openInGitHubAria')}
>
<RiExternalLinkLine className="h-4 w-4" />
<Icon name="external-link" className="h-4 w-4" />
</a>
)}
</div>
@@ -407,7 +402,7 @@ export function GitHubPrPickerDialog({
>
{isLoadingMore ? (
<span className="inline-flex items-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
{t('session.githubPrPicker.loading.more')}
</span>
) : (
@@ -446,7 +441,7 @@ export function GitHubPrPickerDialog({
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<RiGithubLine className="h-5 w-5" />
<Icon name="github" className="h-5 w-5" />
{title}
</DialogTitle>
<DialogDescription>
@@ -23,18 +23,6 @@ import {
CommandList,
CommandSeparator,
} from '@/components/ui/command';
import {
RiGitBranchLine,
RiGitRepositoryLine,
RiGithubLine,
RiLoader4Line,
RiRefreshLine,
RiErrorWarningLine,
RiCheckLine,
RiExternalLinkLine,
RiCloseLine,
RiArrowDownSLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
@@ -58,6 +46,7 @@ import { useGitBranches, useGitStore, useGitLoadingBranches } from '@/stores/use
import { GitHubIntegrationDialog } from './GitHubIntegrationDialog';
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { Icon } from "@/components/icon/Icon";
import type {
GitHubIssue,
GitHubIssueComment,
@@ -1037,7 +1026,7 @@ export function NewWorktreeDialog({
<div className={cn('flex items-center gap-1.5 text-destructive', isMobile ? 'w-full justify-center order-first' : 'mr-auto')}>
{validation.touched && (validation.branchError || validation.worktreeError) && (
<>
<RiErrorWarningLine className="h-3.5 w-3.5" />
<Icon name="error-warning" className="h-3.5 w-3.5" />
<span className="typography-micro">
{validation.branchError || validation.worktreeError}
</span>
@@ -1062,7 +1051,7 @@ export function NewWorktreeDialog({
disabled={!canCreate || isCreating}
className={cn('gap-1.5', isMobile && 'flex-1')}
>
{isCreating && <RiLoader4Line className="h-3.5 w-3.5 animate-spin" />}
{isCreating && <Icon name="loader-4" className="h-3.5 w-3.5 animate-spin" />}
{isCreating ? t('session.newWorktree.actions.creating') : t('session.newWorktree.actions.createWorktree')}
</Button>
</div>
@@ -1082,8 +1071,8 @@ export function NewWorktreeDialog({
<div className="w-full mb-4">
<SortableTabsStrip
items={[
{ id: 'new-branch', label: t('session.newWorktree.mode.newBranch'), icon: <RiGitBranchLine className="h-3.5 w-3.5" /> },
{ id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: <RiGitRepositoryLine className="h-3.5 w-3.5" /> },
{ id: 'new-branch', label: t('session.newWorktree.mode.newBranch'), icon: <Icon name="git-branch" className="h-3.5 w-3.5" /> },
{ id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: <Icon name="git-repository" className="h-3.5 w-3.5" /> },
]}
activeId={mode}
onSelect={(id) => handleModeChange(id as Mode)}
@@ -1110,7 +1099,7 @@ export function NewWorktreeDialog({
<span className={existingBranchState.selectedBranch ? 'text-foreground' : 'text-muted-foreground'}>
{existingBranchState.selectedBranch || t('session.newWorktree.chooseBranch')}
</span>
<RiGitBranchLine className="h-4 w-4 text-muted-foreground" />
<Icon name="git-branch" className="h-4 w-4 text-muted-foreground" />
</Button>
<Button
variant="ghost"
@@ -1120,7 +1109,7 @@ export function NewWorktreeDialog({
disabled={!canFetchBranches || isLoadingBranches}
title={t('session.newWorktree.fetchBranches')}
>
{isLoadingBranches ? <RiLoader4Line className="size-4 animate-spin" /> : <RiRefreshLine className="size-4" />}
{isLoadingBranches ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="refresh" className="size-4" />}
</Button>
</div>
@@ -1266,7 +1255,7 @@ export function NewWorktreeDialog({
onClick={() => setGithubDialogOpen(true)}
className="gap-1.5 h-7"
>
<RiGithubLine className="size-4 text-status-success" />
<Icon name="github" className="size-4 text-status-success" />
{newBranchState.linkedIssue || newBranchState.linkedPr ? t('session.newWorktree.actions.change') : t('session.newWorktree.actions.startFromGitHubIssuePr')}
</Button>
)}
@@ -1293,7 +1282,7 @@ export function NewWorktreeDialog({
/>
{newBranchState.linkedPr && (
<div className="flex items-center gap-1.5 text-muted-foreground">
<RiCheckLine className="h-3.5 w-3.5 text-status-success" />
<Icon name="check" className="h-3.5 w-3.5 text-status-success" />
<span className="typography-micro">
{t('session.newWorktree.usingPrBranch', { branch: newBranchState.linkedPr.head })}
</span>
@@ -1301,7 +1290,7 @@ export function NewWorktreeDialog({
)}
{newBranchState.linkedIssue && !newBranchState.linkedPr && (
<div className="flex items-center gap-1.5 text-muted-foreground">
<RiCheckLine className="h-3.5 w-3.5 text-status-success" />
<Icon name="check" className="h-3.5 w-3.5 text-status-success" />
<span className="typography-micro">
{t('session.newWorktree.fromIssue', { number: newBranchState.linkedIssue.number, title: newBranchState.linkedIssue.title })}
</span>
@@ -1335,7 +1324,7 @@ export function NewWorktreeDialog({
)}
title={t('session.newWorktree.resetToMatchBranchName')}
>
<RiRefreshLine className="h-3 w-3" />
<Icon name="refresh" className="h-3 w-3" />
<span>{t('session.newWorktree.actions.reset')}</span>
</button>
)}
@@ -1380,7 +1369,7 @@ export function NewWorktreeDialog({
<span className={newBranchState.sourceBranch ? 'text-foreground' : 'text-muted-foreground'}>
{newBranchState.sourceBranch || t('session.newWorktree.selectSourceBranchPlaceholder')}
</span>
<RiGitBranchLine className="h-4 w-4 text-muted-foreground" />
<Icon name="git-branch" className="h-4 w-4 text-muted-foreground" />
</Button>
{newBranchState.sourceBranch && (
<div className="typography-micro text-muted-foreground">
@@ -1509,7 +1498,7 @@ export function NewWorktreeDialog({
<div className="mt-2 px-2 py-1.5 rounded bg-muted/30">
{/* Row 1: Type, number, title, actions */}
<div className="flex items-center gap-2">
<RiGithubLine className="h-3.5 w-3.5 text-status-success shrink-0" />
<Icon name="github" className="h-3.5 w-3.5 text-status-success shrink-0" />
{newBranchState.linkedIssue && (
<span className="typography-micro text-muted-foreground shrink-0">
@@ -1533,14 +1522,14 @@ export function NewWorktreeDialog({
className="text-muted-foreground hover:text-foreground shrink-0"
onClick={(e) => e.stopPropagation()}
>
<RiExternalLinkLine className="h-3 w-3" />
<Icon name="external-link" className="h-3 w-3" />
</a>
<button
onClick={handleClearLinkedItem}
className="text-muted-foreground hover:text-foreground shrink-0 p-0.5 rounded hover:bg-muted transition-colors"
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
</div>
@@ -1567,7 +1556,7 @@ export function NewWorktreeDialog({
<DialogHeader className="flex flex-row items-center justify-between">
<div className="flex items-center gap-3">
<DialogTitle className="flex items-center gap-2 shrink-0">
<RiGitBranchLine className="h-5 w-5" />
<Icon name="git-branch" className="h-5 w-5" />
{t('session.newWorktree.title')}
</DialogTitle>
@@ -1575,8 +1564,8 @@ export function NewWorktreeDialog({
<div className="w-[280px] shrink-0">
<SortableTabsStrip
items={[
{ id: 'new-branch', label: t('session.newWorktree.mode.newBranch'), icon: <RiGitBranchLine className="h-3.5 w-3.5" /> },
{ id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: <RiGitRepositoryLine className="h-3.5 w-3.5" /> },
{ id: 'new-branch', label: t('session.newWorktree.mode.newBranch'), icon: <Icon name="git-branch" className="h-3.5 w-3.5" /> },
{ id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: <Icon name="git-repository" className="h-3.5 w-3.5" /> },
]}
activeId={mode}
onSelect={(id) => handleModeChange(id as Mode)}
@@ -1602,7 +1591,7 @@ export function NewWorktreeDialog({
<span className={cn('truncate', existingBranchState.selectedBranch ? 'text-foreground' : 'text-muted-foreground')}>
{existingBranchState.selectedBranch || t('session.newWorktree.chooseBranch')}
</span>
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" sideOffset={6} portalToBody className="w-[min(42rem,calc(100vw-2rem))] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={existingBranchDropdownContentRef}>
@@ -1714,7 +1703,7 @@ export function NewWorktreeDialog({
disabled={!canFetchBranches || isLoadingBranches}
title={t('session.newWorktree.fetchBranches')}
>
{isLoadingBranches ? <RiLoader4Line className="size-4 animate-spin" /> : <RiRefreshLine className="size-4" />}
{isLoadingBranches ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="refresh" className="size-4" />}
</Button>
</div>
</div>
@@ -1731,7 +1720,7 @@ export function NewWorktreeDialog({
onClick={() => setGithubDialogOpen(true)}
className="gap-1.5 h-7"
>
<RiGithubLine className="size-4 text-status-success" />
<Icon name="github" className="size-4 text-status-success" />
{newBranchState.linkedIssue || newBranchState.linkedPr ? t('session.newWorktree.actions.change') : t('session.newWorktree.actions.startFromGitHubIssuePr')}
</Button>
)}
@@ -1758,7 +1747,7 @@ export function NewWorktreeDialog({
/>
{newBranchState.linkedPr && (
<div className="flex items-center gap-1.5 text-muted-foreground">
<RiCheckLine className="h-3.5 w-3.5 text-status-success" />
<Icon name="check" className="h-3.5 w-3.5 text-status-success" />
<span className="typography-micro">
{t('session.newWorktree.usingPrBranch', { branch: newBranchState.linkedPr.head })}
</span>
@@ -1766,7 +1755,7 @@ export function NewWorktreeDialog({
)}
{newBranchState.linkedIssue && !newBranchState.linkedPr && (
<div className="flex items-center gap-1.5 text-muted-foreground">
<RiCheckLine className="h-3.5 w-3.5 text-status-success" />
<Icon name="check" className="h-3.5 w-3.5 text-status-success" />
<span className="typography-micro">
{t('session.newWorktree.fromIssue', { number: newBranchState.linkedIssue.number, title: newBranchState.linkedIssue.title })}
</span>
@@ -1800,7 +1789,7 @@ export function NewWorktreeDialog({
)}
title={t('session.newWorktree.resetToMatchBranchName')}
>
<RiRefreshLine className="h-3 w-3" />
<Icon name="refresh" className="h-3 w-3" />
<span>{t('session.newWorktree.actions.reset')}</span>
</button>
)}
@@ -1842,7 +1831,7 @@ export function NewWorktreeDialog({
<span className={cn('truncate', newBranchState.sourceBranch ? 'text-foreground' : 'text-muted-foreground')}>
{newBranchState.sourceBranch || t('session.newWorktree.selectSourceBranchPlaceholder')}
</span>
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" portalToBody className="w-[min(42rem,calc(100vw-2rem))] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={sourceBranchDropdownContentRef}>
@@ -1944,7 +1933,7 @@ export function NewWorktreeDialog({
<div className="mt-2 px-2 py-1.5 rounded bg-muted/30">
{/* Row 1: Type, number, title, actions */}
<div className="flex items-center gap-2">
<RiGithubLine className="h-3.5 w-3.5 text-status-success shrink-0" />
<Icon name="github" className="h-3.5 w-3.5 text-status-success shrink-0" />
{newBranchState.linkedIssue && (
<span className="typography-micro text-muted-foreground shrink-0">
@@ -1968,14 +1957,14 @@ export function NewWorktreeDialog({
className="text-muted-foreground hover:text-foreground shrink-0"
onClick={(e) => e.stopPropagation()}
>
<RiExternalLinkLine className="h-3 w-3" />
<Icon name="external-link" className="h-3 w-3" />
</a>
<button
onClick={handleClearLinkedItem}
className="text-muted-foreground hover:text-foreground shrink-0 p-0.5 rounded hover:bg-muted transition-colors"
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
</div>
@@ -2002,7 +1991,7 @@ export function NewWorktreeDialog({
<div className="flex items-center gap-1.5 text-destructive">
{validation.touched && (validation.branchError || validation.worktreeError) && (
<>
<RiErrorWarningLine className="h-3.5 w-3.5" />
<Icon name="error-warning" className="h-3.5 w-3.5" />
<span className="typography-micro">
{validation.branchError || validation.worktreeError}
</span>
@@ -2025,7 +2014,7 @@ export function NewWorktreeDialog({
disabled={!canCreate || isCreating}
className="gap-1.5"
>
{isCreating && <RiLoader4Line className="h-3.5 w-3.5 animate-spin" />}
{isCreating && <Icon name="loader-4" className="h-3.5 w-3.5 animate-spin" />}
{isCreating ? t('session.newWorktree.actions.creating') : t('session.newWorktree.actions.createWorktree')}
</Button>
</div>
@@ -1,5 +1,4 @@
import React from 'react';
import { RiAddLine, RiDeleteBinLine, RiSendPlaneLine } from '@remixicon/react';
import { toast } from '@/components/ui';
import { Checkbox } from '@/components/ui/checkbox';
import {
@@ -10,6 +9,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Icon } from "@/components/icon/Icon";
import {
deleteProjectPlanFile,
getProjectContextData,
@@ -575,7 +575,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
aria-label={t('rightSidebar.contextNotesTodo.todo.addAria')}
title={t('rightSidebar.contextNotesTodo.todo.addAria')}
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
</button>
</div>
@@ -623,7 +623,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
aria-label={t('rightSidebar.contextNotesTodo.todo.actions.delete', { text: todo.text })}
title={t('rightSidebar.contextNotesTodo.todo.actions.delete', { text: todo.text })}
>
<RiDeleteBinLine className="h-3.5 w-3.5" />
<Icon name="delete-bin" className="h-3.5 w-3.5" />
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -634,7 +634,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
aria-label={t('rightSidebar.contextNotesTodo.todo.actions.send', { text: todo.text })}
title={t('rightSidebar.contextNotesTodo.todo.actions.send', { text: todo.text })}
>
<RiSendPlaneLine className="h-3.5 w-3.5" />
<Icon name="send-plane" className="h-3.5 w-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
@@ -692,7 +692,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
aria-label={t('rightSidebar.contextNotesTodo.plans.importFromFile')}
title={t('rightSidebar.contextNotesTodo.plans.importFromFile')}
>
<RiAddLine className="h-3.5 w-3.5" />
<Icon name="add" className="h-3.5 w-3.5" />
</button>
</div>
@@ -723,7 +723,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
title={t('rightSidebar.contextNotesTodo.plans.deletePlan')}
aria-label={t('rightSidebar.contextNotesTodo.plans.deletePlanWithTitle', { title: plan.title })}
>
<RiDeleteBinLine className="h-3.5 w-3.5" />
<Icon name="delete-bin" className="h-3.5 w-3.5" />
</button>
</li>
))}
@@ -9,12 +9,12 @@ import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { toast } from '@/components/ui';
import { RiAddLine, RiCloseLine, RiCalendarLine, RiArrowLeftSLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/react';
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
import { AgentSelector } from '@/components/sections/commands/AgentSelector';
import { isPrimaryMode } from '@/components/chat/mobileControlsUtils';
import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete';
import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete';
import { Icon } from "@/components/icon/Icon";
import { useConfigStore } from '@/stores/useConfigStore';
import { useUIStore } from '@/stores/useUIStore';
import type { ScheduledTask } from '@/lib/scheduledTasksApi';
@@ -700,7 +700,6 @@ export function ScheduledTaskEditorDialog(props: {
};
}, [isDatePickerOpen]);
const variantOptions = React.useMemo(() => {
const provider = providers.find((item) => item.id === draft.execution.providerID);
const model = provider?.models?.find((item) => item.id === draft.execution.modelID) as { variants?: Record<string, unknown> } | undefined;
@@ -1066,10 +1065,10 @@ export function ScheduledTaskEditorDialog(props: {
onClick={() => setIsDatePickerOpen((prev) => !prev)}
>
<span className="inline-flex items-center gap-2">
<RiCalendarLine className="h-4 w-4 text-muted-foreground" />
<Icon name="calendar" className="h-4 w-4 text-muted-foreground" />
<span className="typography-ui-label text-foreground">{formatDateLabel(draft.schedule.onceDate, t('sessions.scheduledTasks.editor.date.placeholder'), locale)}</span>
</span>
<RiArrowDownSLine className="h-4 w-4 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-4 w-4 text-muted-foreground" />
</button>
{isDatePickerOpen ? (
@@ -1082,7 +1081,7 @@ export function ScheduledTaskEditorDialog(props: {
aria-label={t('sessions.scheduledTasks.editor.date.previousMonth')}
disabled={isAtCurrentMonth}
>
<RiArrowLeftSLine className="h-4 w-4" />
<Icon name="arrow-left-s" className="h-4 w-4" />
</button>
<div className="typography-ui-label text-foreground">
{new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(calendarMonth)}
@@ -1093,7 +1092,7 @@ export function ScheduledTaskEditorDialog(props: {
onClick={() => setCalendarMonth((prev) => shiftMonth(prev, 1))}
aria-label={t('sessions.scheduledTasks.editor.date.nextMonth')}
>
<RiArrowRightSLine className="h-4 w-4" />
<Icon name="arrow-right-s" className="h-4 w-4" />
</button>
</div>
@@ -1255,7 +1254,7 @@ export function ScheduledTaskEditorDialog(props: {
onClick={() => removeTimeAt(index)}
aria-label={t('sessions.scheduledTasks.editor.times.removeAria')}
>
<RiCloseLine className="h-4 w-4" />
<Icon name="close" className="h-4 w-4" />
</Button>
) : null}
</div>
@@ -1263,7 +1262,7 @@ export function ScheduledTaskEditorDialog(props: {
</div>
<div>
<Button type="button" size="sm" variant="outline" onClick={addTime}>
<RiAddLine className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.editor.times.add')}
<Icon name="add" className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.editor.times.add')}
</Button>
</div>
</div>
@@ -11,19 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Checkbox } from '@/components/ui/checkbox';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { toast } from '@/components/ui';
import {
RiLoader4Line,
RiPlayLine,
RiEdit2Line,
RiDeleteBinLine,
RiAddLine,
RiFolderLine,
RiTimerLine,
RiHistoryLine,
RiCheckboxCircleLine,
RiErrorWarningLine,
RiPulseLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { useUIStore } from '@/stores/useUIStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
@@ -159,14 +147,14 @@ const STATUS_META: Record<
ScheduledTaskStatus,
{
tone: StatusTone;
Icon: React.ComponentType<{ className?: string }>;
Icon: string;
spin?: boolean;
}
> = {
success: { tone: 'success', Icon: RiCheckboxCircleLine },
error: { tone: 'error', Icon: RiErrorWarningLine },
running: { tone: 'warning', Icon: RiLoader4Line, spin: true },
idle: { tone: 'muted', Icon: RiPulseLine },
success: { tone: 'success', Icon: 'checkbox-circle' },
error: { tone: 'error', Icon: 'error-warning' },
running: { tone: 'warning', Icon: 'loader-4', spin: true },
idle: { tone: 'muted', Icon: 'pulse' },
};
const toneStyle = (tone: StatusTone): React.CSSProperties => {
@@ -213,7 +201,7 @@ export function ScheduledTasksDialog() {
iconColor: currentTheme.colors.surface.foreground,
},
);
const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const projectIconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const iconColor = project.color ? PROJECT_COLOR_MAP[project.color] : undefined;
return (
@@ -225,10 +213,10 @@ export function ScheduledTasksDialog() {
>
<img src={imageUrl} alt="" className="h-full w-full object-contain" draggable={false} />
</span>
) : ProjectIcon ? (
<ProjectIcon className="h-3.5 w-3.5 shrink-0" style={iconColor ? { color: iconColor } : undefined} />
) : projectIconName ? (
<Icon name={projectIconName} className="h-3.5 w-3.5 shrink-0" style={iconColor ? { color: iconColor } : undefined} />
) : (
<RiFolderLine className="h-3.5 w-3.5 shrink-0 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined} />
<Icon name="folder" className="h-3.5 w-3.5 shrink-0 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined}/>
)}
<span className="truncate">{displayLabel}</span>
</span>
@@ -423,7 +411,7 @@ export function ScheduledTasksDialog() {
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
{projectSelector}
<Button onClick={openNewTaskEditor} disabled={!selectedProjectID}>
<RiAddLine className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.newTask')}
<Icon name="add" className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.newTask')}
</Button>
</div>
) : (
@@ -433,7 +421,7 @@ export function ScheduledTasksDialog() {
<div className="min-h-[280px]">
{loading ? (
<div className="flex items-center gap-2 typography-meta text-muted-foreground">
<RiLoader4Line className="h-4 w-4 animate-spin" /> {t('sessions.scheduledTasks.dialog.loading')}
<Icon name="loader-4" className="h-4 w-4 animate-spin" /> {t('sessions.scheduledTasks.dialog.loading')}
</div>
) : tasks.length === 0 ? (
<div className="rounded-lg border border-dashed border-border p-4 typography-meta text-muted-foreground">
@@ -474,7 +462,7 @@ export function ScheduledTasksDialog() {
<div className="mt-3 flex flex-wrap items-center gap-x-5 gap-y-1 typography-micro text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<RiTimerLine className="h-3.5 w-3.5" />
<Icon name="timer" className="h-3.5 w-3.5" />
<span className="font-medium text-foreground">{t('sessions.scheduledTasks.dialog.nextRun.label')}</span>
{nextAt ? (
<>
@@ -487,14 +475,14 @@ export function ScheduledTasksDialog() {
)}
</span>
<span className="inline-flex items-center gap-1.5">
<RiHistoryLine className="h-3.5 w-3.5" />
<Icon name="history" className="h-3.5 w-3.5" />
<span className="font-medium text-foreground">{t('sessions.scheduledTasks.dialog.lastRun.label')}</span>
{status === 'running' ? (
<span
className="inline-flex items-center gap-1"
style={{ color: 'var(--status-warning)' }}
>
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
<Icon name="loader-4" className="h-3.5 w-3.5 animate-spin" />
{t('sessions.scheduledTasks.dialog.lastRun.runningNow')}
</span>
) : lastAt ? (
@@ -504,7 +492,7 @@ export function ScheduledTasksDialog() {
className="inline-flex items-center gap-1"
style={{ color: `var(--status-${meta.tone})` }}
>
<meta.Icon className="h-3.5 w-3.5" />
<Icon name={meta.Icon} className="h-3.5 w-3.5" />
{statusLabel}
</span>
) : null}
@@ -522,7 +510,7 @@ export function ScheduledTasksDialog() {
className="mt-3 flex items-start gap-2 rounded-md border p-2 typography-micro"
style={toneStyle('error')}
>
<RiErrorWarningLine className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<Icon name="error-warning" className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span className="min-w-0 break-words">{task.state.lastError}</span>
</div>
) : null}
@@ -553,7 +541,7 @@ export function ScheduledTasksDialog() {
onClick={() => void handleRunNow(task)}
disabled={isBusy}
>
<RiPlayLine className="h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.runNow')}
<Icon name="play" className="h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.runNow')}
</Button>
<Button
variant="outline"
@@ -565,7 +553,7 @@ export function ScheduledTasksDialog() {
disabled={isBusy}
aria-label={t('sessions.scheduledTasks.dialog.actions.editAria', { taskName: task.name })}
>
<RiEdit2Line className="h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.edit')}
<Icon name="edit-2" className="h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.edit')}
</Button>
<Button
variant="destructive"
@@ -574,7 +562,7 @@ export function ScheduledTasksDialog() {
disabled={isBusy}
aria-label={t('sessions.scheduledTasks.dialog.actions.deleteAria', { taskName: task.name })}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</Button>
</div>
</div>
@@ -612,7 +600,7 @@ export function ScheduledTasksDialog() {
onClick={openNewTaskEditor}
disabled={!selectedProjectID}
>
<RiAddLine className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.newTask')}
<Icon name="add" className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.newTask')}
</Button>
)}
>
@@ -10,8 +10,8 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { RiCheckboxBlankLine, RiCheckboxLine, RiDeleteBinLine, RiGitBranchLine } from '@remixicon/react';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { Icon } from "@/components/icon/Icon";
import { DirectoryExplorerDialog } from './DirectoryExplorerDialog';
import { cn, formatPathForDisplay } from '@/lib/utils';
import type { Session } from '@opencode-ai/sdk/v2';
@@ -606,7 +606,7 @@ export const SessionDialogs: React.FC = () => {
{isWorktreeDelete ? (
<div className="space-y-2 rounded-lg bg-muted/30 p-3">
<div className="flex items-center gap-2">
<RiGitBranchLine className="h-4 w-4 text-muted-foreground" />
<Icon name="git-branch" className="h-4 w-4 text-muted-foreground" />
<span className="typography-meta font-medium text-foreground">{t('sessions.sidebar.sessionDialogs.worktree.label')}</span>
{targetWorktree?.label ? (
<span className="typography-micro text-muted-foreground/70">{targetWorktree.label}</span>
@@ -651,9 +651,9 @@ export const SessionDialogs: React.FC = () => {
)}
>
{deleteDialogShouldRemoveRemote ? (
<RiCheckboxLine className="size-4 text-primary" />
<Icon name="checkbox" className="size-4 text-primary" />
) : (
<RiCheckboxBlankLine className="size-4" />
<Icon name="checkbox-blank" className="size-4" />
)}
{t('sessions.sidebar.sessionDialogs.actions.deleteRemoteBranch')}
</button>
@@ -680,9 +680,9 @@ export const SessionDialogs: React.FC = () => {
)}
>
{deleteDialogShouldDeleteLocalBranch ? (
<RiCheckboxLine className="size-4 text-primary" />
<Icon name="checkbox" className="size-4 text-primary" />
) : (
<RiCheckboxBlankLine className="size-4" />
<Icon name="checkbox-blank" className="size-4" />
)}
{t('sessions.sidebar.sessionDialogs.actions.deleteLocalBranch')}
</button>
@@ -713,7 +713,7 @@ export const SessionDialogs: React.FC = () => {
className="inline-flex items-center gap-1.5 typography-meta text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50"
aria-pressed={!showDeletionDialog}
>
{!showDeletionDialog ? <RiCheckboxLine className="size-4 text-primary" /> : <RiCheckboxBlankLine className="size-4" />}
{!showDeletionDialog ? <Icon name="checkbox" className="size-4 text-primary" /> : <Icon name="checkbox-blank" className="size-4" />}
{t('sessions.sidebar.dialogs.neverAsk')}
</button>
<div className="flex items-center gap-2">
@@ -779,7 +779,7 @@ export const SessionDialogs: React.FC = () => {
>
<DialogHeader>
<DialogTitle className={cn(isWorktreeDelete && 'flex items-center gap-2')}>
{isWorktreeDelete && <RiDeleteBinLine className="h-5 w-5" />}
{isWorktreeDelete && <Icon name="delete-bin" className="h-5 w-5" />}
{deleteDialogTitle}
</DialogTitle>
{deleteDialogDescription && <DialogDescription>{deleteDialogDescription}</DialogDescription>}
@@ -1,19 +1,8 @@
import React from 'react';
import {
RiFolderLine,
RiFolderOpenLine,
RiArrowRightSLine,
RiArrowDownSLine,
RiPencilAiLine,
RiDeleteBinLine,
RiCheckLine,
RiCloseLine,
RiAddLine,
RiFolderAddLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
interface SessionFolderItemProps<TSessionNode> {
folder: SessionFolder;
@@ -143,7 +132,7 @@ const SessionFolderItemBase = <TSessionNode,>({
};
}, [isRenaming]);
const FolderIcon = isCollapsed ? RiFolderLine : RiFolderOpenLine;
const folderIconName = isCollapsed ? 'folder' : 'folder-open';
const isSubFolder = depth > 0;
return (
@@ -179,7 +168,7 @@ const SessionFolderItemBase = <TSessionNode,>({
? (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')} />
<Icon name={folderIconName} className={cn('h-3.5 w-3.5 flex-shrink-0', isDropTarget ? 'text-primary' : 'text-muted-foreground')} />
{renaming ? (
<form
@@ -220,7 +209,7 @@ const SessionFolderItemBase = <TSessionNode,>({
onPointerDown={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
>
<RiCheckLine className="size-4" />
<Icon name="check" className="size-4" />
</button>
<button
type="button"
@@ -232,7 +221,7 @@ const SessionFolderItemBase = <TSessionNode,>({
onMouseDown={(event) => event.stopPropagation()}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<RiCloseLine className="size-4" />
<Icon name="close" className="size-4" />
</button>
</form>
) : (
@@ -244,9 +233,9 @@ const SessionFolderItemBase = <TSessionNode,>({
{sessions.length}
</span>
{isCollapsed ? (
<RiArrowRightSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<Icon name="arrow-right-s" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
) : (
<RiArrowDownSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
)}
</div>
)}
@@ -274,7 +263,7 @@ const SessionFolderItemBase = <TSessionNode,>({
aria-label={t('sessions.sidebar.folderItem.newSessionAria', { folderName: folder.name })}
title={t('sessions.sidebar.project.actions.newSession')}
>
<RiAddLine className="h-3.5 w-3.5" />
<Icon name="add" className="h-3.5 w-3.5" />
</button>
) : null}
{/* Only allow sub-folders at depth 0 (one level deep max) */}
@@ -289,7 +278,7 @@ const SessionFolderItemBase = <TSessionNode,>({
aria-label={t('sessions.sidebar.folderItem.newSubfolderAria', { folderName: folder.name })}
title={t('sessions.sidebar.folderItem.newSubfolder')}
>
<RiFolderAddLine className="h-3.5 w-3.5" />
<Icon name="folder-add" className="h-3.5 w-3.5" />
</button>
) : null}
{!archivedBucket ? (
@@ -302,7 +291,7 @@ const SessionFolderItemBase = <TSessionNode,>({
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.folderItem.renameAria', { folderName: folder.name })}
>
<RiPencilAiLine className="h-3.5 w-3.5" />
<Icon name="pencil-ai" className="h-3.5 w-3.5" />
</button>
) : null}
<button
@@ -316,7 +305,7 @@ const SessionFolderItemBase = <TSessionNode,>({
? t('sessions.sidebar.folderItem.deleteArchivedInFolderAria', { folderName: folder.name })
: t('sessions.sidebar.folderItem.deleteFolderAria', { folderName: folder.name })}
>
<RiDeleteBinLine className="h-3.5 w-3.5" />
<Icon name="delete-bin" className="h-3.5 w-3.5" />
</button>
</div>
</div>
@@ -1,6 +1,5 @@
import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2';
import { RiLayoutLeftLine } from '@remixicon/react';
import { toast } from '@/components/ui';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useI18n } from '@/lib/i18n';
@@ -38,6 +37,7 @@ import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders
import { getGitHubPrStatusKey, usePrVisualSummaryByKeys, useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
import { UpdateDialog } from '@/components/ui/UpdateDialog';
import { Icon } from "@/components/icon/Icon";
import { SessionGroupSection } from './sidebar/SessionGroupSection';
import { SidebarHeader } from './sidebar/SidebarHeader';
import { SidebarActivitySections } from './sidebar/SidebarActivitySections';
@@ -1648,7 +1648,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
className={desktopSidebarToggleButtonClass}
aria-label={t('sessions.sidebar.header.actions.closeSessions')}
>
<RiLayoutLeftLine className="h-[18px] w-[18px]" />
<Icon name="layout-left" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent>
@@ -17,8 +17,8 @@ import { AgentSelector } from '@/components/sections/commands/AgentSelector';
import { useConfigStore } from '@/stores/useConfigStore';
import { useAgentsStore } from '@/stores/useAgentsStore';
import { isPrimaryMode } from '@/components/chat/mobileControlsUtils';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { RiArrowDownSLine } from '@remixicon/react';
import { useI18n } from '@/lib/i18n';
type TodoSendTarget = 'session' | 'worktree';
@@ -70,7 +70,7 @@ const ThinkingPill = ({ value, options, disabled, onChange }: ThinkingPillProps)
)}
>
<span className="typography-micro whitespace-nowrap font-medium capitalize">{label}</span>
<RiArrowDownSLine className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
</div>
);
@@ -7,12 +7,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
RiAddLine,
RiCloseLine,
RiDeleteBinLine,
RiFolderLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
import { useI18n } from '@/lib/i18n';
@@ -67,7 +62,7 @@ export const BulkActionBar: React.FC<Props> = ({
className={iconButtonClass}
aria-label={t('sessions.sidebar.bulkActions.moveToFolder')}
>
<RiFolderLine className="h-4 w-4" />
<Icon name="folder" className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
@@ -87,7 +82,7 @@ export const BulkActionBar: React.FC<Props> = ({
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onCreateFolderAndMove}>
<RiAddLine className="mr-1 h-4 w-4" />
<Icon name="add" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.folders.newFolderEllipsis')}
</DropdownMenuItem>
{canRemoveFromFolder ? (
@@ -95,7 +90,7 @@ export const BulkActionBar: React.FC<Props> = ({
onClick={onRemoveFromFolder}
className="text-destructive focus:text-destructive"
>
<RiCloseLine className="mr-1 h-4 w-4" />
<Icon name="close" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.folders.removeFromFolder')}
</DropdownMenuItem>
) : null}
@@ -111,7 +106,7 @@ export const BulkActionBar: React.FC<Props> = ({
className={cn(destructiveIconButtonClass)}
aria-label={destructiveLabel}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{destructiveLabel}</p></TooltipContent>
@@ -125,7 +120,7 @@ export const BulkActionBar: React.FC<Props> = ({
className={iconButtonClass}
aria-label={t('sessions.sidebar.header.actions.exitSelection')}
>
<RiCloseLine className="h-4 w-4" />
<Icon name="close" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{t('sessions.sidebar.header.actions.exitSelection')}</p></TooltipContent>
@@ -1,6 +1,6 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { RiCheckboxBlankLine, RiCheckboxLine } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import type { Session } from '@opencode-ai/sdk/v2';
import { useI18n } from '@/lib/i18n';
@@ -65,7 +65,7 @@ export function SessionDeleteConfirmDialog(props: {
className="inline-flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50"
aria-pressed={!showDeletionDialog}
>
{!showDeletionDialog ? <RiCheckboxLine className="h-4 w-4 text-primary" /> : <RiCheckboxBlankLine className="h-4 w-4" />}
{!showDeletionDialog ? <Icon name="checkbox" className="h-4 w-4 text-primary" /> : <Icon name="checkbox-blank" className="h-4 w-4" />}
{t('sessions.sidebar.dialogs.neverAsk')}
</button>
<div className="flex items-center gap-2">
@@ -135,7 +135,7 @@ export function BulkSessionDeleteConfirmDialog(props: {
className="inline-flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50"
aria-pressed={!showDeletionDialog}
>
{!showDeletionDialog ? <RiCheckboxLine className="h-4 w-4 text-primary" /> : <RiCheckboxBlankLine className="h-4 w-4" />}
{!showDeletionDialog ? <Icon name="checkbox" className="h-4 w-4 text-primary" /> : <Icon name="checkbox-blank" className="h-4 w-4" />}
{t('sessions.sidebar.dialogs.neverAsk')}
</button>
<div className="flex items-center gap-2">
@@ -1,15 +1,7 @@
import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2';
import {
RiAddLine,
RiArchiveLine,
RiArrowDownSLine,
RiArrowLeftLongLine,
RiArrowRightSLine,
RiDeleteBinLine,
RiGitBranchLine,
} from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { sessionEvents } from '@/lib/sessionEvents';
import type { MainTab } from '@/stores/useUIStore';
@@ -536,7 +528,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
<TooltipTrigger asChild>
<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">
<RiGitBranchLine
<Icon name="git-branch"
className={cn('h-3.5 w-3.5 shrink-0', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
@@ -544,7 +536,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
'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 ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
{prIndicator.url ? (
@@ -568,7 +560,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{baseBranchLabel && headBranchLabel ? (
<>
<span>{baseBranchLabel}</span>
<RiArrowLeftLongLine className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<Icon name="arrow-left-long" className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<span>{headBranchLabel}</span>
</>
) : (
@@ -593,12 +585,12 @@ export function SessionGroupSection(props: Props): React.ReactNode {
) : group.isArchivedBucket ? (
<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">
<RiArchiveLine className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')} />
<Icon name="archive" className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')} />
<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 ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
<span className="min-w-0 flex-1 truncate">{renderHighlightedText(group.label, normalizedSessionSearchQuery)}</span>
@@ -606,7 +598,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
) : (!group.isMain || group.worktree) ? (
<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">
<RiGitBranchLine
<Icon name="git-branch"
className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
@@ -614,7 +606,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
'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 ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
<span className="min-w-0 flex-1 truncate">{renderHighlightedText(group.label, normalizedSessionSearchQuery)}</span>
@@ -626,16 +618,15 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{showBranchSubtitle && statusLine ? (
<span className="inline-flex min-w-0 items-center gap-1.5 leading-tight">
{group.isArchivedBucket ? (
<RiArchiveLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<Icon name="archive" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
) : (!group.isMain || isGitProject) ? (
showInlinePrTitle && prIndicator ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
<RiGitBranchLine
className="h-3.5 w-3.5 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
<Icon name="git-branch" className="h-3.5 w-3.5 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}/>
</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6} align="start" className="max-w-sm">
@@ -645,7 +636,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{baseBranchLabel && headBranchLabel ? (
<>
<span>{baseBranchLabel}</span>
<RiArrowLeftLongLine className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<Icon name="arrow-left-long" className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<span>{headBranchLabel}</span>
</>
) : (
@@ -666,10 +657,9 @@ export function SessionGroupSection(props: Props): React.ReactNode {
</TooltipContent>
</Tooltip>
) : (
<RiGitBranchLine
className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
<Icon name="git-branch" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}/>
)
) : null}
<span
@@ -698,7 +688,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.group.actions.deleteArchivedInGroupAria', { label: group.label })}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.group.actions.deleteArchivedSessions')}</p></TooltipContent>
@@ -722,7 +712,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.group.actions.deleteGroupAria', { label: group.label })}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.group.actions.deleteWorktree')}</p></TooltipContent>
@@ -745,7 +735,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.group.actions.newDraftInGroupAria', { label: group.label })}
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.project.actions.newDraftSession')}</p></TooltipContent>
@@ -11,34 +11,12 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
RiAddLine,
RiArchiveLine,
RiArrowDownSLine,
RiArrowRightSLine,
RiChat4Line,
RiCheckLine,
RiCloseLine,
RiDeleteBinLine,
RiDownloadLine,
RiErrorWarningLine,
RiFileCopyLine,
RiFolderLine,
RiLinkUnlinkM,
RiMore2Line,
RiPencilAiLine,
RiPushpinLine,
RiShare2Line,
RiShieldLine,
RiUnpinLine,
RiGitBranchLine,
RiWindowLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { canUseElectronDesktopIPC, invokeDesktop, isVSCodeRuntime } from '@/lib/desktop';
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Icon } from "@/components/icon/Icon";
import { buildExportFilename, downloadAsMarkdown, formatSessionAsMarkdown, getExportRevealLabelKey, revealExportedMarkdown, saveAsMarkdownDesktop } from '@/lib/exportSession';
import type { ChildSessionExport } from '@/lib/exportSession';
import { buildSessionMessageRecordsSnapshot, useDirectoryStore, useGlobalSessionStatus, useSession, useSessionPermissions } from '@/sync/sync-context';
@@ -479,7 +457,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
title={t('sessions.sidebar.session.rename.save')}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<RiCheckLine className="size-4" />
<Icon name="check" className="size-4" />
</button>
<button
type="button"
@@ -488,17 +466,17 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
title={t('sessions.sidebar.session.rename.cancel')}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<RiCloseLine className="size-4" />
<Icon name="close" className="size-4" />
</button>
</form>
{!isMinimalMode ? (
<div className="flex items-center justify-between gap-3 text-muted-foreground/60 min-w-0 overflow-hidden leading-tight" style={{ fontSize: 'calc(var(--text-ui-label) * 0.85)' }}>
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden">
{hasChildren ? <span className="inline-flex items-center justify-center flex-shrink-0">{isExpanded ? <RiArrowDownSLine className="h-3 w-3" /> : <RiArrowRightSLine className="h-3 w-3" />}</span> : null}
{hasChildren ? <span className="inline-flex items-center justify-center flex-shrink-0">{isExpanded ? <Icon name="arrow-down-s" className="h-3 w-3" /> : <Icon name="arrow-right-s" className="h-3 w-3" />}</span> : null}
<span className="flex-shrink-0">{sessionUpdatedLabel}</span>
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0 text-[0.92em]"><span className="text-status-success/80">+{sessionDiffStats.additions}</span><span className="text-status-error/65">/-{sessionDiffStats.deletions}</span></span> : null}
{hasSecondaryProjectLabel ? <span className="truncate">{secondaryMeta?.projectLabel}</span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><RiGitBranchLine className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
</div>
</div>
) : null}
@@ -537,7 +515,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
)}
>
{showStatusMarker ? statusMarkerContent : null}
{isPinnedSession ? <RiPushpinLine className="h-3 w-3 flex-shrink-0 text-primary" aria-label={t('sessions.sidebar.session.status.pinned')} /> : null}
{isPinnedSession ? <Icon name="pushpin" className="h-3 w-3 flex-shrink-0 text-primary" aria-label={t('sessions.sidebar.session.status.pinned')}/> : null}
</span>
) : null;
const subsessionChevron = hasChildren ? (
@@ -566,12 +544,12 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
? t('sessions.sidebar.session.subsessions.collapse')
: t('sessions.sidebar.session.subsessions.expand')}
>
{isExpanded ? <RiArrowDownSLine className="h-3 w-3" /> : <RiArrowRightSLine className="h-3 w-3" />}
{isExpanded ? <Icon name="arrow-down-s" className="h-3 w-3" /> : <Icon name="arrow-right-s" className="h-3 w-3" />}
</span>
) : null;
const streamingIndicator = isZombie
? <RiErrorWarningLine className="h-4 w-4 text-status-warning" />
? <Icon name="error-warning" className="h-4 w-4 text-status-warning" />
: null;
const handleMenuOpenChange = (open: boolean) => {
@@ -663,33 +641,33 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
}}
className="[&>svg]:mr-1"
>
<RiPencilAiLine className="mr-1 h-4 w-4" />
<Icon name="pencil-ai" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.rename')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => togglePinnedSession(session.id)} className="[&>svg]:mr-1">
{isPinnedSession ? <RiUnpinLine className="mr-1 h-4 w-4" /> : <RiPushpinLine className="mr-1 h-4 w-4" />}
{isPinnedSession ? <Icon name="unpin" className="mr-1 h-4 w-4" /> : <Icon name="pushpin" className="mr-1 h-4 w-4" />}
{isPinnedSession ? t('sessions.sidebar.session.menu.unpin') : t('sessions.sidebar.session.menu.pin')}
</DropdownMenuItem>
{!resolvedSession.share ? (
<DropdownMenuItem onClick={() => handleShareSession(resolvedSession)} className="[&>svg]:mr-1">
<RiShare2Line className="mr-1 h-4 w-4" />
<Icon name="share-2" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.share')}
</DropdownMenuItem>
) : (
<>
<DropdownMenuItem onClick={() => { if (resolvedSession.share?.url) handleCopyShareUrl(resolvedSession.share.url, session.id); }} className="[&>svg]:mr-1">
{copiedSessionId === session.id
? <><RiCheckLine className="mr-1 h-4 w-4" style={{ color: 'var(--status-success)' }} />{t('sessions.sidebar.session.menu.copied')}</>
: <><RiFileCopyLine className="mr-1 h-4 w-4" />{t('sessions.sidebar.session.menu.copyLink')}</>}
? <><Icon name="check" className="mr-1 h-4 w-4" style={{ color: 'var(--status-success)' }}/>{t('sessions.sidebar.session.menu.copied')}</>
: <><Icon name="file-copy" className="mr-1 h-4 w-4" />{t('sessions.sidebar.session.menu.copyLink')}</>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleUnshareSession(session.id)} className="[&>svg]:mr-1">
<RiLinkUnlinkM className="mr-1 h-4 w-4" />
<Icon name="link-unlink-m" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.unshare')}
</DropdownMenuItem>
</>
)}
<DropdownMenuItem onClick={() => { void handleExportSession(); }} className="[&>svg]:mr-1">
<RiDownloadLine className="mr-1 h-4 w-4" />
<Icon name="download" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.exportMarkdown')}
</DropdownMenuItem>
@@ -700,7 +678,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger className="[&>svg]:mr-1"><RiFolderLine className="h-4 w-4" />{t('sessions.sidebar.folders.moveToFolder')}</DropdownMenuSubTrigger>
<DropdownMenuSubTrigger className="[&>svg]:mr-1"><Icon name="folder" className="h-4 w-4" />{t('sessions.sidebar.folders.moveToFolder')}</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="min-w-[180px]">
{scopeFolders.length === 0 ? (
<DropdownMenuItem disabled className="text-muted-foreground">{t('sessions.sidebar.folders.none')}</DropdownMenuItem>
@@ -708,18 +686,18 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
scopeFolders.map((folder) => (
<DropdownMenuItem key={folder.id} onClick={() => { if (currentFolderId === folder.id) removeSessionFromFolder(sessionDirectory, session.id); else addSessionToFolder(sessionDirectory, folder.id, session.id); }}>
<span className="flex-1 truncate">{folder.name}</span>
{currentFolderId === folder.id ? <RiCheckLine className="ml-2 h-3.5 w-3.5 text-primary flex-shrink-0" /> : null}
{currentFolderId === folder.id ? <Icon name="check" className="ml-2 h-3.5 w-3.5 text-primary flex-shrink-0" /> : null}
</DropdownMenuItem>
))
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => { const newFolder = createFolderAndStartRename(sessionDirectory); if (!newFolder) return; addSessionToFolder(sessionDirectory, newFolder.id, session.id); }}>
<RiAddLine className="mr-1 h-4 w-4" />
<Icon name="add" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.folders.newFolderEllipsis')}
</DropdownMenuItem>
{currentFolderId ? (
<DropdownMenuItem onClick={() => { removeSessionFromFolder(sessionDirectory, session.id); }} className="text-destructive focus:text-destructive">
<RiCloseLine className="mr-1 h-4 w-4" />
<Icon name="close" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.folders.removeFromFolder')}
</DropdownMenuItem>
) : null}
@@ -742,7 +720,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
}}
className="[&>svg]:mr-1"
>
<RiChat4Line className="mr-1 h-4 w-4" />
<Icon name="chat-4" className="mr-1 h-4 w-4" />
<span className="truncate">{t('sessions.sidebar.session.menu.openInSidePanel')}</span>
<span className="shrink-0 typography-micro px-1 rounded leading-none pb-px text-[var(--status-warning)] bg-[var(--status-warning)]/10">{t('sessions.sidebar.session.menu.betaBadge')}</span>
</DropdownMenuItem>
@@ -754,14 +732,14 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onClick={handleOpenMiniChatWindow}
className="[&>svg]:mr-1"
>
<RiWindowLine className="mr-1 h-4 w-4" />
<Icon name="window" className="mr-1 h-4 w-4" />
<span className="truncate">{t('sessions.sidebar.session.menu.openMiniChatWindow')}</span>
</DropdownMenuItem>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:text-destructive [&>svg]:mr-1" onClick={() => handleDeleteSession(session, { archivedBucket })}>
<RiDeleteBinLine className="mr-1 h-4 w-4" />
<Icon name="delete-bin" className="mr-1 h-4 w-4" />
{archivedBucket ? t('sessions.sidebar.bulkActions.delete') : t('sessions.sidebar.bulkActions.archive')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -824,7 +802,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
) : null}
{pendingPermissionCount > 0 ? (
<span className="inline-flex items-center gap-1 rounded bg-destructive/10 px-1 py-0.5 text-[0.7rem] text-destructive flex-shrink-0" title={t('sessions.sidebar.session.status.permissionRequired')} aria-label={t('sessions.sidebar.session.status.permissionRequired')}>
<RiShieldLine className="h-3 w-3" />
<Icon name="shield" className="h-3 w-3" />
<span className="leading-none">{pendingPermissionCount}</span>
</span>
) : null}
@@ -841,7 +819,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<div className={cn('flex items-center gap-3 text-left text-muted-foreground', secondaryMeta?.branchLabel ? 'justify-between' : 'justify-start')}>
{secondaryMeta?.branchLabel ? (
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden">
<span className="inline-flex min-w-0 items-center gap-0.5"><RiGitBranchLine className="h-3 w-3 flex-shrink-0" /><span className="truncate">{secondaryMeta.branchLabel}</span></span>
<span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0" /><span className="truncate">{secondaryMeta.branchLabel}</span></span>
</div>
) : null}
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0.5"><span className="text-status-success">+{sessionDiffStats.additions}</span><span className="text-status-error">-{sessionDiffStats.deletions}</span></span> : null}
@@ -875,7 +853,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<div className={cn('block min-w-0 flex-1 truncate typography-ui-label font-normal', isActive ? 'text-primary' : 'text-foreground')}>{renderHighlightedText(sessionTitle, normalizedSessionSearchQuery)}</div>
{pendingPermissionCount > 0 ? (
<span className="inline-flex items-center gap-1 rounded bg-destructive/10 px-1 py-0.5 text-[0.7rem] text-destructive flex-shrink-0" title={t('sessions.sidebar.session.status.permissionRequired')} aria-label={t('sessions.sidebar.session.status.permissionRequired')}>
<RiShieldLine className="h-3 w-3" />
<Icon name="shield" className="h-3 w-3" />
<span className="leading-none">{pendingPermissionCount}</span>
</span>
) : null}
@@ -887,7 +865,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<span className="flex-shrink-0">{sessionUpdatedLabel}</span>
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0 text-[0.92em]"><span className="text-status-success/80">+{sessionDiffStats.additions}</span><span className="text-muted-foreground/60">/</span><span className="text-status-error/65">-{sessionDiffStats.deletions}</span></span> : null}
{hasSecondaryProjectLabel ? <span className="truncate">{secondaryMeta?.projectLabel}</span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><RiGitBranchLine className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
</div>
</div>
) : null}
@@ -924,7 +902,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onClick={handleQuickArchiveClick}
onKeyDown={(event) => event.stopPropagation()}
>
<RiArchiveLine className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
<Icon name="archive" className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
</button>
</TooltipTrigger>
<TooltipContent side="left" sideOffset={8}>
@@ -950,7 +928,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onClick={handleMenuTriggerClick}
onKeyDown={(event) => event.stopPropagation()}
>
<RiMore2Line className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
<Icon name="more-2" className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
</button>
</DropdownMenuTrigger>
{sessionMenuContent}
@@ -1,8 +1,8 @@
import React from 'react';
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import type { SessionNode } from './types';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
type ActivityItem = {
node: SessionNode;
@@ -77,7 +77,7 @@ export function SidebarActivitySections({ sections, renderSessionNode }: Props):
aria-expanded={!isCollapsed}
>
<span className="inline-flex h-4 w-4 items-center justify-center text-muted-foreground">
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
<span className="text-[14px] font-normal text-foreground/95">{section.title}</span>
</button>
@@ -1,7 +1,7 @@
import React from 'react';
import { RiInformationLine, RiQuestionLine, RiSettings3Line } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
type Props = {
@@ -32,7 +32,7 @@ export function SidebarFooter({
<Tooltip>
<TooltipTrigger asChild>
<button type="button" onClick={onOpenSettings} className={footerButtonClassName} aria-label={t('sessions.sidebar.footer.actions.settings')}>
<RiSettings3Line className="h-4.5 w-4.5" />
<Icon name="settings-3" className="h-4.5 w-4.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{t('sessions.sidebar.footer.actions.settings')}</p></TooltipContent>
@@ -40,7 +40,7 @@ export function SidebarFooter({
<Tooltip>
<TooltipTrigger asChild>
<button type="button" onClick={onOpenShortcuts} className={footerButtonClassName} aria-label={t('sessions.sidebar.footer.actions.shortcuts')}>
<RiQuestionLine className="h-4.5 w-4.5" />
<Icon name="question" className="h-4.5 w-4.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{t('sessions.sidebar.footer.actions.shortcuts')}</p></TooltipContent>
@@ -48,7 +48,7 @@ export function SidebarFooter({
<Tooltip>
<TooltipTrigger asChild>
<button type="button" onClick={onOpenAbout} className={footerButtonClassName} aria-label={t('sessions.sidebar.footer.actions.aboutOpenChamber')}>
<RiInformationLine className="h-4.5 w-4.5" />
<Icon name="information" className="h-4.5 w-4.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{t('sessions.sidebar.footer.actions.aboutOpenChamber')}</p></TooltipContent>
@@ -7,21 +7,9 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
RiCheckLine,
RiCheckboxMultipleLine,
RiChatNewLine,
RiEqualizer2Line,
RiFolderAddLine,
RiLayoutLeftLine,
RiSearchLine,
RiCloseLine,
RiContractUpDownLine,
RiExpandUpDownLine,
RiCalendarScheduleLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import { Icon } from "@/components/icon/Icon";
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { useI18n } from '@/lib/i18n';
@@ -118,7 +106,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50"
aria-label={t('sessions.sidebar.header.actions.closeSessions')}
>
<RiLayoutLeftLine className="h-[18px] w-[18px]" />
<Icon name="layout-left" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.closeSessions')}</p></TooltipContent>
@@ -132,7 +120,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.addProject')}
>
<RiFolderAddLine className={headerActionIconClass} />
<Icon name="folder-add" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.addProject')}</p></TooltipContent>
@@ -145,7 +133,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.newSession')}
>
<RiChatNewLine className={headerActionIconClass} />
<Icon name="chat-new" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.newSession')}</p></TooltipContent>
@@ -176,7 +164,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.scheduledTasks')}
>
<RiCalendarScheduleLine className={headerActionIconClass} />
<Icon name="calendar-schedule" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.scheduledTasks')}</p></TooltipContent>
@@ -191,7 +179,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
aria-label={t('sessions.sidebar.header.actions.searchSessions')}
aria-expanded={isSessionSearchOpen}
>
<RiSearchLine className={headerActionIconClass} />
<Icon name="search" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.searchSessions')}</p></TooltipContent>
@@ -208,7 +196,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
: t('sessions.sidebar.header.actions.selectSessions')}
aria-pressed={selectionModeEnabled}
>
<RiCheckboxMultipleLine className={headerActionIconClass} />
<Icon name="checkbox-multiple" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
@@ -227,7 +215,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.sessionDisplayMode')}
>
<RiEqualizer2Line className={headerActionIconClass} />
<Icon name="equalizer-2" className={headerActionIconClass} />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
@@ -239,14 +227,14 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.default')}</span>
{displayMode === 'default' ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{displayMode === 'default' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setDisplayMode('minimal')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.minimal')}</span>
{displayMode === 'minimal' ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{displayMode === 'minimal' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
@@ -254,15 +242,15 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.showRecent')}</span>
{showRecentSection ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{showRecentSection ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={collapseAllProjects} className="flex items-center gap-2">
<RiContractUpDownLine className="h-4 w-4" />
<Icon name="contract-up-down" className="h-4 w-4" />
<span>{t('sessions.sidebar.header.displayMode.collapseAll')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={expandAllProjects} className="flex items-center gap-2">
<RiExpandUpDownLine className="h-4 w-4" />
<Icon name="expand-up-down" className="h-4 w-4" />
<span>{t('sessions.sidebar.header.displayMode.expandAll')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -281,7 +269,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
<span>{t('sessions.sidebar.header.search.escapeHint')}</span>
</div>
<div className="relative">
<RiSearchLine className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Icon name="search" className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
ref={sessionSearchInputRef}
value={sessionSearchQuery}
@@ -306,7 +294,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="absolute right-1 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground hover:bg-interactive-hover/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.header.search.clear')}
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
) : null}
</div>
@@ -10,7 +10,7 @@ import {
useDroppable,
type DragEndEvent,
} from '@dnd-kit/core';
import { RiStickyNoteLine } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
export const DraggableSessionRow: React.FC<{
sessionId: string;
@@ -122,7 +122,7 @@ export const SessionFolderDndScope: React.FC<{
}}
className="flex items-center rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-2.5 py-1 shadow-none pointer-events-none"
>
<RiStickyNoteLine className="h-4 w-4 text-muted-foreground mr-2 flex-shrink-0" />
<Icon name="sticky-note" className="h-4 w-4 text-muted-foreground mr-2 flex-shrink-0" />
<div className="min-w-0 flex-1 truncate typography-ui-label font-normal text-foreground">
{activeDragTitle}
</div>
@@ -8,16 +8,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
import {
RiAddLine,
RiArrowDownSLine,
RiArrowRightSLine,
RiCloseLine,
RiFolderLine,
RiMore2Line,
RiNodeTree,
RiPencilAiLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
import { useThemeSystem } from '@/contexts/useThemeSystem';
@@ -104,7 +95,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
setImageFailed(false);
}, [id, projectIconImage?.updatedAt]);
const ProjectIcon = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null;
const projectIconName = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null;
const iconColor = projectColor ? (PROJECT_COLOR_MAP[projectColor] ?? null) : null;
const imageUrl = !imageFailed
? getProjectIconImageUrl({ id, iconImage: projectIconImage }, {
@@ -186,7 +177,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
'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 ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
{imageUrl ? (
<span
@@ -204,10 +195,10 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
onError={() => setImageFailed(true)}
/>
</span>
) : ProjectIcon ? (
<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} />
) : projectIconName ? (
<Icon name={projectIconName} 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={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} />
<Icon name="folder" 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 className={cn(
@@ -242,7 +233,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
)}
aria-label={t('sessions.sidebar.project.actions.newWorktree')}
>
<RiNodeTree className="h-4 w-4" />
<Icon name="node-tree" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
@@ -271,25 +262,25 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
onMouseDown={handleMenuTriggerMouseDown}
onClick={handleMenuTriggerClick}
>
<RiMore2Line className="h-3.5 w-3.5" />
<Icon name="more-2" className="h-3.5 w-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[180px]">
{showCreateButtons && !isRepo && !hideDirectoryControls && onNewSession && (
<DropdownMenuItem onClick={onNewSession}>
<RiAddLine className="mr-1.5 h-4 w-4" />
<Icon name="add" className="mr-1.5 h-4 w-4" />
{t('sessions.sidebar.project.actions.newSession')}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onRenameStart}>
<RiPencilAiLine className="mr-1.5 h-4 w-4" />
<Icon name="pencil-ai" className="mr-1.5 h-4 w-4" />
{t('sessions.sidebar.session.menu.rename')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={onClose}
className="text-destructive focus:text-destructive"
>
<RiCloseLine className="mr-1.5 h-4 w-4" />
<Icon name="close" className="mr-1.5 h-4 w-4" />
{t('sessions.sidebar.project.actions.closeProject')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -314,7 +305,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
? t('sessions.sidebar.project.actions.newDraftSession')
: t('sessions.sidebar.project.actions.newSession')}
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>