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
@@ -1,5 +1,4 @@
import React from 'react';
import { RiArrowLeftLine } from '@remixicon/react';
import type { Message, Part, Session } from '@opencode-ai/sdk/v2';
import { ChatInput } from './ChatInput';
@@ -19,6 +18,7 @@ import { useChatTurnNavigation } from './hooks/useChatTurnNavigation';
import { useDeviceInfo } from '@/lib/device';
import { Button } from '@/components/ui/button';
import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar';
import { Icon } from "@/components/icon/Icon";
import type { PermissionRequest } from '@/types/permission';
import type { QuestionRequest } from '@/types/question';
import { cn } from '@/lib/utils';
@@ -521,7 +521,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
? t('chat.container.returnToParent.titleNamed', { title: parentSession.title })
: t('chat.container.returnToParent.title')}
>
<RiArrowLeftLine className="h-4 w-4" />
<Icon name="arrow-left" className="h-4 w-4" />
{t('chat.container.returnToParent.label')}
</Button>
) : null;
@@ -657,7 +657,6 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
};
}, [currentSessionId, isDesktopExpandedInput, navigation, scrollRef]);
React.useLayoutEffect(() => {
const container = scrollRef.current;
if (!container) return;
@@ -1,8 +1,8 @@
import React from 'react';
import { RiChat3Line, RiRestartLine } from '@remixicon/react';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
interface ChatErrorBoundaryState {
hasError: boolean;
@@ -57,7 +57,7 @@ class ChatErrorBoundaryView extends React.Component<ChatErrorBoundaryViewProps,
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="flex items-center justify-center gap-2 text-destructive">
<RiChat3Line className="h-5 w-5" />
<Icon name="chat-3" className="h-5 w-5" />
{this.props.texts.title}
</CardTitle>
</CardHeader>
@@ -83,7 +83,7 @@ class ChatErrorBoundaryView extends React.Component<ChatErrorBoundaryViewProps,
<div className="flex gap-2">
<Button onClick={this.handleReset} variant="outline" className="flex-1">
<RiRestartLine className="h-4 w-4 mr-2" />
<Icon name="restart" className="h-4 w-4 mr-2" />
{this.props.texts.resetAction}
</Button>
</div>
+24 -38
View File
@@ -1,20 +1,5 @@
import React from 'react';
import { Textarea } from '@/components/ui/textarea';
import {
RiAddCircleLine,
RiAiAgentLine,
RiAttachment2,
RiCloseLine,
RiCommandLine,
RiExternalLinkLine,
RiFolderLine,
RiFullscreenLine,
RiGitPullRequestLine,
RiShieldCheckLine,
RiShieldUserLine,
RiGithubLine,
RiSendPlane2Line,
} from '@remixicon/react';
import { BrowserVoiceButton } from '@/components/voice';
// sessionStore removed — currentSessionId comes from useSessionUIStore
import { useConfigStore } from '@/stores/useConfigStore';
@@ -61,6 +46,7 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSepa
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { GitHubIssuePickerDialog } from '@/components/session/GitHubIssuePickerDialog';
import { GitHubPrPickerDialog } from '@/components/session/GitHubPrPickerDialog';
import { Icon } from "@/components/icon/Icon";
import { useChatSearchDirectory } from '@/hooks/useChatSearchDirectory';
import { opencodeClient } from '@/lib/opencode/client';
import { useProjectsStore } from '@/stores/useProjectsStore';
@@ -291,7 +277,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
title={t('chat.chatInput.actions.commands')}
aria-label={t('chat.chatInput.actions.commands')}
>
<RiCommandLine className={cn(iconSizeClass)} />
<Icon name="command" className={cn(iconSizeClass)} />
</button>
) : null}
<input
@@ -312,7 +298,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
title={t('chat.chatInput.actions.attachFiles')}
aria-label={t('chat.chatInput.actions.attachFiles')}
>
<RiAttachment2 className={cn(iconSizeClass, 'text-current')} />
<Icon name="attachment-2" className={cn(iconSizeClass, 'text-current')} />
</button>
) : (
<DropdownMenu>
@@ -323,7 +309,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
title={t('chat.chatInput.actions.addAttachment')}
aria-label={t('chat.chatInput.actions.addAttachment')}
>
<RiAddCircleLine className={cn(iconSizeClass, 'text-current')} />
<Icon name="add-circle" className={cn(iconSizeClass, 'text-current')} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
@@ -332,7 +318,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
requestAnimationFrame(handlePickLocalFiles);
}}
>
<RiAttachment2 />
<Icon name="attachment-2"/>
{t('chat.chatInput.actions.attachFiles')}
</DropdownMenuItem>
<DropdownMenuItem
@@ -340,7 +326,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
requestAnimationFrame(openIssuePicker);
}}
>
<RiGithubLine />
<Icon name="github"/>
{t('chat.chatInput.actions.linkGithubIssue')}
</DropdownMenuItem>
<DropdownMenuItem
@@ -348,7 +334,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
requestAnimationFrame(openPrPicker);
}}
>
<RiGitPullRequestLine />
<Icon name="git-pull-request"/>
{t('chat.chatInput.actions.linkGithubPr')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -364,7 +350,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
title={t('chat.chatInput.actions.modelAgentSettings')}
aria-label={t('chat.chatInput.actions.modelAgentSettings')}
>
<RiAiAgentLine className={cn(iconSizeClass, 'text-current')} />
<Icon name="ai-agent" className={cn(iconSizeClass, 'text-current')} />
</button>
) : null}
</div>
@@ -427,9 +413,9 @@ const PermissionAutoAcceptButton = React.memo(function PermissionAutoAcceptButto
title={ariaLabel}
>
{permissionAutoAcceptEnabled ? (
<RiShieldCheckLine className={cn(iconSizeClass)} style={{ color: 'var(--status-info)' }} />
<Icon name="shield-check" className={cn(iconSizeClass)} style={{ color: 'var(--status-info)' }} />
) : (
<RiShieldUserLine className={cn(iconSizeClass)} />
<Icon name="shield-user" className={cn(iconSizeClass)} />
)}
</button>
);
@@ -480,7 +466,7 @@ const FocusModeButton = React.memo(function FocusModeButton(props: FocusModeButt
aria-label={t('chat.chatInput.focusMode.toggleAria')}
aria-pressed={isExpandedInput}
>
<RiFullscreenLine className={cn(iconSizeClass)} />
<Icon name="fullscreen" className={cn(iconSizeClass)} />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={8}>
@@ -547,7 +533,7 @@ const ComposerActionButtons = React.memo(function ComposerActionButtons(props: C
)}
aria-label={t('chat.chatInput.actions.sendMessageAria')}
>
<RiSendPlane2Line className={cn(sendIconSizeClass)} />
<Icon name="send-plane-2" className={cn(sendIconSizeClass)} />
</button>
);
@@ -574,7 +560,7 @@ const ComposerActionButtons = React.memo(function ComposerActionButtons(props: C
)}
aria-label={t('chat.chatInput.actions.queueMessageAria')}
>
<RiSendPlane2Line className={cn(sendIconSizeClass, '-rotate-90')} />
<Icon name="send-plane-2" className={cn(sendIconSizeClass, '-rotate-90')} />
</button>
) : null}
<button
@@ -3237,7 +3223,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
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 = getProjectIconColor(project.color);
return (
@@ -3249,10 +3235,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
>
<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">{getProjectDisplayLabel(project)}</span>
</span>
@@ -3377,7 +3363,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
aria-label={t('chat.chatInput.devServerLogsRemove')}
title={t('chat.chatInput.devServerLogsRemove')}
>
<RiCloseLine className="h-3 w-3" />
<Icon name="close" className="h-3 w-3" />
</button>
</div>
) : null}
@@ -3398,7 +3384,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
aria-label={t('chat.chatInput.previewContextRemove')}
title={t('chat.chatInput.previewContextRemove')}
>
<RiCloseLine className="h-3 w-3" />
<Icon name="close" className="h-3 w-3" />
</button>
</div>
) : null}
@@ -3439,7 +3425,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors"
aria-label={t('chat.chatInput.linked.issue.openInBrowserAria')}
>
<RiExternalLinkLine className="h-4 w-4 text-muted-foreground" />
<Icon name="external-link" className="h-4 w-4 text-muted-foreground" />
</a>
<button
type="button"
@@ -3450,7 +3436,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
aria-label={t('chat.chatInput.linked.issue.removeAria')}
title={t('chat.chatInput.linked.issue.removeAria')}
>
<RiCloseLine className="h-4 w-4 text-muted-foreground" />
<Icon name="close" className="h-4 w-4 text-muted-foreground" />
</button>
</span>
</div>
@@ -3492,7 +3478,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors"
aria-label={t('chat.chatInput.linked.pr.openInBrowserAria')}
>
<RiExternalLinkLine className="h-4 w-4 text-muted-foreground" />
<Icon name="external-link" className="h-4 w-4 text-muted-foreground" />
</a>
<button
type="button"
@@ -3503,7 +3489,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
aria-label={t('chat.chatInput.linked.pr.removeAria')}
title={t('chat.chatInput.linked.pr.removeAria')}
>
<RiCloseLine className="h-4 w-4 text-muted-foreground" />
<Icon name="close" className="h-4 w-4 text-muted-foreground" />
</button>
</span>
</div>
@@ -3623,7 +3609,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
title={t('chat.chatInput.actions.attachFiles')}
aria-label={t('chat.chatInput.actions.attachFiles')}
>
<RiAttachment2 className={cn(iconSizeClass, 'text-current')} />
<Icon name="attachment-2" className={cn(iconSizeClass, 'text-current')} />
</button>
</div>
<p className="mt-2 typography-ui-label text-muted-foreground">
@@ -1,11 +1,11 @@
import React from 'react';
import { RiCommandLine, RiFileLine, RiFlashlightLine, RiRefreshLine, RiScissorsLine, RiTerminalBoxLine, RiArrowGoBackLine, RiArrowGoForwardLine, RiSearchEyeLine, RiTimeLine } from '@remixicon/react';
import { cn, fuzzyMatch } from '@/lib/utils';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionMessages } from '@/sync/sync-context';
import { useCommandsStore } from '@/stores/useCommandsStore';
import { useSkillsStore } from '@/stores/useSkillsStore';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
type CommandSource = 'openchamber' | 'opencode';
@@ -243,26 +243,26 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
switch (command.name) {
case 'init':
return <RiFileLine className="h-3.5 w-3.5 text-green-500" />;
return <Icon name="file" className="h-3.5 w-3.5 text-green-500" />;
case 'undo':
return <RiArrowGoBackLine className="h-3.5 w-3.5 text-orange-500" />;
return <Icon name="arrow-go-back" className="h-3.5 w-3.5 text-orange-500" />;
case 'redo':
return <RiArrowGoForwardLine className="h-3.5 w-3.5 text-orange-500" />;
return <Icon name="arrow-go-forward" className="h-3.5 w-3.5 text-orange-500" />;
case 'timeline':
return <RiTimeLine className="h-3.5 w-3.5" />;
return <Icon name="time" className="h-3.5 w-3.5" />;
case 'compact':
return <RiScissorsLine className="h-3.5 w-3.5 text-purple-500" />;
return <Icon name="scissors" className="h-3.5 w-3.5 text-purple-500" />;
case 'review':
return <RiSearchEyeLine className="h-3.5 w-3.5 text-blue-500" />;
return <Icon name="search-eye" className="h-3.5 w-3.5 text-blue-500" />;
case 'test':
case 'build':
case 'run':
return <RiTerminalBoxLine className="h-3.5 w-3.5 text-cyan-500" />;
return <Icon name="terminal-box" className="h-3.5 w-3.5 text-cyan-500" />;
default:
if (command.isBuiltIn) {
return <RiFlashlightLine className="h-3.5 w-3.5 text-yellow-500" />;
return <Icon name="flashlight" className="h-3.5 w-3.5 text-yellow-500" />;
}
return <RiCommandLine className="h-3.5 w-3.5 text-muted-foreground" />;
return <Icon name="command" className="h-3.5 w-3.5 text-muted-foreground" />;
}
};
@@ -315,7 +315,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2">
{loading ? (
<div className="flex items-center justify-center py-4">
<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>
@@ -1,5 +1,4 @@
import React, { useRef, memo } from 'react';
import { RiAttachment2, RiCloseLine, RiFileImageLine, RiFileLine, RiFilePdfLine, RiGithubLine, RiGitPullRequestLine, RiAddLine, RiPushpin2Line } from '@remixicon/react';
import { useInputStore } from '@/sync/input-store';
import type { AttachedFile } from '@/sync/session-ui-store';
import { useUIStore } from '@/stores/useUIStore';
@@ -9,6 +8,7 @@ import { openExternalUrl } from '@/lib/url';
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
import { useDeviceInfo } from '@/lib/device';
@@ -108,7 +108,7 @@ export const FileAttachmentButton = memo(() => {
)}
aria-label={t('chat.fileAttachment.actions.attachAria')}
>
<RiAttachment2 className={iconSizeClass} />
<Icon name="attachment-2" className={iconSizeClass} />
</button>
</TooltipTrigger>
<TooltipContent side="top">
@@ -171,7 +171,7 @@ const ImagePreview = memo(({ file, onRemove }: ImagePreviewProps) => {
className="flex items-center justify-center h-5 w-5 flex-shrink-0 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer"
aria-label={t('chat.fileAttachment.actions.removeNamed', { name: displayName })}
>
<RiCloseLine className="h-4 w-4 text-muted-foreground" />
<Icon name="close" className="h-4 w-4 text-muted-foreground" />
</span>
</button>
);
@@ -194,7 +194,7 @@ const ImagePreview = memo(({ file, onRemove }: ImagePreviewProps) => {
title={t('chat.fileAttachment.actions.removeImage')}
aria-label={t('chat.fileAttachment.actions.removeNamed', { name: displayName })}
>
<RiCloseLine className="h-2.5 w-2.5" />
<Icon name="close" className="h-2.5 w-2.5" />
</button>
</div>
);
@@ -263,7 +263,7 @@ const FileChip = memo(({ file, onRemove }: FileChipProps) => {
className="flex items-center justify-center h-5 w-5 flex-shrink-0 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer"
aria-label={t('chat.fileAttachment.actions.removeNamed', { name: displayName })}
>
<RiCloseLine className="h-4 w-4 text-muted-foreground" />
<Icon name="close" className="h-4 w-4 text-muted-foreground" />
</span>
</button>
);
@@ -301,7 +301,7 @@ const VSCodeFileChip = memo(({ file, onRemove }: FileChipProps) => {
aria-label={t('chat.fileAttachment.activeEditor.remove')}
title={t('chat.fileAttachment.activeEditor.remove')}
>
<RiCloseLine className="h-4 w-4 text-muted-foreground" />
<Icon name="close" className="h-4 w-4 text-muted-foreground" />
</span>
<FileTypeIcon filePath={file.filename} extension={extension} className="h-4 w-4" />
<span className={cn('text-foreground', isSelectionAttachment ? 'whitespace-nowrap' : 'truncate max-w-[200px]')}>
@@ -450,7 +450,7 @@ export const ActiveEditorFileSuggestion = memo(() => {
onClick={() => { void handlePinSelection(); }}
className="flex items-center justify-center h-5 w-5 flex-shrink-0 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer"
>
<RiPushpin2Line className="h-4 w-4" />
<Icon name="pushpin-2" className="h-4 w-4" />
</button>
<FileTypeIcon filePath={fileName} extension={ext} className="h-4 w-4 flex-shrink-0" />
<span className="text-xs whitespace-nowrap">{`${displayName}:${selectionRange}`}</span>
@@ -469,7 +469,7 @@ export const ActiveEditorFileSuggestion = memo(() => {
onClick={handleAddFile}
className="flex items-center justify-center h-5 w-5 flex-shrink-0 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer"
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
</button>
<FileTypeIcon filePath={fileName} extension={ext} className="h-4 w-4 flex-shrink-0" />
<span className="text-xs truncate max-w-[220px]">{displayName}</span>
@@ -610,9 +610,9 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
className="inline-flex items-center bg-muted/30 border border-border/30 typography-meta gap-1 px-2 py-0.5 rounded-lg text-foreground hover:text-primary transition-colors"
>
{githubLinkKind === 'pr' ? (
<RiGitPullRequestLine className="text-muted-foreground h-3.5 w-3.5" />
<Icon name="git-pull-request" className="text-muted-foreground h-3.5 w-3.5" />
) : (
<RiGithubLine className="text-muted-foreground h-3.5 w-3.5" />
<Icon name="github" className="text-muted-foreground h-3.5 w-3.5" />
)}
<div className="overflow-hidden max-w-[220px]">
<span className="truncate block" title={fileName}>{fileName}</span>
@@ -621,7 +621,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
) : (
<div className="inline-flex items-center bg-muted/30 border border-border/30 typography-meta gap-1 px-2 py-0.5 rounded-lg">
{file.mime?.includes('pdf') ? (
<RiFilePdfLine className="text-muted-foreground h-3.5 w-3.5" />
<Icon name="file-pdf" className="text-muted-foreground h-3.5 w-3.5" />
) : (
<FileTypeIcon filePath={fileName} extension={ext} className="text-muted-foreground h-3.5 w-3.5" />
)}
@@ -668,7 +668,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
/>
) : (
<div className="h-full w-full flex items-center justify-center bg-muted/30 text-muted-foreground">
<RiFileImageLine className="h-6 w-6" />
<Icon name="file-image" className="h-6 w-6" />
</div>
)}
<span className="sr-only">{filename}</span>
@@ -735,9 +735,9 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
>
<div className="flex-shrink-0">
{githubLinkKind === 'pr' ? (
<RiGitPullRequestLine className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
<Icon name="git-pull-request" className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
) : (
<RiGithubLine className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
<Icon name="github" className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
)}
</div>
<div className="flex-1 min-w-0">
@@ -779,11 +779,11 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
>
<div className="flex-shrink-0">
{file.mime?.startsWith('image/') ? (
<RiFileImageLine className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
<Icon name="file-image" className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
) : file.mime?.includes('pdf') ? (
<RiFilePdfLine className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
<Icon name="file-pdf" className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
) : (
<RiFileLine className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
<Icon name="file" className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
)}
</div>
<div className="flex-1 min-w-0">
@@ -1,5 +1,4 @@
import React from 'react';
import { RiCodeLine, RiFileImageLine, RiFileLine, RiFilePdfLine, RiFolder3Fill, RiRefreshLine } from '@remixicon/react';
import { cn, truncatePathMiddle } from '@/lib/utils';
import { useFileSearchStore } from '@/stores/useFileSearchStore';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -9,6 +8,7 @@ import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useChatSearchDirectory } from '@/hooks/useChatSearchDirectory';
import type { ProjectFileSearchHit } from '@/lib/opencode/client';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Icon } from "@/components/icon/Icon";
import { useDirectoryShowHidden } from '@/lib/directoryShowHidden';
import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
import { useI18n } from '@/lib/i18n';
@@ -431,20 +431,20 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
case 'tsx':
case 'js':
case 'jsx':
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-info)]" />;
return <Icon name="code" className="h-3.5 w-3.5 text-[var(--status-info)]" />;
case 'json':
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-warning)]" />;
return <Icon name="code" className="h-3.5 w-3.5 text-[var(--status-warning)]" />;
case 'md':
case 'mdx':
return <RiFileLine className="h-3.5 w-3.5 text-muted-foreground" />;
return <Icon name="file" className="h-3.5 w-3.5 text-muted-foreground" />;
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
case 'svg':
return <RiFileImageLine className="h-3.5 w-3.5 text-[var(--status-success)]" />;
return <Icon name="file-image" className="h-3.5 w-3.5 text-[var(--status-success)]" />;
default:
return <RiFilePdfLine className="h-3.5 w-3.5 text-muted-foreground" />;
return <Icon name="file-pdf" className="h-3.5 w-3.5 text-muted-foreground" />;
}
};
@@ -499,7 +499,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0">
{(!scopeResultsToActiveTab || activeTab === 'files') && loading ? (
<div className="flex items-center justify-center py-4">
<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="pb-2">
@@ -550,7 +550,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
onClick={() => handleFileSelect(dir)}
onMouseEnter={() => setSelectedIndex(rowIndex)}
>
<RiFolder3Fill className="h-3.5 w-3.5 text-primary/60" />
<Icon name="folder-3-fill" className="h-3.5 w-3.5 text-primary/60" />
<span className="flex-1 min-w-0 truncate" aria-label={relativePath}>
{displayPath}
</span>
@@ -11,9 +11,9 @@ import remend from 'remend';
import { FadeInOnReveal } from './message/FadeInOnReveal';
import type { Part } from '@opencode-ai/sdk/v2';
import { cn } from '@/lib/utils';
import { RiFileCopyLine, RiCheckLine, RiDownloadLine, RiEyeLine, RiCodeLine } from '@remixicon/react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { copyTextToClipboard } from '@/lib/clipboard';
import { useI18n } from '@/lib/i18n';
@@ -228,7 +228,7 @@ const TableCopyButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement | nul
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.table.actions.copyTitle')}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
{showMenu && (
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-none">
@@ -286,7 +286,7 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement |
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.table.actions.downloadTitle')}
>
<RiDownloadLine className="size-3.5" />
<Icon name="download" className="size-3.5" />
</button>
{showMenu && (
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-none">
@@ -425,7 +425,7 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.mermaid.actions.copyTitle')}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
</div>
</div>
@@ -449,7 +449,7 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.mermaid.actions.copyTitle')}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
</div>
</div>
@@ -472,14 +472,14 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.mermaid.actions.copySourceTitle')}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
<button
onClick={handleDownloadSvg}
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.mermaid.actions.downloadSvgTitle')}
>
{downloaded ? <RiCheckLine className="size-3.5" /> : <RiDownloadLine className="size-3.5" />}
{downloaded ? <Icon name="check" className="size-3.5" /> : <Icon name="download" className="size-3.5" />}
</button>
</div>
</div>
@@ -793,7 +793,7 @@ const MarkdownCodeBlock: React.FC<{
aria-pressed={viewMode === 'preview'}
aria-label={viewMode === 'preview' ? 'Show code' : 'Preview HTML'}
>
{viewMode === 'preview' ? <RiCodeLine className="size-3.5" /> : <RiEyeLine className="size-3.5" />}
{viewMode === 'preview' ? <Icon name="code" className="size-3.5" /> : <Icon name="eye" className="size-3.5" />}
</button>
) : null}
{canPreview ? (
@@ -804,7 +804,7 @@ const MarkdownCodeBlock: React.FC<{
title="Download HTML"
aria-label="Download HTML"
>
<RiDownloadLine className="size-3.5" />
<Icon name="download" className="size-3.5" />
</button>
) : null}
<button
@@ -814,7 +814,7 @@ const MarkdownCodeBlock: React.FC<{
title={copied ? 'Copied' : 'Copy code'}
aria-label={copied ? 'Copied' : 'Copy code'}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
</div>
</div>
@@ -959,7 +959,7 @@ const buildMarkdownComponents = ({
title={previewTitle ?? previewLabel ?? 'Open preview pane'}
data-loopback-preview-trigger="true"
>
<RiEyeLine className="size-3" aria-hidden="true" />
<Icon name="eye" className="size-3" aria-hidden="true"/>
<span className="font-medium">{previewLabel ?? 'Preview'}</span>
</button>
) : null}
@@ -9,15 +9,6 @@ import type { Session } from '@opencode-ai/sdk/v2';
import type { ProjectEntry } from '@/lib/api/types';
import { cn, formatDirectoryName } from '@/lib/utils';
import { getAgentColor } from '@/lib/agentColors';
import {
RiLoader4Line,
RiAddLine,
RiDragMove2Line,
RiDeleteBinLine,
RiEditLine,
RiArrowUpLine,
RiArrowDownLine,
} from '@remixicon/react';
import {
DndContext,
closestCenter,
@@ -51,6 +42,7 @@ import { Button } from '@/components/ui/button';
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
import { useDrawerSwipe } from '@/hooks/useDrawerSwipe';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { Icon } from "@/components/icon/Icon";
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { useNotificationStore } from '@/sync/notification-store';
import { useI18n } from '@/lib/i18n';
@@ -275,7 +267,7 @@ function useProjectStatus(
function StatusIndicator({ isRunning, needsAttention }: { isRunning: boolean; needsAttention: boolean }) {
if (isRunning) {
return <RiLoader4Line className="h-2.5 w-2.5 animate-spin text-[var(--status-info)]" />;
return <Icon name="loader-4" className="h-2.5 w-2.5 animate-spin text-[var(--status-info)]" />;
}
if (needsAttention) {
return <div className="h-1.5 w-1.5 rounded-full bg-[var(--status-error)]" />;
@@ -287,7 +279,7 @@ function RunningIndicator({ count }: { count: number }) {
if (count === 0) return null;
return (
<span className="flex items-center gap-1 text-[13px] text-[var(--status-info)]">
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
<Icon name="loader-4" className="h-3.5 w-3.5 animate-spin" />
{count}
</span>
);
@@ -369,10 +361,9 @@ function SessionItem({
className="flex-shrink-0"
title={`Sub-session: ${getSessionTitle(child)}`}
>
<RiLoader4Line
className="h-2.5 w-2.5 animate-spin"
style={{ color: `var(${childColor.var})` }}
/>
<Icon name="loader-4" className="h-2.5 w-2.5 animate-spin"
style={{ color: `var(${childColor.var})` }}/>
</div>
);
})}
@@ -428,7 +419,7 @@ function SessionStatusHeader({
childIndicators = []
}: SessionStatusHeaderProps) {
const [imageFailed, setImageFailed] = React.useState(false);
const ProjectIcon = currentProjectIcon ? PROJECT_ICON_MAP[currentProjectIcon] : null;
const projectIconName = currentProjectIcon ? PROJECT_ICON_MAP[currentProjectIcon] : null;
const imageUrl = !imageFailed ? currentProjectIconImageUrl : null;
const projectColorVar = currentProjectColor ? (PROJECT_COLOR_MAP[currentProjectColor] ?? null) : null;
const extraCount = childIndicators.length > 3 ? childIndicators.length - 3 : 0;
@@ -459,8 +450,8 @@ function SessionStatusHeader({
onError={() => setImageFailed(true)}
/>
</span>
) : ProjectIcon && (
<ProjectIcon
) : projectIconName && (
<Icon name={projectIconName}
className="h-2.5 w-2.5"
style={projectColorVar ? { color: projectColorVar } : undefined}
/>
@@ -492,10 +483,9 @@ function SessionStatusHeader({
className="flex-shrink-0"
title={`Sub-session: ${child.session.title || 'Untitled'}`}
>
<RiLoader4Line
className="h-2.5 w-2.5 animate-spin"
style={{ color: `var(${childColor.var})` }}
/>
<Icon name="loader-4" className="h-2.5 w-2.5 animate-spin"
style={{ color: `var(${childColor.var})` }}/>
</div>
);
})}
@@ -513,8 +503,6 @@ function SessionStatusHeader({
);
}
// Hook for long press with movement detection
function useLongPress(
onLongPress: () => void,
@@ -625,7 +613,7 @@ function SortableProjectItem({
};
const [imageFailed, setImageFailed] = React.useState(false);
const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const projectIconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const projectIconImageUrl = !imageFailed
? getProjectIconImageUrl(project, {
themeVariant: currentTheme.metadata.variant,
@@ -650,7 +638,7 @@ function SortableProjectItem({
{...attributes}
{...listeners}
>
<RiDragMove2Line className="h-4 w-4" />
<Icon name="drag-move-2" className="h-4 w-4" />
</button>
{/* Project info */}
@@ -668,8 +656,8 @@ function SortableProjectItem({
onError={() => setImageFailed(true)}
/>
</span>
) : ProjectIcon ? (
<ProjectIcon
) : projectIconName ? (
<Icon name={projectIconName}
className="h-5 w-5 flex-shrink-0"
style={projectColorVar ? { color: projectColorVar } : undefined}
/>
@@ -690,7 +678,7 @@ function SortableProjectItem({
disabled={isFirst}
className="p-1.5 rounded text-[var(--surface-mutedForeground)] hover:text-[var(--surface-foreground)] hover:bg-[var(--interactive-hover)] disabled:opacity-30 disabled:cursor-not-allowed"
>
<RiArrowUpLine className="h-4 w-4" />
<Icon name="arrow-up" className="h-4 w-4" />
</button>
<button
type="button"
@@ -698,7 +686,7 @@ function SortableProjectItem({
disabled={isLast}
className="p-1.5 rounded text-[var(--surface-mutedForeground)] hover:text-[var(--surface-foreground)] hover:bg-[var(--interactive-hover)] disabled:opacity-30 disabled:cursor-not-allowed"
>
<RiArrowDownLine className="h-4 w-4" />
<Icon name="arrow-down" className="h-4 w-4" />
</button>
<div className="w-px h-5 bg-[var(--interactive-border)] mx-1" />
@@ -709,7 +697,7 @@ function SortableProjectItem({
onClick={onEdit}
className="p-1.5 rounded text-[var(--surface-mutedForeground)] hover:text-[var(--primary-base)] hover:bg-[var(--primary-base)]/10"
>
<RiEditLine className="h-4 w-4" />
<Icon name="edit" className="h-4 w-4" />
</button>
{/* Delete button */}
@@ -718,7 +706,7 @@ function SortableProjectItem({
onClick={onDelete}
className="p-1.5 rounded text-[var(--surface-mutedForeground)] hover:text-[var(--status-error)] hover:bg-[var(--status-error)]/10"
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</div>
</div>
@@ -863,7 +851,7 @@ function ProjectButton({
}: ProjectButtonProps) {
const { currentTheme } = useThemeSystem();
const [imageFailed, setImageFailed] = React.useState(false);
const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const projectIconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const projectIconImageUrl = !imageFailed
? getProjectIconImageUrl(project, {
themeVariant: currentTheme.metadata.variant,
@@ -900,7 +888,7 @@ function ProjectButton({
{/* Status indicators */}
<div className="flex items-center gap-0.5">
{status.hasRunning && (
<RiLoader4Line className="h-2.5 w-2.5 animate-spin text-[var(--status-info)]" />
<Icon name="loader-4" className="h-2.5 w-2.5 animate-spin text-[var(--status-info)]" />
)}
{!status.hasRunning && status.hasUnread && (
<div className="h-1.5 w-1.5 rounded-full bg-[var(--status-error)]" />
@@ -921,8 +909,8 @@ function ProjectButton({
onError={() => setImageFailed(true)}
/>
</span>
) : ProjectIcon && (
<ProjectIcon
) : projectIconName && (
<Icon name={projectIconName}
className="h-3.5 w-3.5"
style={projectColorVar ? { color: projectColorVar } : undefined}
/>
@@ -1021,7 +1009,7 @@ function ProjectBar({
className="flex items-center justify-center !py-1.5 px-2 rounded-md border border-[var(--primary-base)]/60 bg-[var(--primary-base)]/5 text-[var(--primary-base)]/80 hover:text-[var(--primary-base)] hover:bg-[var(--primary-base)]/10 !min-h-0"
aria-label={t('chat.mobileStatus.projects.addAria')}
>
<RiAddLine className="h-3 w-3" />
<Icon name="add" className="h-3 w-3" />
</button>
</div>
);
@@ -1097,7 +1085,7 @@ function ProjectBar({
className="flex items-center justify-center !py-1.5 px-2 rounded-md border border-[var(--primary-base)]/60 bg-[var(--primary-base)]/5 text-[var(--primary-base)]/80 hover:text-[var(--primary-base)] hover:bg-[var(--primary-base)]/10 shrink-0 !min-h-0"
aria-label={t('chat.mobileStatus.projects.addAria')}
>
<RiAddLine className="h-3.5 w-3.5" />
<Icon name="add" className="h-3.5 w-3.5" />
</button>
{/* Delete confirmation dialog */}
@@ -1,5 +1,4 @@
import React from 'react';
import type { ComponentType } from 'react';
import {
DndContext,
PointerSensor,
@@ -10,31 +9,6 @@ import {
} from '@dnd-kit/core';
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS as DndCSS } from '@dnd-kit/utilities';
import {
RiAddLine,
RiAiAgentLine,
RiArrowDownSLine,
RiArrowGoBackLine,
RiArrowRightSLine,
RiBrainAi3Line,
RiCheckLine,
RiCheckboxCircleLine,
RiCloseCircleLine,
RiDraggable,
RiFileImageLine,
RiFileMusicLine,
RiFilePdfLine,
RiFileVideoLine,
RiLoader4Line,
RiPencilAiLine,
RiQuestionLine,
RiSearchLine,
RiStarFill,
RiStarLine,
RiText,
RiTimeLine,
RiToolsLine,
} from '@remixicon/react';
import type { EditPermissionMode } from '@/stores/types/sessionTypes';
import type { ModelMetadata } from '@/types';
import {
@@ -51,6 +25,8 @@ import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { TextLoop } from '@/components/ui/TextLoop';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
import { isDesktopShell } from '@/lib/desktop';
import { getAgentColor } from '@/lib/agentColors';
@@ -71,8 +47,8 @@ import { formatEffortLabel, getCycledPrimaryAgentName, type MobileControlsPanel
import { useI18n } from '@/lib/i18n';
import { useOpenCodeReadiness } from '@/hooks/useOpenCodeReadiness';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type IconComponent = ComponentType<any>;
type IconComponent = IconName;
type ProviderModel = Record<string, unknown> & { id?: string; name?: string };
@@ -172,13 +148,13 @@ interface CapabilityDefinition {
const CAPABILITY_DEFINITIONS: CapabilityDefinition[] = [
{
key: 'tool_call',
icon: RiToolsLine,
icon: "tools",
label: 'Tool calling',
isActive: (metadata) => metadata?.tool_call === true,
},
{
key: 'reasoning',
icon: RiBrainAi3Line,
icon: "brain-ai-3",
label: 'Reasoning',
isActive: (metadata) => metadata?.reasoning === true,
},
@@ -198,11 +174,11 @@ type ModalityIcon = {
type ModelApplyResult = 'applied' | 'provider-missing' | 'model-missing';
const MODALITY_ICON_MAP: Record<string, ModalityIconDefinition> = {
text: { icon: RiText, label: 'Text' },
image: { icon: RiFileImageLine, label: 'Image' },
video: { icon: RiFileVideoLine, label: 'Video' },
audio: { icon: RiFileMusicLine, label: 'Audio' },
pdf: { icon: RiFilePdfLine, label: 'PDF' },
text: { icon: "text", label: 'Text' },
image: { icon: "file-image", label: 'Image' },
video: { icon: "file-video", label: 'Video' },
audio: { icon: "file-music", label: 'Audio' },
pdf: { icon: "file-pdf", label: 'PDF' },
};
const normalizeModality = (value: string) => value.trim().toLowerCase();
@@ -587,15 +563,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const iconStyle = { color: iconColor };
if (mode === 'full') {
return <RiPencilAiLine className={combinedClassName} style={iconStyle} />;
return <Icon name="pencil-ai" className={combinedClassName} style={iconStyle} />;
}
if (mode === 'allow') {
return <RiCheckboxCircleLine className={combinedClassName} style={iconStyle} />;
return <Icon name="checkbox-circle" className={combinedClassName} style={iconStyle} />;
}
if (mode === 'deny') {
return <RiCloseCircleLine className={combinedClassName} style={iconStyle} />;
return <Icon name="close-circle" className={combinedClassName} style={iconStyle} />;
}
return <RiQuestionLine className={combinedClassName} style={iconStyle} />;
return <Icon name="question" className={combinedClassName} style={iconStyle} />;
}, [editToggleIconClass]);
const currentProvider = getCurrentProvider();
@@ -1359,7 +1335,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return name.charAt(0).toUpperCase() + name.slice(1);
};
const renderIconBadge = (IconComp: IconComponent, label: string, key: string) => (
const renderIconBadge = (iconName: IconComponent, label: string, key: string) => (
<span
key={key}
className="flex h-5 w-5 items-center justify-center rounded-xl bg-muted/60 text-muted-foreground"
@@ -1367,7 +1343,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
aria-label={label}
role="img"
>
<IconComp className="h-3.5 w-3.5" />
<Icon name={iconName} className="h-3.5 w-3.5" />
</span>
);
@@ -1577,7 +1553,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
</div>
)}
{}
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
<div className="typography-micro text-muted-foreground mb-1">{t('chat.modelControls.permissions')}</div>
@@ -1617,7 +1592,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
<div className="flex items-center justify-between">
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
<RiCheckboxCircleLine className="h-4 w-4 text-foreground" />
<Icon name="checkbox-circle" className="h-4 w-4 text-foreground" />
</div>
</div>
)}
@@ -1756,7 +1731,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<span className="typography-meta font-medium text-foreground truncate">
{getModelDisplayName(model)}
</span>
{isSelected ? <RiCheckLine className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" /> : null}
{isSelected ? <Icon name="check" className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" /> : null}
</div>
{contextText || indicatorIcons.length > 0 ? (
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden typography-micro text-muted-foreground">
@@ -1770,14 +1745,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
) : null}
{indicatorIcons.length > 0 ? (
<div className="flex min-w-0 items-center gap-1 overflow-hidden whitespace-nowrap pl-0.5">
{indicatorIcons.map(({ key, icon: IconComponent, label }) => (
{indicatorIcons.map(({ key, icon: iconName, label }) => (
<span
key={`meta-${providerId}-${modelId}-${key}`}
className="flex h-4 w-4 flex-shrink-0 items-center justify-center text-muted-foreground"
title={label}
aria-label={label}
>
<IconComponent className="h-3 w-3" />
<Icon name={iconName} className="h-3 w-3" />
</span>
))}
</div>
@@ -1795,7 +1770,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')}
>
<span className="whitespace-nowrap">{variantLabel}</span>
{isExpanded ? <RiArrowDownSLine className="h-3.5 w-3.5" /> : <RiArrowRightSLine className="h-3.5 w-3.5" />}
{isExpanded ? <Icon name="arrow-down-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-right-s" className="h-3.5 w-3.5" />}
</button>
) : null}
<div className="flex flex-shrink-0 items-start gap-1.5">
@@ -1818,9 +1793,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
: t('chat.modelControls.addToFavorites')}
>
{isFavoriteModel(providerId, modelId) ? (
<RiStarFill className="h-4 w-4" />
<Icon name="star-fill" className="h-4 w-4" />
) : (
<RiStarLine className="h-4 w-4" />
<Icon name="star" className="h-4 w-4" />
)}
</button>
</div>
@@ -1875,7 +1850,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex flex-col gap-2">
<div>
<div className="relative">
<RiSearchLine className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Icon name="search" className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
value={mobileModelQuery}
onChange={(event) => setMobileModelQuery(event.target.value)}
@@ -1889,7 +1864,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={t('chat.modelControls.clearSearch')}
>
<RiCloseCircleLine className="h-4 w-4" />
<Icon name="close-circle" className="h-4 w-4" />
</button>
)}
</div>
@@ -1905,7 +1880,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{filteredFavorites.length > 0 && (
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
<RiStarFill className="h-3 w-3 inline-block mr-1.5 text-primary" />
<Icon name="star-fill" className="h-3 w-3 inline-block mr-1.5 text-primary" />
{t('chat.modelControls.favorites')}
</div>
<div className="flex flex-col border-t border-border/30">
@@ -1923,7 +1898,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{filteredRecents.length > 0 && (
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
<RiTimeLine className="h-3 w-3 inline-block mr-1.5" />
<Icon name="time" className="h-3 w-3 inline-block mr-1.5" />
{t('chat.modelControls.recent')}
</div>
<div className="flex flex-col border-t border-border/30">
@@ -1971,9 +1946,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
)}
</div>
{isExpanded ? (
<RiArrowDownSLine className="h-3 w-3 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-3 w-3 text-muted-foreground" />
) : (
<RiArrowRightSLine className="h-3 w-3 text-muted-foreground" />
<Icon name="arrow-right-s" className="h-3 w-3 text-muted-foreground" />
)}
</button>
@@ -2037,7 +2012,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onClick={handleBack}
className="flex items-center gap-1 rounded-lg px-1.5 py-1 typography-meta text-muted-foreground hover:bg-interactive-hover"
>
<RiArrowGoBackLine className="h-4 w-4" />
<Icon name="arrow-go-back" className="h-4 w-4" />
<span>{t('onboarding.common.actions.back')}</span>
</button>
<h2 className="typography-ui-label font-semibold text-foreground">{t('chat.modelControls.thinking')}</h2>
@@ -2056,7 +2031,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onClick={() => handleSelect(undefined)}
>
<span className="typography-meta font-medium text-foreground">{t('chat.modelControls.default')}</span>
{isDefault && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
{isDefault && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />}
</button>
{targetVariants.map((variant) => {
@@ -2075,7 +2050,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onClick={() => handleSelect(variant)}
>
<span className="typography-meta font-medium text-foreground">{label}</span>
{selected && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
{selected && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />}
</button>
);
})}
@@ -2122,7 +2097,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{capitalizeAgentName(agent.name)}
</span>
{isSelected && (
<RiCheckLine className="h-4 w-4 text-primary ml-auto flex-shrink-0" />
<Icon name="check" className="h-4 w-4 text-primary ml-auto flex-shrink-0" />
)}
</div>
{agent.description && (
@@ -2288,7 +2263,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
if (hasCapabilities) {
slides.push(
<div key="capabilities" className="flex items-center gap-0.5">
{indicatorIcons.map(({ id, icon: Icon, label }) => (
{indicatorIcons.map(({ id, icon: iconName, label }) => (
<span
key={id}
className="flex h-3.5 w-3.5 items-center justify-center text-muted-foreground"
@@ -2296,7 +2271,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
role="img"
title={label}
>
<Icon className="h-2.5 w-2.5" />
<Icon name={iconName} className="h-2.5 w-2.5" />
</span>
))}
</div>
@@ -2355,7 +2330,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
aria-label={t('chat.modelControls.reorderFavoriteAria')}
title={t('chat.modelControls.reorderFavoriteTitle')}
>
<RiDraggable className="h-3.5 w-3.5" />
<Icon name="draggable" className="h-3.5 w-3.5" />
</button>
) : null}
<div className="flex items-center gap-1.5 flex-1 min-w-0">
@@ -2395,7 +2370,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
</div>
) : null}
{isSelected && (
<RiCheckLine className="h-4 w-4 text-primary" />
<Icon name="check" className="h-4 w-4 text-primary" />
)}
<button
onClick={(e) => {
@@ -2415,9 +2390,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
: t('chat.modelControls.addToFavorites')}
>
{isFavorite ? (
<RiStarFill className="h-3.5 w-3.5" />
<Icon name="star-fill" className="h-3.5 w-3.5" />
) : (
<RiStarLine className="h-3.5 w-3.5" />
<Icon name="star" className="h-3.5 w-3.5" />
)}
</button>
</div>
@@ -2669,7 +2644,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
>
{!isReady ? (
<>
<RiLoader4Line className={cn(controlIconSize, 'animate-spin text-muted-foreground flex-shrink-0')} />
<Icon name="loader-4" className={cn(controlIconSize, 'animate-spin text-muted-foreground flex-shrink-0')} />
<span className={cn(
'model-controls__model-label',
controlTextSize,
@@ -2684,10 +2659,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
providerId={currentProviderId}
className={cn(controlIconSize, 'flex-shrink-0')}
/>
<RiPencilAiLine className={cn(controlIconSize, 'text-primary/60 hidden')} />
<Icon name="pencil-ai" className={cn(controlIconSize, 'text-primary/60 hidden')} />
</>
) : (
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
<Icon name="pencil-ai" className={cn(controlIconSize, 'text-muted-foreground')} />
)}
{isReady && (
<span
@@ -2712,7 +2687,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{/* Search Input */}
<div className="p-2 border-b border-border/40">
<div className="relative">
<RiSearchLine className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
type="text"
placeholder={t('chat.modelControls.searchModels')}
@@ -2744,7 +2719,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
className="typography-meta group flex items-center gap-1 rounded-md px-2 py-1.5 cursor-pointer hover:bg-interactive-hover/50"
>
<span className="flex h-4 w-4 items-center justify-center text-muted-foreground">
<RiAddLine className="h-4 w-4 -mr-0.5" />
<Icon name="add" className="h-4 w-4 -mr-0.5" />
</span>
<span className="font-medium text-foreground">{t('chat.modelControls.addNewProvider')}</span>
</div>
@@ -2763,7 +2738,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuLabel
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
>
<RiStarFill className="h-4 w-4 text-primary" />
<Icon name="star-fill" className="h-4 w-4 text-primary" />
{t('chat.modelControls.favorites')}
</DropdownMenuLabel>
{favoriteSortingEnabled ? (
@@ -2813,7 +2788,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuLabel
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
>
<RiTimeLine className="h-4 w-4" />
<Icon name="time" className="h-4 w-4" />
{t('chat.modelControls.recent')}
</DropdownMenuLabel>
{filteredRecents.map(({ model, providerID, modelID }) => {
@@ -2879,9 +2854,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<span className="min-w-0 truncate">{provider.name}</span>
<span className="flex h-4 w-4 flex-shrink-0 items-center justify-center text-muted-foreground">
{isExpanded ? (
<RiArrowDownSLine className="h-4 w-4" />
<Icon name="arrow-down-s" className="h-4 w-4" />
) : (
<RiArrowRightSLine className="h-4 w-4" />
<Icon name="arrow-right-s" className="h-4 w-4" />
)}
</span>
</div>
@@ -2923,7 +2898,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
>
{!isReady ? (
<>
<RiLoader4Line className={cn(controlIconSize, 'animate-spin text-muted-foreground flex-shrink-0')} />
<Icon name="loader-4" className={cn(controlIconSize, 'animate-spin text-muted-foreground flex-shrink-0')} />
<span className="typography-micro font-medium text-muted-foreground min-w-0">
{readinessLabel}
</span>
@@ -2936,7 +2911,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
className={cn(controlIconSize, 'flex-shrink-0')}
/>
) : (
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
<Icon name="pencil-ai" className={cn(controlIconSize, 'text-muted-foreground')} />
)}
<span
ref={modelLabelRef}
@@ -3043,7 +3018,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
</div>
)}
<div className="flex flex-col gap-1">
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.permissions')}</span>
<div className="flex items-center gap-3">
@@ -3078,7 +3052,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{hasCustomPrompt && (
<div className="flex items-center justify-between gap-3">
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
<RiCheckboxCircleLine className="h-4 w-4 text-foreground" />
<Icon name="checkbox-circle" className="h-4 w-4 text-foreground" />
</div>
)}
</div>
@@ -3106,7 +3080,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
'cursor-pointer hover:bg-transparent hover:opacity-70',
)}
>
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
<Icon name="brain-ai-3" className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
<span className={cn(
'model-controls__variant-label',
controlTextSize,
@@ -3131,7 +3105,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
buttonHeight,
)}
>
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
<Icon name="brain-ai-3" className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
<span
className={cn(
'model-controls__variant-label',
@@ -3151,7 +3125,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuItem className="typography-meta" onSelect={() => handleVariantSelect(undefined)}>
<div className="flex items-center justify-between gap-2 w-full min-w-0">
<span className="typography-meta font-medium text-foreground truncate min-w-0">{t('chat.modelControls.default')}</span>
{isDefault && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
{isDefault && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />}
</div>
</DropdownMenuItem>
{availableVariants.length > 0 && <DropdownMenuSeparator />}
@@ -3166,7 +3140,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
>
<div className="flex items-center justify-between gap-2 w-full min-w-0">
<span className="typography-meta font-medium text-foreground truncate min-w-0">{label}</span>
{selected && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
{selected && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />}
</div>
</DropdownMenuItem>
);
@@ -3194,7 +3168,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
)}>
{!isReady ? (
<>
<RiLoader4Line
<Icon name="loader-4"
className={cn(
controlIconSize,
'flex-shrink-0 animate-spin text-muted-foreground'
@@ -3212,7 +3186,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
</>
) : (
<>
<RiAiAgentLine
<Icon name="ai-agent"
className={cn(
controlIconSize,
'flex-shrink-0',
@@ -3239,7 +3213,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col">
<div className="p-2 border-b border-border/40">
<div className="relative">
<RiSearchLine className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
type="text"
placeholder={t('chat.modelControls.searchAgents')}
@@ -3262,7 +3236,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onSelect={() => handleAgentChange(defaultAgentName)}
>
<div className="flex items-center gap-1.5">
<RiArrowGoBackLine className="h-3.5 w-3.5 text-muted-foreground" />
<Icon name="arrow-go-back" className="h-3.5 w-3.5 text-muted-foreground" />
<span className="font-medium">{t('chat.modelControls.resetToDefault')}</span>
</div>
</DropdownMenuItem>
@@ -3323,7 +3297,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
>
{!isReady ? (
<>
<RiLoader4Line
<Icon name="loader-4"
className={cn(
controlIconSize,
'flex-shrink-0 animate-spin text-muted-foreground'
@@ -3341,7 +3315,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
</>
) : (
<>
<RiAiAgentLine
<Icon name="ai-agent"
className={cn(
controlIconSize,
'flex-shrink-0',
@@ -1,12 +1,12 @@
import React from 'react';
import { Popover } from '@base-ui/react/popover';
import { RiFileEditLine, RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useGitStore, useIsGitRepo } from '@/stores/useGitStore';
import { useUIStore } from '@/stores/useUIStore';
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
import { sessionEvents } from '@/lib/sessionEvents';
import { normalizePath } from '@/components/session/sidebar/utils';
import { Icon } from "@/components/icon/Icon";
import {
type ChangedFileEntry,
type GitChangedFile,
@@ -103,7 +103,7 @@ export const PendingChangesBar: React.FC = React.memo(() => {
type="button"
className="flex min-w-0 max-w-full items-center gap-1 text-left text-muted-foreground"
>
<RiFileEditLine className="h-3.5 w-3.5 flex-shrink-0 text-[var(--status-warning)]" />
<Icon name="file-edit" className="h-3.5 w-3.5 flex-shrink-0 text-[var(--status-warning)]" />
<span className="min-w-0 typography-ui-label text-foreground flex-shrink-0">{labelHead}</span>
<span className="status-row__changed-label min-w-0 typography-ui-label text-foreground truncate">
{t('chat.pendingChanges.changedInWorkspace')}
@@ -113,9 +113,9 @@ export const PendingChangesBar: React.FC = React.memo(() => {
{totalRemoved > 0 ? <span style={{ color: 'var(--status-error)' }}>-{totalRemoved}</span> : null}
</span>
{isExpanded ? (
<RiArrowUpSLine className="h-3.5 w-3.5 flex-shrink-0" />
<Icon name="arrow-up-s" className="h-3.5 w-3.5 flex-shrink-0" />
) : (
<RiArrowDownSLine className="h-3.5 w-3.5 flex-shrink-0" />
<Icon name="arrow-down-s" className="h-3.5 w-3.5 flex-shrink-0" />
)}
</button>
}
@@ -1,5 +1,4 @@
import React from 'react';
import { RiCheckLine, RiCloseLine, RiFileEditLine, RiGlobalLine, RiPencilAiLine, RiQuestionLine, RiTerminalBoxLine, RiTimeLine, RiToolsLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import type { PermissionRequest, PermissionResponse } from '@/types/permission';
import { useSessionUIStore } from '@/sync/session-ui-store';
@@ -9,6 +8,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Icon } from "@/components/icon/Icon";
import { DiffPreview, WritePreview } from './DiffPreview';
import { useI18n } from '@/lib/i18n';
@@ -52,22 +52,22 @@ const getToolIcon = (toolName: string) => {
const tool = toolName.toLowerCase();
if (tool === 'edit' || tool === 'multiedit' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
return <RiPencilAiLine className={iconClass} />;
return <Icon name="pencil-ai" className={iconClass} />;
}
if (tool === 'write' || tool === 'create' || tool === 'file_write') {
return <RiFileEditLine className={iconClass} />;
return <Icon name="file-edit" className={iconClass} />;
}
if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal' || tool === 'shell_command') {
return <RiTerminalBoxLine className={iconClass} />;
return <Icon name="terminal-box" className={iconClass} />;
}
if (tool === 'webfetch' || tool === 'fetch' || tool === 'curl' || tool === 'wget') {
return <RiGlobalLine className={iconClass} />;
return <Icon name="global" className={iconClass} />;
}
return <RiToolsLine className={iconClass} />;
return <Icon name="tools" className={iconClass} />;
};
const getToolDisplayName = (toolName: string): string => {
@@ -325,7 +325,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
<div className="px-2 py-1.5 border-b border-border/20 bg-muted/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<RiQuestionLine className="h-3.5 w-3.5 text-[var(--status-warning)]" />
<Icon name="question" className="h-3.5 w-3.5 text-[var(--status-warning)]" />
<span className="typography-meta font-medium text-muted-foreground">
Permission Required
</span>
@@ -376,7 +376,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
e.currentTarget.style.backgroundColor = 'rgb(var(--status-success) / 0.1)';
}}
>
<RiCheckLine className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
<Icon name="check" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Allow Once
</button>
@@ -399,7 +399,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.5)';
}}
>
<RiTimeLine className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
<Icon name="time" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
{(() => {
const always = (permission.always as string[]) || (permission.metadata.always as string[]) || [];
if (always.length === 0) return "Always Allow";
@@ -432,7 +432,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.5)';
}}
>
<RiTimeLine className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
<Icon name="time" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Always Allow
</button>
)}
@@ -455,7 +455,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
e.currentTarget.style.backgroundColor = 'rgb(var(--status-error) / 0.1)';
}}
>
<RiCloseLine className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
<Icon name="close" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Deny
</button>
@@ -1,9 +1,9 @@
import React from 'react';
import { RiCheckLine, RiCloseLine, RiTimeLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import type { PermissionRequest as PermissionRequestPayload, PermissionResponse } from '@/types/permission';
import * as sessionActions from '@/sync/session-actions';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
interface PermissionRequestProps {
permission: PermissionRequestPayload;
@@ -73,7 +73,7 @@ export const PermissionRequest: React.FC<PermissionRequestProps> = ({
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<RiCheckLine className="h-3 w-3" />
<Icon name="check" className="h-3 w-3" />
{t('chat.permissionRequest.actions.once')}
</button>
@@ -95,7 +95,7 @@ export const PermissionRequest: React.FC<PermissionRequestProps> = ({
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<RiTimeLine className="h-3 w-3" />
<Icon name="time" className="h-3 w-3" />
{t('chat.permissionRequest.actions.always')}
</button>
@@ -117,7 +117,7 @@ export const PermissionRequest: React.FC<PermissionRequestProps> = ({
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<RiCloseLine className="h-3 w-3" />
<Icon name="close" className="h-3 w-3" />
{t('chat.permissionRequest.actions.reject')}
</button>
@@ -1,7 +1,7 @@
import React from 'react';
import { RiArrowRightSLine, RiCheckLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react';
import { Checkbox } from '@/components/ui/checkbox';
import { Radio } from '@/components/ui/radio';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { isIMECompositionEvent } from '@/lib/ime';
@@ -217,7 +217,7 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
{/* Header */}
<div className="px-2 py-1.5 border-b border-border/20">
<div className="flex items-center gap-2">
<RiQuestionLine className="h-3.5 w-3.5 text-primary" />
<Icon name="question" className="h-3.5 w-3.5 text-primary" />
<span className="typography-meta font-medium text-muted-foreground">{t('chat.questionCard.inputNeeded')}</span>
{isFromSubagent ? (
<span className="typography-micro text-muted-foreground px-1.5 py-0.5 rounded bg-foreground/5">
@@ -257,7 +257,7 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
: 'text-foreground/85 hover:text-foreground hover:bg-interactive-hover/20'
)}
>
{isSummary ? <RiListCheck3 className="h-3 w-3" /> : null}
{isSummary ? <Icon name="list-check-3" className="h-3 w-3" /> : null}
{tab.label}
</button>
);
@@ -366,7 +366,7 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
)}
>
<div className="flex items-center gap-2">
<RiEditLine className={cn(
<Icon name="edit" className={cn(
'h-3.5 w-3.5',
isCustomActive ? 'text-primary' : 'text-muted-foreground/50'
)} />
@@ -427,7 +427,7 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
{requiredSatisfied ? <RiCheckLine className="h-3 w-3" /> : <RiArrowRightSLine className="h-3 w-3" />}
{requiredSatisfied ? <Icon name="check" className="h-3 w-3" /> : <Icon name="arrow-right-s" className="h-3 w-3" />}
{requiredSatisfied ? t('chat.questionCard.submit') : t('chat.questionCard.next')}
</button>
@@ -441,7 +441,7 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<RiCloseLine className="h-3 w-3" />
<Icon name="close" className="h-3 w-3" />
{t('chat.questionCard.dismiss')}
</button>
@@ -1,9 +1,9 @@
import React, { memo } from 'react';
import { RiCloseLine, RiMessage2Line } from '@remixicon/react';
import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useInputStore } from '@/sync/input-store';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
interface QueuedMessageChipProps {
message: QueuedMessage;
@@ -35,9 +35,8 @@ const QueuedMessageChip = memo(({ message, sessionId, onEdit }: QueuedMessageChi
onClick={() => onEdit(message)}
className="flex min-w-0 flex-1 items-center gap-1.5 text-left hover:opacity-80 transition-opacity"
>
<RiMessage2Line
className="h-4 w-4 flex-shrink-0 text-muted-foreground"
/>
<Icon name="message-2" className="h-4 w-4 flex-shrink-0 text-muted-foreground"
/>
<span className="text-muted-foreground flex-shrink-0">
Queued
{attachmentCount > 0 && (
@@ -54,7 +53,7 @@ const QueuedMessageChip = memo(({ message, sessionId, onEdit }: QueuedMessageChi
className="flex items-center justify-center h-6 w-6 flex-shrink-0 hover:bg-[var(--interactive-hover)] rounded-full transition-colors"
aria-label={t('chat.queuedMessage.removeAria')}
>
<RiCloseLine className="h-4 w-4 text-muted-foreground" />
<Icon name="close" className="h-4 w-4 text-muted-foreground" />
</button>
</div>
);
+13 -21
View File
@@ -1,14 +1,5 @@
import React from "react";
import { useSessionUIStore } from '@/sync/session-ui-store';
import {
RiArrowDownSLine,
RiArrowUpDoubleLine,
RiArrowUpSLine,
RiCheckboxCircleLine,
RiCloseCircleLine,
RiRecordCircleLine,
RiTimeLine,
} from "@remixicon/react";
import { cn } from "@/lib/utils";
import { useDirectorySync } from "@/sync/sync-context";
import type { Todo } from "@opencode-ai/sdk/v2/client";
@@ -22,6 +13,7 @@ import { useTodosPersistStore } from "@/stores/useTodosPersistStore";
import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder";
import { isVSCodeRuntime } from "@/lib/desktop";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Icon } from "@/components/icon/Icon";
import { useI18n } from "@/lib/i18n";
const STATUS_ROW_CONTAINER_STYLE = { containerType: "inline-size" as const, containerName: "status-row" };
@@ -48,9 +40,9 @@ const priorityClassName: Record<TodoPriority, string> = {
};
const priorityIcon: Record<TodoPriority, React.ReactNode> = {
high: <RiArrowUpDoubleLine className="h-3.5 w-3.5" aria-hidden="true" />,
medium: <RiArrowUpSLine className="h-3.5 w-3.5" aria-hidden="true" />,
low: <RiArrowDownSLine className="h-3.5 w-3.5" aria-hidden="true" />,
high: <Icon name="arrow-up-double" className="h-3.5 w-3.5" aria-hidden="true"/>,
medium: <Icon name="arrow-up-s" className="h-3.5 w-3.5" aria-hidden="true"/>,
low: <Icon name="arrow-down-s" className="h-3.5 w-3.5" aria-hidden="true"/>,
};
const statusLabelKey: Record<TodoStatus, string> = {
@@ -78,11 +70,11 @@ const TodoItemRow: React.FC<TodoItemRowProps> = ({ todo }) => {
const statusIcon =
todo.status === "in_progress" ? (
<RiRecordCircleLine className="h-3.5 w-3.5 text-[var(--status-info)]" aria-hidden="true" />
<Icon name="record-circle" className="h-3.5 w-3.5 text-[var(--status-info)]" aria-hidden="true"/>
) : todo.status === "completed" ? (
<RiCheckboxCircleLine className="h-3.5 w-3.5 text-[var(--status-success)]" aria-hidden="true" />
<Icon name="checkbox-circle" className="h-3.5 w-3.5 text-[var(--status-success)]" aria-hidden="true"/>
) : (
<RiTimeLine className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
<Icon name="time" className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true"/>
);
return (
@@ -248,7 +240,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
className="flex items-center justify-center h-[1.2rem] w-[1.2rem] text-[var(--status-error)] transition-opacity hover:opacity-80 focus-visible:outline-none flex-shrink-0"
aria-label={t('chat.statusRow.actions.stopGeneratingAria')}
>
<RiCloseCircleLine size={18} aria-hidden="true" />
<Icon name="close-circle" aria-hidden="true"/>
</button>
) : null;
@@ -271,19 +263,19 @@ export const StatusRow: React.FC<StatusRowProps> = ({
)}
<span className="typography-meta flex items-center gap-1 tabular-nums" aria-hidden="true">
<span className="flex items-center gap-0.5">
<RiRecordCircleLine className="h-3.5 w-3.5 text-[var(--status-info)]" />
<Icon name="record-circle" className="h-3.5 w-3.5 text-[var(--status-info)]" />
{statusSummary.active}
</span>
<span>·</span>
<span className="flex items-center gap-0.5">
<RiTimeLine className="h-3.5 w-3.5" />
<Icon name="time" className="h-3.5 w-3.5" />
{statusSummary.left}
</span>
</span>
{isExpanded ? (
<RiArrowUpSLine className="h-3.5 w-3.5" />
<Icon name="arrow-up-s" className="h-3.5 w-3.5" />
) : (
<RiArrowDownSLine className="h-3.5 w-3.5" />
<Icon name="arrow-down-s" className="h-3.5 w-3.5" />
)}
</button>
) : null;
@@ -301,7 +293,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
{showAssistantStatus && showAbortStatus ? (
<div className="flex h-full items-center text-[var(--status-error)] pl-0.5">
<span className="flex items-center gap-1.5 typography-ui-label">
<RiCloseCircleLine size={16} aria-hidden="true" />
<Icon name="close-circle" aria-hidden="true"/>
{t('chat.statusRow.aborted')}
</span>
</div>
@@ -9,8 +9,8 @@ import {
import { Input } from '@/components/ui/input';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionMessageRecords } from '@/sync/sync-context';
import { RiLoader4Line, RiSearchLine, RiTimeLine, RiGitBranchLine, RiArrowGoBackLine } from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import type { Part } from '@opencode-ai/sdk/v2';
import { useI18n } from '@/lib/i18n';
import { useDeviceInfo } from '@/lib/device';
@@ -151,7 +151,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RiTimeLine className="h-5 w-5" />
<Icon name="time" className="h-5 w-5" />
{t('chat.timeline.title')}
</DialogTitle>
<DialogDescription>
@@ -160,7 +160,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
</DialogHeader>
<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
autoFocus
placeholder={t('chat.timeline.searchPlaceholder')}
@@ -235,7 +235,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
onOpenChange(false);
}}
>
<RiArrowGoBackLine className="h-4 w-4" />
<Icon name="arrow-go-back" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{t('chat.timeline.actions.revertFromHere')}</TooltipContent>
@@ -253,9 +253,9 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
disabled={forkingMessageId === message.info.id}
>
{forkingMessageId === message.info.id ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<RiGitBranchLine className="h-4 w-4" />
<Icon name="git-branch" className="h-4 w-4" />
)}
</button>
</TooltipTrigger>
@@ -299,11 +299,11 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
<span>{t('chat.timeline.help.clickMessage')}</span>
</div>
<div className="flex items-center gap-2">
<RiArrowGoBackLine className="h-4 w-4 flex-shrink-0" />
<Icon name="arrow-go-back" className="h-4 w-4 flex-shrink-0" />
<span>{t('chat.timeline.help.undoToPoint')}</span>
</div>
<div className="flex items-center gap-2">
<RiGitBranchLine className="h-4 w-4 flex-shrink-0" />
<Icon name="git-branch" className="h-4 w-4 flex-shrink-0" />
<span>{t('chat.timeline.help.createSessionFromHere')}</span>
</div>
</div>
@@ -1,5 +1,4 @@
import React from 'react';
import { RiFileEditLine, RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react';
import type { ToolPart } from '@opencode-ai/sdk/v2';
import { Popover } from '@base-ui/react/popover';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
@@ -17,6 +16,7 @@ import {
import { ChangedFilesList } from './ChangedFilesList';
import { changedFilesPopoverClassName, changedFilesPopoverStyle } from './changedFilesPopover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import type { TurnActivityRecord } from './lib/turns/types';
interface TurnChangedFilesDropdownProps {
@@ -96,12 +96,12 @@ export const TurnChangedFilesDropdown: React.FC<TurnChangedFilesDropdownProps> =
onPointerDownCapture={syncPortalContainer}
onFocusCapture={syncPortalContainer}
>
<RiFileEditLine className="h-3.5 w-3.5" />
<Icon name="file-edit" className="h-3.5 w-3.5" />
<span className="message-footer__label">{label}</span>
{isExpanded ? (
<RiArrowUpSLine className="h-3.5 w-3.5" />
<Icon name="arrow-up-s" className="h-3.5 w-3.5" />
) : (
<RiArrowDownSLine className="h-3.5 w-3.5" />
<Icon name="arrow-down-s" className="h-3.5 w-3.5" />
)}
</button>
}
@@ -1,7 +1,7 @@
import React from 'react';
import { RiArrowDownLine } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
@@ -26,7 +26,7 @@ const ScrollToBottomButton: React.FC<ScrollToBottomButtonProps> = ({ visible, on
className="size-8 rounded-full [corner-shape:round] p-0 shadow-none bg-background/95 hover:bg-interactive-hover"
aria-label={t('chat.scrollToBottom.aria')}
>
<RiArrowDownLine className="h-4 w-4" />
<Icon name="arrow-down" className="h-4 w-4" />
</Button>
</div>
);
@@ -1,7 +1,7 @@
import React from 'react';
import { RiAlignJustify, RiLayoutColumnLine } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
export type DiffViewMode = 'side-by-side' | 'unified';
@@ -30,9 +30,9 @@ export const DiffViewToggle: React.FC<DiffViewToggleProps> = ({ mode, onModeChan
title={mode === 'side-by-side' ? 'Switch to unified view' : 'Switch to side-by-side view'}
>
{mode === 'side-by-side' ? (
<RiAlignJustify className="h-3 w-3" />
<Icon name="align-justify" className="h-3 w-3" />
) : (
<RiLayoutColumnLine className="h-3 w-3" />
<Icon name="layout-column" className="h-3 w-3" />
)}
</Button>
);
@@ -16,7 +16,6 @@ import { FadeInOnReveal } from './FadeInOnReveal';
import { Button } from '@/components/ui/button';
import { SaveProjectPlanDialog } from '@/components/session/SaveProjectPlanDialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { RiCheckLine, RiFileCopyLine, RiChatNewLine, RiArrowGoBackLine, RiGitBranchLine, RiHourglassLine, RiTimeLine, RiVolumeUpLine, RiStopLine, RiImageDownloadLine, RiLoader4Line, RiErrorWarningLine, RiBookletLine, RiGlobalLine, RiInformationLine } from '@remixicon/react';
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
@@ -34,6 +33,7 @@ import { useChatSurfaceMode } from '@/components/chat/useChatSurfaceMode';
import { isVSCodeRuntime } from '@/lib/desktop';
import { toPng } from 'html-to-image';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { formatTimestampForDisplay } from './timeFormat';
import { ToolRevealOnMount } from './parts/ToolRevealOnMount';
import { StaticToolRow } from './parts/ProgressiveGroup';
@@ -89,7 +89,6 @@ const normalizeSubtaskModel = (model: SubtaskPartLike['model']): string | null =
return `${providerID}/${modelID}`;
};
const UserSubtaskPart: React.FC<{ part: SubtaskPartLike }> = ({ part }) => {
const [expanded, setExpanded] = React.useState(false);
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
@@ -244,7 +243,7 @@ const UserShellActionPart: React.FC<{ part: ShellActionPartLike }> = ({ part })
aria-label={copiedOutput ? t('chat.messageBody.shellCommand.copied') : t('chat.messageBody.shellCommand.copyOutput')}
title={copiedOutput ? t('chat.messageBody.shellCommand.copied') : t('chat.messageBody.shellCommand.copyOutput')}
>
{copiedOutput ? <RiCheckLine className="h-3.5 w-3.5" /> : <RiFileCopyLine className="h-3.5 w-3.5" />}
{copiedOutput ? <Icon name="check" className="h-3.5 w-3.5" /> : <Icon name="file-copy" className="h-3.5 w-3.5" />}
</button>
</div>
{expanded ? (
@@ -268,8 +267,6 @@ const formatTurnDuration = (durationMs: number): string => {
return `${minutes}m ${seconds}s`;
};
interface MessageBodyProps {
sessionId?: string;
messageId: string;
@@ -463,7 +460,7 @@ const UserMessageBody = React.memo(({ messageId, parts, isMobile, alwaysShowActi
onRevert();
}}
>
<RiArrowGoBackLine className="h-3 w-3" />
<Icon name="arrow-go-back" className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.revert')}</TooltipContent>
@@ -484,7 +481,7 @@ const UserMessageBody = React.memo(({ messageId, parts, isMobile, alwaysShowActi
effectiveOnFork();
}}
>
<RiGitBranchLine className="h-3 w-3" />
<Icon name="git-branch" className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.fork')}</TooltipContent>
@@ -510,9 +507,9 @@ const UserMessageBody = React.memo(({ messageId, parts, isMobile, alwaysShowActi
}}
>
{isMessageCopied ? (
<RiCheckLine className="h-3 w-3 text-[color:var(--status-success)]" />
<Icon name="check" className="h-3 w-3 text-[color:var(--status-success)]" />
) : (
<RiFileCopyLine className="h-3 w-3" />
<Icon name="file-copy" className="h-3 w-3" />
)}
</Button>
</TooltipTrigger>
@@ -770,9 +767,9 @@ const AssistantMessageActionButtons = React.memo(({
}}
>
{isMessageCopied ? (
<RiCheckLine className="h-3.5 w-3.5 text-[color:var(--status-success)]" />
<Icon name="check" className="h-3.5 w-3.5 text-[color:var(--status-success)]" />
) : (
<RiFileCopyLine className="h-3.5 w-3.5" />
<Icon name="file-copy" className="h-3.5 w-3.5" />
)}
</Button>
</TooltipTrigger>
@@ -796,9 +793,9 @@ const AssistantMessageActionButtons = React.memo(({
}}
>
{isSharing ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<RiImageDownloadLine className="h-4 w-4" />
<Icon name="image-download" className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
@@ -820,9 +817,9 @@ const AssistantMessageActionButtons = React.memo(({
onClick={handleTTSClick}
>
{isTTSPlaying ? (
<RiStopLine className="h-3.5 w-3.5" />
<Icon name="stop" className="h-3.5 w-3.5" />
) : (
<RiVolumeUpLine className="h-3.5 w-3.5" />
<Icon name="volume-up" className="h-3.5 w-3.5" />
)}
</Button>
</TooltipTrigger>
@@ -1033,7 +1030,6 @@ const AssistantMessageBody = React.memo(({
return resolved ? { id: resolved.id, path: resolved.path } : null;
}, [availableWorktreesByProject, currentSession?.directory, effectiveDirectory, projects]);
const hasTools = toolParts.length > 0;
const hasPendingTools = React.useMemo(() => {
@@ -1082,7 +1078,6 @@ const AssistantMessageBody = React.memo(({
return toolParts.every((toolPart) => isToolFinalized(toolPart));
}, [toolParts, hasPendingTools, isToolFinalized]);
const reasoningParts = React.useMemo(() => {
return visibleParts.filter((part) => part.type === 'reasoning');
}, [visibleParts]);
@@ -1105,7 +1100,6 @@ const AssistantMessageBody = React.memo(({
hasTools &&
(hasPendingTools || hasOpenStep || !allToolsFinalized);
const shouldHoldTools = awaitingMessageCompletion
|| (hasTools && (hasPendingTools || hasOpenStep || !allToolsFinalized));
const shouldHoldReasoning = awaitingMessageCompletion || shouldHoldForReasoning;
@@ -1398,7 +1392,7 @@ const AssistantMessageBody = React.memo(({
const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish;
const showErrorMessage = Boolean(errorMessage);
const ErrorIcon = errorVariant === 'info' ? RiInformationLine : RiErrorWarningLine;
const errorIconName = errorVariant === 'info' ? 'information' : 'error-warning';
const shouldShowMessageActions = hasCopyableText;
const shouldShowTurnFooter = isLastAssistantInTurn && hasTextContent && (hasStopFinish || Boolean(errorMessage));
const shouldRenderActionsInActivity = isSortedRenderMode;
@@ -1466,7 +1460,6 @@ const AssistantMessageBody = React.memo(({
const shouldRenderStandaloneActionsAfterContent = shouldShowStandaloneMessageActions && lastRenderableTextPartIndex < 0;
const renderedParts = React.useMemo(() => {
const rendered: React.ReactNode[] = [];
@@ -1729,7 +1722,7 @@ const AssistantMessageBody = React.memo(({
openContextPreview(directory, messagePreviewUrl);
}}
>
<RiGlobalLine className="h-4 w-4" />
<Icon name="global" className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.openPreview')}</TooltipContent>
@@ -1750,7 +1743,7 @@ const AssistantMessageBody = React.memo(({
onPointerDown={(event) => event.stopPropagation()}
onClick={handleSaveAsPlanClick}
>
<RiBookletLine className="h-4 w-4" />
<Icon name="booklet" className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.saveAsPlan')}</TooltipContent>
@@ -1766,7 +1759,7 @@ const AssistantMessageBody = React.memo(({
onPointerDown={(event) => event.stopPropagation()}
onClick={handleForkClick}
>
<RiChatNewLine className="h-4 w-4" />
<Icon name="chat-new" className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{t('chat.messageBody.actions.startNewSession')}</TooltipContent>
@@ -1824,7 +1817,7 @@ const AssistantMessageBody = React.memo(({
: 'bg-[var(--status-error-background)] border-[var(--status-error-border)]',
)}>
<div className="flex items-center gap-2">
<ErrorIcon className={cn(
<Icon name={errorIconName} className={cn(
'h-4 w-4 shrink-0',
errorVariant === 'info' ? 'text-[var(--status-info)]' : 'text-[var(--status-error)]',
)} />
@@ -1862,7 +1855,7 @@ const AssistantMessageBody = React.memo(({
<Tooltip>
<TooltipTrigger asChild>
<span className="text-sm text-muted-foreground/60 tabular-nums flex items-center gap-1">
<RiHourglassLine className="h-3.5 w-3.5" />
<Icon name="hourglass" className="h-3.5 w-3.5" />
<span className="message-footer__label">{turnDurationText}</span>
</span>
</TooltipTrigger>
@@ -1876,7 +1869,7 @@ const AssistantMessageBody = React.memo(({
className={footerTimestampClassName}
aria-label={`Message time: ${footerTimestamp}`}
>
<RiTimeLine className="h-3.5 w-3.5" />
<Icon name="time" className="h-3.5 w-3.5" />
<span className="message-footer__label">{footerTimestamp}</span>
</span>
</TooltipTrigger>
@@ -1,8 +1,8 @@
import React from 'react';
import { RiAiAgentLine, RiBrainAi3Line, RiUser3Line } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { getAgentColor } from '@/lib/agentColors';
import { useProviderLogo } from '@/hooks/useProviderLogo';
import { Icon } from "@/components/icon/Icon";
interface MessageHeaderProps {
isUser: boolean;
@@ -23,7 +23,7 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
<div className="flex-shrink-0">
{isUser ? (
<div className="w-9 h-9 rounded-xl bg-primary/10 flex items-center justify-center">
<RiUser3Line className="h-4 w-4 text-primary" />
<Icon name="user-3" className="h-4 w-4 text-primary" />
</div>
) : (
<div className="flex items-center justify-center">
@@ -38,10 +38,9 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
onError={handleLogoError}
/>
) : (
<RiBrainAi3Line
className="h-4 w-4"
style={{ color: `var(${getAgentColor(agentName).var})` }}
/>
<Icon name="brain-ai-3" className="h-4 w-4"
style={{ color: `var(${getAgentColor(agentName).var})` }}/>
)}
</div>
)}
@@ -64,7 +63,7 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
getAgentColor(agentName).class
)}
>
<RiAiAgentLine className="h-3 w-3 flex-shrink-0" />
<Icon name="ai-agent" className="h-3 w-3 flex-shrink-0" />
<span className="font-medium">{agentName}</span>
</div>
)}
@@ -85,7 +84,7 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
: undefined
}
>
<RiBrainAi3Line className="h-3 w-3 flex-shrink-0" />
<Icon name="brain-ai-3" className="h-3 w-3 flex-shrink-0" />
<span className="font-medium">{variant.length > 0 ? variant[0].toLowerCase() + variant.slice(1) : variant}</span>
</div>
)}
@@ -5,10 +5,10 @@ import { useSessions } from '@/sync/sync-context';
import { useInputStore } from '@/sync/input-store';
import { useUIStore } from '@/stores/useUIStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { RiBookletLine, RiChatNewLine, RiAddLine, RiFileCopyLine, RiLoader4Line } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { copyTextToClipboard } from '@/lib/clipboard';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { getProjectNotesAndTodos, saveProjectNotesAndTodos } from '@/lib/openchamberConfig';
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
@@ -32,7 +32,6 @@ interface SelectionPayload {
rect: DOMRect;
}
const appendDistilledInsightToNotes = (existingNotes: string, insight: string): string => {
const trimmedInsight = insight.trim().replace(/^[-*+]\s+/, '');
if (!trimmedInsight) {
@@ -592,7 +591,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
title={t('chat.textSelection.title.addToCurrentChat')}
type="button"
>
<RiAddLine className="h-5 w-5 flex-shrink-0" />
<Icon name="add" className="h-5 w-5 flex-shrink-0" />
<span className="min-w-0 whitespace-normal">{t('chat.textSelection.actions.addToChat')}</span>
</button>
@@ -608,7 +607,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
title={t('chat.textSelection.title.newSessionWithSelection')}
type="button"
>
<RiChatNewLine className="h-5 w-5 flex-shrink-0" />
<Icon name="chat-new" className="h-5 w-5 flex-shrink-0" />
<span className="min-w-0 whitespace-normal">{t('chat.textSelection.actions.newSession')}</span>
</button>
@@ -624,7 +623,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
title={t('chat.textSelection.actions.copy')}
type="button"
>
<RiFileCopyLine className="h-5 w-5 flex-shrink-0" />
<Icon name="file-copy" className="h-5 w-5 flex-shrink-0" />
<span className="min-w-0 whitespace-normal">{t('chat.textSelection.actions.copy')}</span>
</button>
@@ -642,7 +641,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
title={t('chat.textSelection.title.saveInsightToNotes')}
type="button"
>
{isAddingToNotes ? <RiLoader4Line className="h-5 w-5 flex-shrink-0 animate-spin" /> : <RiBookletLine className="h-5 w-5 flex-shrink-0" />}
{isAddingToNotes ? <Icon name="loader-4" className="h-5 w-5 flex-shrink-0 animate-spin" /> : <Icon name="booklet" className="h-5 w-5 flex-shrink-0" />}
<span className="min-w-0 whitespace-normal">{t('chat.textSelection.actions.addToNotes')}</span>
</button>
) : null}
@@ -685,7 +684,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
title={t('chat.textSelection.title.addToCurrentChat')}
type="button"
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
<span className="whitespace-nowrap">{t('chat.textSelection.actions.addToChat')}</span>
</button>
@@ -703,7 +702,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
title={t('chat.textSelection.title.newSessionWithSelection')}
type="button"
>
<RiChatNewLine className="h-4 w-4" />
<Icon name="chat-new" className="h-4 w-4" />
<span className="whitespace-nowrap">{t('chat.textSelection.actions.newSession')}</span>
</button>
@@ -724,7 +723,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
title={t('chat.textSelection.title.saveInsightToNotes')}
type="button"
>
{isAddingToNotes ? <RiLoader4Line className="h-4 w-4 animate-spin" /> : <RiBookletLine className="h-4 w-4" />}
{isAddingToNotes ? <Icon name="loader-4" className="h-4 w-4 animate-spin" /> : <Icon name="booklet" className="h-4 w-4" />}
<span className="whitespace-nowrap">{t('chat.textSelection.actions.addToNotes')}</span>
</button>
</>
@@ -1,6 +1,5 @@
import React from 'react';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { RiArrowLeftSLine, RiArrowRightSLine, RiBrainAi3Line, RiCloseLine, RiFileImageLine, RiFileList2Line, RiFilePdfLine, RiFileSearchLine, RiFolder6Line, RiGitBranchLine, RiGlobalLine, RiListCheck3, RiLoader4Line, RiPencilAiLine, RiSearchLine, RiTaskLine, RiTerminalBoxLine, RiToolsLine } from '@remixicon/react';
import { File as PierreFile, PatchDiff } from '@pierre/diffs/react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { createPortal } from 'react-dom';
@@ -26,6 +25,7 @@ import type { ToolPopupContent, DiffViewMode } from './types';
import { DiffViewToggle } from './DiffViewToggle';
import { VirtualizedCodeBlock, type CodeLine } from './parts/VirtualizedCodeBlock';
import { JsonTreeView } from '@/components/ui/JsonTreeView';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
interface ToolOutputDialogProps {
@@ -40,54 +40,54 @@ const getToolIcon = (toolName: string) => {
const tool = toolName.toLowerCase();
if (tool === 'reasoning') {
return <RiBrainAi3Line className={iconClass} />;
return <Icon name="brain-ai-3" className={iconClass} />;
}
if (tool === 'image-preview') {
return <RiFileImageLine className={iconClass} />;
return <Icon name="file-image" className={iconClass} />;
}
if (tool === 'mermaid-preview') {
return <RiFileList2Line className={iconClass} />;
return <Icon name="file-list-2" className={iconClass} />;
}
if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
return <RiPencilAiLine className={iconClass} />;
return <Icon name="pencil-ai" className={iconClass} />;
}
if (tool === 'write' || tool === 'create' || tool === 'file_write') {
return <RiFilePdfLine className={iconClass} />;
return <Icon name="file-pdf" className={iconClass} />;
}
if (tool === 'read' || tool === 'view' || tool === 'file_read' || tool === 'cat') {
return <RiFilePdfLine className={iconClass} />;
return <Icon name="file-pdf" className={iconClass} />;
}
if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal') {
return <RiTerminalBoxLine className={iconClass} />;
return <Icon name="terminal-box" className={iconClass} />;
}
if (tool === 'list' || tool === 'ls' || tool === 'dir' || tool === 'list_files') {
return <RiFolder6Line className={iconClass} />;
return <Icon name="folder-6" className={iconClass} />;
}
if (tool === 'search' || tool === 'grep' || tool === 'find' || tool === 'ripgrep') {
return <RiSearchLine className={iconClass} />;
return <Icon name="search" className={iconClass} />;
}
if (tool === 'glob') {
return <RiFileSearchLine className={iconClass} />;
return <Icon name="file-search" className={iconClass} />;
}
if (tool === 'fetch' || tool === 'curl' || tool === 'wget' || tool === 'webfetch') {
return <RiGlobalLine className={iconClass} />;
return <Icon name="global" className={iconClass} />;
}
if (tool === 'web-search' || tool === 'websearch' || tool === 'search_web' || tool === 'google' || tool === 'bing' || tool === 'duckduckgo') {
return <RiSearchLine className={iconClass} />;
return <Icon name="search" className={iconClass} />;
}
if (tool === 'todowrite' || tool === 'todoread') {
return <RiListCheck3 className={iconClass} />;
return <Icon name="list-check-3" className={iconClass} />;
}
if (tool === 'plan_enter') {
return <RiFileList2Line className={iconClass} />;
return <Icon name="file-list-2" className={iconClass} />;
}
if (tool === 'plan_exit') {
return <RiTaskLine className={iconClass} />;
return <Icon name="task" className={iconClass} />;
}
if (tool.startsWith('git')) {
return <RiGitBranchLine className={iconClass} />;
return <Icon name="git-branch" className={iconClass} />;
}
return <RiToolsLine className={iconClass} />;
return <Icon name="tools" className={iconClass} />;
};
const PREVIEW_ANIMATION_MS = 150;
@@ -442,7 +442,7 @@ const ImagePreviewDialog: React.FC<{
className="absolute left-3 top-1/2 -translate-y-1/2 z-10 h-10 w-10 flex items-center justify-center rounded-full bg-black/40 text-foreground/90 hover:bg-black/55 focus:outline-none focus:ring-2 focus:ring-primary/60"
aria-label={t('chat.toolOutputDialog.image.previousAria')}
>
<RiArrowLeftSLine className="h-6 w-6" />
<Icon name="arrow-left-s" className="h-6 w-6" />
</button>
<button
type="button"
@@ -451,7 +451,7 @@ const ImagePreviewDialog: React.FC<{
className="absolute right-3 top-1/2 -translate-y-1/2 z-10 h-10 w-10 flex items-center justify-center rounded-full bg-black/40 text-foreground/90 hover:bg-black/55 focus:outline-none focus:ring-2 focus:ring-primary/60"
aria-label={t('chat.toolOutputDialog.image.nextAria')}
>
<RiArrowRightSLine className="h-6 w-6" />
<Icon name="arrow-right-s" className="h-6 w-6" />
</button>
</>
)}
@@ -480,7 +480,7 @@ const ImagePreviewDialog: React.FC<{
onClick={() => onOpenChange(false)}
aria-label={t('chat.toolOutputDialog.image.closeAria')}
>
<RiCloseLine className="h-4 w-4" />
<Icon name="close" className="h-4 w-4" />
</button>
</div>
@@ -927,7 +927,7 @@ const MermaidPreviewDialog: React.FC<{
onClick={() => onOpenChange(false)}
aria-label={t('chat.toolOutputDialog.mermaid.closeAria')}
>
<RiCloseLine className="h-4 w-4" />
<Icon name="close" className="h-4 w-4" />
</button>
</div>
<div
@@ -937,7 +937,7 @@ const MermaidPreviewDialog: React.FC<{
<div className="h-full overflow-hidden">
{status === 'loading' && (
<div className="h-full min-h-28 flex items-center justify-center gap-2 text-muted-foreground typography-meta">
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
<span>{t('chat.toolOutputDialog.mermaid.loading')}</span>
</div>
)}
@@ -1016,7 +1016,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
<div className="flex-shrink-0 pb-1">
<div className="flex items-start gap-2 text-foreground typography-ui-header font-semibold">
{popup.metadata?.tool ? getToolIcon(popup.metadata.tool as string) : (
<RiToolsLine className="h-3.5 w-3.5 text-foreground flex-shrink-0" />
<Icon name="tools" className="h-3.5 w-3.5 text-foreground flex-shrink-0" />
)}
<span className="break-words flex-1 leading-tight">{popup.title}</span>
{popup.isDiff && (
@@ -1,5 +1,4 @@
import React from 'react';
import { RiStackLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import type { TurnActivityRecord as TurnActivityPart } from '../../lib/turns/types';
import type { ToolPart as ToolPartType } from '@opencode-ai/sdk/v2';
@@ -11,6 +10,7 @@ import { MinDurationShineText } from './MinDurationShineText';
import { ToolRevealOnMount } from './ToolRevealOnMount';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import { Text } from '@/components/ui/text';
import { Icon } from "@/components/icon/Icon";
import { FadeInOnReveal } from '../FadeInOnReveal';
import { getToolIcon } from './toolPresentation';
import { getToolMetadata } from '@/lib/toolHelpers';
@@ -927,7 +927,7 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
onClick={onToggle}
>
<span className="inline-flex h-5 items-center flex-shrink-0" style={{ color: 'var(--tools-icon)' }}>
<RiStackLine className="h-3.5 w-3.5" />
<Icon name="stack" className="h-3.5 w-3.5" />
</span>
<span
className="leading-5 font-semibold inline-flex h-5 items-center flex-shrink-0"
@@ -1,10 +1,10 @@
import React from 'react';
import type { ComponentType } from 'react';
import type { Part } from '@opencode-ai/sdk/v2';
import { RiArrowDownSLine, RiArrowRightSLine, RiBrainAi3Line, RiChatAi3Line } from '@remixicon/react';
import { cn } from '@/lib/utils';
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
import { useUIStore } from '@/stores/useUIStore';
import { useDurationTickerNow } from './useDurationTicker';
import { MarkdownRenderer } from '../../MarkdownRenderer';
@@ -14,15 +14,12 @@ type PartWithText = Part & { text?: string; content?: string; time?: { start?: n
export type ReasoningVariant = 'thinking' | 'justification';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type IconComponent = ComponentType<any>;
const variantConfig: Record<
ReasoningVariant,
{ label: string; Icon: IconComponent }
{ label: string; Icon: IconName }
> = {
thinking: { label: 'Thinking', Icon: RiBrainAi3Line },
justification: { label: 'Justification', Icon: RiChatAi3Line },
thinking: { label: 'Thinking', Icon: 'brain-ai-3' },
justification: { label: 'Justification', Icon: 'chat-ai-3' },
};
const cleanReasoningText = (text: string): string => {
@@ -99,7 +96,7 @@ export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
const [isExpanded, setIsExpanded] = React.useState(false);
const summary = React.useMemo(() => getReasoningSummary(text), [text]);
const { label, Icon } = variantConfig[variant];
const { label, Icon: iconName } = variantConfig[variant];
const timeStart = typeof time?.start === 'number' && Number.isFinite(time.start) ? time.start : undefined;
const timeEnd = typeof time?.end === 'number' && Number.isFinite(time.end) ? time.end : undefined;
@@ -131,7 +128,7 @@ export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
!isExpanded && (alwaysShowActions ? 'opacity-0' : 'group-hover/tool:opacity-0')
)}
>
<Icon className="h-3.5 w-3.5" />
<Icon name={iconName} className="h-3.5 w-3.5" />
</div>
<div
className={cn(
@@ -140,7 +137,7 @@ export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
!isExpanded && (alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/tool:opacity-100')
)}
>
{isExpanded ? <RiArrowDownSLine className="h-3.5 w-3.5" /> : <RiArrowRightSLine className="h-3.5 w-3.5" />}
{isExpanded ? <Icon name="arrow-down-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-right-s" className="h-3.5 w-3.5" />}
</div>
</div>
<span className="typography-meta font-medium">{label}</span>
@@ -1,7 +1,6 @@
import React from 'react';
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
import { RiArrowDownSLine, RiArrowRightSLine, RiExternalLinkLine } from '@remixicon/react';
import { PatchDiff } from '@pierre/diffs/react';
import { cn } from '@/lib/utils';
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
@@ -34,6 +33,7 @@ import {
tryParseJsonOutput,
} from '../toolRenderers';
import { JsonTreeViewer } from '@/components/ui/JsonTreeViewer';
import { Icon } from "@/components/icon/Icon";
import { DiffViewToggle, type DiffViewMode } from '../DiffViewToggle';
import { MinDurationShineText } from './MinDurationShineText';
import { ToolRevealOnMount } from './ToolRevealOnMount';
@@ -722,7 +722,7 @@ const ToolScrollableSection: React.FC<ToolScrollableSectionProps> = ({
disableHorizontal ? 'overflow-y-auto overflow-x-hidden' : 'overflow-auto',
className,
)}
size={24}
>
<div className="w-full min-w-0">
{children}
@@ -1190,7 +1190,7 @@ const TaskToolSummary: React.FC<{
onPointerDown={(event) => event.stopPropagation()}
onClick={handleOpenSession}
>
<RiExternalLinkLine className="h-3.5 w-3.5 flex-shrink-0" />
<Icon name="external-link" className="h-3.5 w-3.5 flex-shrink-0" />
<span className="typography-meta text-primary font-medium">{t('chat.toolPart.openSubtask', { type: agentType.charAt(0).toUpperCase() + agentType.slice(1) })}</span>
</button>
)}
@@ -1208,9 +1208,9 @@ const TaskToolSummary: React.FC<{
}}
>
{isOutputExpanded ? (
<RiArrowDownSLine className="h-3.5 w-3.5 flex-shrink-0" />
<Icon name="arrow-down-s" className="h-3.5 w-3.5 flex-shrink-0" />
) : (
<RiArrowRightSLine className="h-3.5 w-3.5 flex-shrink-0" />
<Icon name="arrow-right-s" className="h-3.5 w-3.5 flex-shrink-0" />
)}
<span className="typography-meta text-foreground/80 font-medium">{t('chat.toolPart.output')}</span>
</button>
@@ -1884,8 +1884,6 @@ const ToolPart: React.FC<ToolPartProps> = ({
sessionEvents.requestGitRefresh({ directory: currentDirectory });
}, [currentDirectory, isError, isFinalized, normalizedPartTool, part.id, status]);
const shouldNotifyStructuralChange = isFinalized || isTaskTool;
const onContentChangeRef = React.useRef(onContentChange);
@@ -2419,7 +2417,6 @@ const ToolPart: React.FC<ToolPartProps> = ({
taskSessionId,
]);
const taskSummaryLenRef = React.useRef<number>(taskSummaryEntries.length);
React.useEffect(() => {
if (!isTaskTool) {
@@ -2565,7 +2562,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
!isExpanded && (alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/tool:opacity-100')
)}
>
{isExpanded ? <RiArrowDownSLine className="h-3.5 w-3.5" /> : <RiArrowRightSLine className="h-3.5 w-3.5" />}
{isExpanded ? <Icon name="arrow-down-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-right-s" className="h-3.5 w-3.5" />}
</div>
</div>
{isMultiFileApplyPatch ? (
@@ -4,7 +4,7 @@ import type { Part } from '@opencode-ai/sdk/v2';
import type { AgentMentionInfo } from '../types';
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
import { useUIStore } from '@/stores/useUIStore';
import { RiArrowUpSLine } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
type PartWithText = Part & { text?: string; content?: string; value?: string };
@@ -149,7 +149,7 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
className="absolute top-0 right-0 z-10 flex items-center justify-center rounded-sm bg-[var(--surface-elevated)] p-0.5 text-[var(--surface-mutedForeground)] hover:text-[var(--surface-foreground)] hover:bg-[var(--interactive-hover)] transition-colors"
aria-label="Collapse"
>
<RiArrowUpSLine className="h-3.5 w-3.5" />
<Icon name="arrow-up-s" className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -1,51 +1,33 @@
import React from 'react';
import {
RiAiAgentLine,
RiBookLine,
RiFileEditLine,
RiFileList2Line,
RiFileSearchLine,
RiFileTextLine,
RiFolder6Line,
RiGitBranchLine,
RiGlobalLine,
RiListCheck2,
RiListCheck3,
RiMenuSearchLine,
RiPencilLine,
RiSurveyLine,
RiTaskLine,
RiTerminalBoxLine,
RiToolsLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
export const getToolIcon = (toolName: string) => {
const iconClass = 'h-3.5 w-3.5 flex-shrink-0';
const tool = toolName.toLowerCase();
if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
return <RiPencilLine className={iconClass} />;
return <Icon name="pencil" className={iconClass} />;
}
if (tool === 'write' || tool === 'create' || tool === 'file_write') {
return <RiFileEditLine className={iconClass} />;
return <Icon name="file-edit" className={iconClass} />;
}
if (tool === 'read' || tool === 'view' || tool === 'file_read' || tool === 'cat') {
return <RiFileTextLine className={iconClass} />;
return <Icon name="file-text" className={iconClass} />;
}
if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal') {
return <RiTerminalBoxLine className={iconClass} />;
return <Icon name="terminal-box" className={iconClass} />;
}
if (tool === 'list' || tool === 'ls' || tool === 'dir' || tool === 'list_files') {
return <RiFolder6Line className={iconClass} />;
return <Icon name="folder-6" className={iconClass} />;
}
if (tool === 'search' || tool === 'grep' || tool === 'find' || tool === 'ripgrep') {
return <RiMenuSearchLine className={iconClass} />;
return <Icon name="menu-search" className={iconClass} />;
}
if (tool === 'glob') {
return <RiFileSearchLine className={iconClass} />;
return <Icon name="file-search" className={iconClass} />;
}
if (tool === 'fetch' || tool === 'curl' || tool === 'wget' || tool === 'webfetch') {
return <RiGlobalLine className={iconClass} />;
return <Icon name="global" className={iconClass} />;
}
if (
tool === 'web-search' ||
@@ -57,31 +39,31 @@ export const getToolIcon = (toolName: string) => {
tool === 'duckduckgo' ||
tool === 'perplexity'
) {
return <RiGlobalLine className={iconClass} />;
return <Icon name="global" className={iconClass} />;
}
if (tool === 'todowrite' || tool === 'todoread') {
return <RiListCheck3 className={iconClass} />;
return <Icon name="list-check-3" className={iconClass} />;
}
if (tool === 'structuredoutput' || tool === 'structured_output') {
return <RiListCheck2 className={iconClass} />;
return <Icon name="list-check-2" className={iconClass} />;
}
if (tool === 'skill') {
return <RiBookLine className={iconClass} />;
return <Icon name="book" className={iconClass} />;
}
if (tool === 'task') {
return <RiAiAgentLine className={iconClass} />;
return <Icon name="ai-agent" className={iconClass} />;
}
if (tool === 'question') {
return <RiSurveyLine className={iconClass} />;
return <Icon name="survey" className={iconClass} />;
}
if (tool === 'plan_enter') {
return <RiFileList2Line className={iconClass} />;
return <Icon name="file-list-2" className={iconClass} />;
}
if (tool === 'plan_exit') {
return <RiTaskLine className={iconClass} />;
return <Icon name="task" className={iconClass} />;
}
if (tool.startsWith('git')) {
return <RiGitBranchLine className={iconClass} />;
return <Icon name="git-branch" className={iconClass} />;
}
return <RiToolsLine className={iconClass} />;
return <Icon name="tools" className={iconClass} />;
};
@@ -1,9 +1,9 @@
import { RiCheckLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { typography } from '@/lib/typography';
import { formatToolInput, detectToolOutputLanguage } from '@/lib/toolHelpers';
import { SimpleMarkdownRenderer } from '../MarkdownRenderer';
import { Icon } from "@/components/icon/Icon";
const cleanOutput = (output: string) => {
let cleaned = output.replace(/^<file>\s*\n?/, '').replace(/\n?<\/file>\s*$/, '');
@@ -473,13 +473,13 @@ export const renderTodoOutput = (
{todosByStatus.completed.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<RiCheckLine className="w-3 h-3" style={{ color: 'var(--status-success)' }} />
<Icon name="check" className="w-3 h-3" style={{ color: 'var(--status-success)' }}/>
<span className="typography-meta font-semibold uppercase tracking-wide" style={{ color: 'var(--status-success)' }}>{labels.completed}</span>
</div>
<div className="space-y-1.5 pl-4">
{todosByStatus.completed.map((todo, idx) => (
<div key={todo.id || idx} className="flex items-start gap-2">
<RiCheckLine className="w-3 h-3 mt-0.5 flex-shrink-0" style={{ color: 'var(--status-success)', opacity: 0.7 }} />
<Icon name="check" className="w-3 h-3 mt-0.5 flex-shrink-0" style={{ color: 'var(--status-success)', opacity: 0.7 }}/>
<span className="typography-code text-foreground flex-1 leading-relaxed">{todo.content}</span>
</div>
))}