From 14357257ae7127b7a82c1d63d685faebd15da4b1 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 13 May 2026 13:26:15 +0300 Subject: [PATCH] perf(ui): migrate icons to SVG sprite system Replace @remixicon/react with a shared Icon component that renders via 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 --- .opencode/skills/theme-system/SKILL.md | 112 +++++++- package.json | 3 +- packages/ui/package.json | 2 +- .../src/components/auth/SessionAuthGate.tsx | 12 +- .../ui/src/components/chat/ChatContainer.tsx | 5 +- .../src/components/chat/ChatErrorBoundary.tsx | 6 +- packages/ui/src/components/chat/ChatInput.tsx | 62 ++--- .../components/chat/CommandAutocomplete.tsx | 22 +- .../ui/src/components/chat/FileAttachment.tsx | 34 +-- .../chat/FileMentionAutocomplete.tsx | 16 +- .../components/chat/MarkdownRendererImpl.tsx | 22 +- .../chat/MobileSessionStatusBar.tsx | 64 ++--- .../ui/src/components/chat/ModelControls.tsx | 150 +++++------ .../src/components/chat/PendingChangesBar.tsx | 8 +- .../ui/src/components/chat/PermissionCard.tsx | 22 +- .../src/components/chat/PermissionRequest.tsx | 8 +- .../ui/src/components/chat/QuestionCard.tsx | 12 +- .../components/chat/QueuedMessageChips.tsx | 9 +- packages/ui/src/components/chat/StatusRow.tsx | 34 +-- .../ui/src/components/chat/TimelineDialog.tsx | 16 +- .../chat/TurnChangedFilesDropdown.tsx | 8 +- .../chat/components/ScrollToBottomButton.tsx | 4 +- .../chat/message/DiffViewToggle.tsx | 6 +- .../components/chat/message/MessageBody.tsx | 45 ++-- .../components/chat/message/MessageHeader.tsx | 15 +- .../chat/message/TextSelectionMenu.tsx | 17 +- .../chat/message/ToolOutputDialog.tsx | 48 ++-- .../chat/message/parts/ProgressiveGroup.tsx | 4 +- .../chat/message/parts/ReasoningPart.tsx | 19 +- .../chat/message/parts/ToolPart.tsx | 15 +- .../chat/message/parts/UserTextPart.tsx | 4 +- .../chat/message/parts/toolPresentation.tsx | 56 ++-- .../components/chat/message/toolRenderers.tsx | 6 +- .../components/comments/InlineCommentCard.tsx | 12 +- .../desktop/DesktopHostSwitcher.tsx | 63 ++--- .../components/desktop/OpenInAppButton.tsx | 10 +- packages/ui/src/components/icon/Icon.tsx | 57 ++++ packages/ui/src/components/icon/README.md | 78 ++++++ packages/ui/src/components/icon/icons.ts | 1 + packages/ui/src/components/icon/sprite.ts | 228 ++++++++++++++++ .../ui/src/components/layout/ContextPanel.tsx | 26 +- .../components/layout/ContextSidebarTab.tsx | 4 +- packages/ui/src/components/layout/Header.tsx | 94 ++++--- .../layout/ProjectActionsButton.tsx | 64 ++--- .../components/layout/ProjectEditDialog.tsx | 5 +- .../components/layout/RightSidebarTabs.tsx | 8 +- .../components/layout/SidebarFilesTree.tsx | 51 ++-- .../ui/src/components/layout/VSCodeLayout.tsx | 15 +- .../ui/src/components/mcp/McpDropdown.tsx | 6 +- .../components/mini-chat/MiniChatLayout.tsx | 8 +- .../components/multirun/ModelMultiSelect.tsx | 14 +- .../components/multirun/MultiRunLauncher.tsx | 30 +-- .../components/onboarding/ChooserScreen.tsx | 12 +- .../onboarding/DesktopConnectionRecovery.tsx | 12 +- .../onboarding/LocalSetupScreen.tsx | 8 +- .../sections/SectionPlaceholder.tsx | 7 +- .../components/sections/agents/AgentsPage.tsx | 26 +- .../sections/agents/AgentsSidebar.tsx | 23 +- .../sections/agents/ModelSelector.tsx | 44 +-- .../sections/behavior/BehaviorPage.tsx | 6 +- .../sections/commands/AgentSelector.tsx | 14 +- .../sections/commands/CommandsPage.tsx | 8 +- .../sections/commands/CommandsSidebar.tsx | 18 +- .../GitIdentityEditorDialog.tsx | 40 ++- .../sections/git-identities/GitPage.tsx | 42 ++- .../magic-prompts/MagicPromptsPage.tsx | 6 +- .../src/components/sections/mcp/McpPage.tsx | 49 ++-- .../components/sections/mcp/McpSidebar.tsx | 24 +- .../sections/openchamber/AboutSettings.tsx | 19 +- .../sections/openchamber/GitHubSettings.tsx | 8 +- .../openchamber/KeyboardShortcutsSettings.tsx | 4 +- .../openchamber/NotificationSettings.tsx | 10 +- .../openchamber/OpenChamberVisualSettings.tsx | 32 ++- .../openchamber/OpenCodeCliSettings.tsx | 6 +- .../openchamber/SessionRetentionSettings.tsx | 6 +- .../sections/openchamber/TunnelSettings.tsx | 60 ++--- .../sections/openchamber/VoiceSettings.tsx | 22 +- .../openchamber/WorktreeSectionContent.tsx | 14 +- .../projects/ProjectActionsSection.tsx | 29 +- .../sections/projects/ProjectsPage.tsx | 12 +- .../sections/projects/ProjectsSidebar.tsx | 12 +- .../sections/providers/ProvidersPage.tsx | 34 +-- .../sections/providers/ProvidersSidebar.tsx | 6 +- .../remote-instances/RemoteInstancesPage.tsx | 59 ++--- .../RemoteInstancesSidebar.tsx | 10 +- .../shared/SettingsProjectSelector.tsx | 6 +- .../sections/shared/SettingsSidebarHeader.tsx | 4 +- .../sections/shared/SettingsSidebarItem.tsx | 16 +- .../sections/shared/SidebarGroup.tsx | 4 +- .../components/sections/skills/SkillsPage.tsx | 20 +- .../sections/skills/SkillsSidebar.tsx | 16 +- .../skills/catalog/AddCatalogDialog.tsx | 5 +- .../skills/catalog/InstallFromRepoDialog.tsx | 12 +- .../skills/catalog/InstallSkillDialog.tsx | 10 +- .../skills/catalog/SkillsCatalogPage.tsx | 17 +- .../components/sections/usage/UsagePage.tsx | 13 +- .../sections/usage/UsageSidebar.tsx | 4 +- .../components/session/BranchPickerDialog.tsx | 39 ++- .../session/DirectoryAutocomplete.tsx | 6 +- .../session/DirectoryExplorerDialog.tsx | 27 +- .../src/components/session/DirectoryTree.tsx | 64 ++--- .../session/GitHubIntegrationDialog.tsx | 41 ++- .../session/GitHubIssuePickerDialog.tsx | 29 +- .../session/GitHubPrPickerDialog.tsx | 21 +- .../components/session/NewWorktreeDialog.tsx | 71 +++-- .../session/ProjectNotesTodoPanel.tsx | 12 +- .../session/ScheduledTaskEditorDialog.tsx | 15 +- .../session/ScheduledTasksDialog.tsx | 54 ++-- .../src/components/session/SessionDialogs.tsx | 16 +- .../components/session/SessionFolderItem.tsx | 33 +-- .../src/components/session/SessionSidebar.tsx | 4 +- .../src/components/session/TodoSendDialog.tsx | 4 +- .../session/sidebar/BulkActionBar.tsx | 17 +- .../session/sidebar/ConfirmDialogs.tsx | 6 +- .../session/sidebar/SessionGroupSection.tsx | 48 ++-- .../session/sidebar/SessionNodeItem.tsx | 78 ++---- .../sidebar/SidebarActivitySections.tsx | 4 +- .../session/sidebar/SidebarFooter.tsx | 8 +- .../session/sidebar/SidebarHeader.tsx | 42 ++- .../session/sidebar/sessionFolderDnd.tsx | 4 +- .../session/sidebar/sortableItems.tsx | 33 +-- packages/ui/src/components/ui/AboutDialog.tsx | 8 +- .../ui/src/components/ui/CommandPalette.tsx | 39 +-- .../src/components/ui/ContextUsageDisplay.tsx | 6 +- .../ui/src/components/ui/ErrorBoundary.tsx | 6 +- packages/ui/src/components/ui/HelpDialog.tsx | 80 +++--- .../ui/src/components/ui/JsonTreeView.tsx | 6 +- .../ui/src/components/ui/JsonTreeViewer.tsx | 6 +- .../ui/src/components/ui/MemoryDebugPanel.tsx | 14 +- .../src/components/ui/MobileOverlayPanel.tsx | 4 +- .../ui/src/components/ui/UpdateDialog.tsx | 25 +- packages/ui/src/components/ui/checkbox.tsx | 7 +- packages/ui/src/components/ui/command.tsx | 8 +- packages/ui/src/components/ui/dialog.tsx | 4 +- .../ui/src/components/ui/dropdown-menu.tsx | 8 +- .../ui/src/components/ui/number-input.tsx | 10 +- packages/ui/src/components/ui/select.tsx | 10 +- .../src/components/ui/sortable-tabs-strip.tsx | 11 +- packages/ui/src/components/views/DiffView.tsx | 29 +- .../ui/src/components/views/FilesView.tsx | 127 ++++----- packages/ui/src/components/views/GitView.tsx | 22 +- .../src/components/views/MultiRunWindow.tsx | 4 +- packages/ui/src/components/views/PlanView.tsx | 11 +- .../components/views/PreviewToggleButton.tsx | 7 +- .../ui/src/components/views/SettingsView.tsx | 88 +++--- .../ui/src/components/views/TerminalView.tsx | 30 +-- .../views/agent-manager/AgentGroupDetail.tsx | 23 +- .../agent-manager/AgentManagerEmptyState.tsx | 32 +-- .../agent-manager/AgentManagerSidebar.tsx | 21 +- .../components/views/git/AIHighlightsBox.tsx | 4 +- .../views/git/BranchIntegrationSection.tsx | 29 +- .../components/views/git/BranchSelector.tsx | 25 +- .../ui/src/components/views/git/ChangeRow.tsx | 9 +- .../components/views/git/ChangesSection.tsx | 8 +- .../components/views/git/CommitSection.tsx | 26 +- .../components/views/git/ConflictDialog.tsx | 14 +- .../components/views/git/GitEmptyState.tsx | 6 +- .../ui/src/components/views/git/GitHeader.tsx | 47 ++-- .../components/views/git/HistoryCommitRow.tsx | 6 +- .../components/views/git/HistorySection.tsx | 10 +- .../views/git/InProgressOperationBanner.tsx | 23 +- .../views/git/IntegrateCommitsSection.tsx | 12 +- .../views/git/PullRequestSection.tsx | 80 +++--- .../src/components/views/git/StashDialog.tsx | 6 +- .../components/views/git/StashesDialog.tsx | 18 +- .../src/components/views/git/SyncActions.tsx | 19 +- .../views/git/WorktreeBranchDisplay.tsx | 14 +- .../components/voice/BrowserVoiceButton.tsx | 17 +- .../components/voice/VoiceStatusIndicator.tsx | 24 +- packages/ui/src/constants/sidebar.ts | 24 +- packages/ui/src/lib/projectActions.ts | 62 ++--- packages/ui/src/lib/projectMeta.ts | 65 ++--- scripts/generate-icon-sprite.mjs | 250 ++++++++++++++++++ 173 files changed, 2342 insertions(+), 2227 deletions(-) create mode 100644 packages/ui/src/components/icon/Icon.tsx create mode 100644 packages/ui/src/components/icon/README.md create mode 100644 packages/ui/src/components/icon/icons.ts create mode 100644 packages/ui/src/components/icon/sprite.ts create mode 100644 scripts/generate-icon-sprite.mjs diff --git a/.opencode/skills/theme-system/SKILL.md b/.opencode/skills/theme-system/SKILL.md index 8fb7ecec..422c69be 100644 --- a/.opencode/skills/theme-system/SKILL.md +++ b/.opencode/skills/theme-system/SKILL.md @@ -1,6 +1,6 @@ --- name: theme-system -description: Use when creating or modifying UI components, styling, or visual elements in OpenChamber. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes. +description: Use when creating or modifying UI components, styling, visual elements, or icons in OpenChamber. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes. All icons must use the shared Icon component from the SVG sprite system - never import from @remixicon/react directly. license: MIT compatibility: opencode --- @@ -15,6 +15,7 @@ OpenChamber uses a JSON-based theme system. Themes are defined in `packages/ui/s - Creating or modifying UI components - Working with colors, backgrounds, borders, or text +- **Working with icons — adding, changing, or creating icon usages** ## Quick Decision Tree @@ -188,12 +189,110 @@ const { currentTheme } = useThemeSystem(); ``` +## Icon System (MANDATORY) + +OpenChamber uses an SVG sprite-based icon system. **Never import from `@remixicon/react`.** Always use the shared `Icon` component. + +### Import + +```tsx +import { Icon } from "@/components/icon/Icon"; +import type { IconName } from "@/components/icon/icons"; +``` + +### Usage + +```tsx + + +``` + +### Naming Convention + +Convert Remixicon component names to kebab-case sprite names: + +1. Strip `Ri` prefix +2. Strip `Line` suffix +3. Convert PascalCase to kebab-case +4. Lowercase everything + +| Remixicon | Sprite name | +|-----------|-------------| +| `RiArrowDownSLine` | `arrow-down-s` | +| `RiCheckLine` | `check` | +| `RiLoader4Line` | `loader-4` | +| `RiGithubFill` | `github-fill` | +| `RiBrainAi3Line` | `brain-ai-3` | + +### Fill Variants + +For filled (solid) icon variants, append `-fill` explicitly. The generator tries `Line` suffix first, then `Fill`, then bare name. + +```tsx + {/* RiGithubFill */} + {/* RiGithubLine (default) */} +``` + +### Sizing + +The `Icon` component has **no `size` prop**. Use Tailwind classes: + +```tsx + {/* 16px - most common */} + {/* 20px */} + {/* 12px */} +``` + +### Adding a New Icon (Workflow) + +**In order:** + +1. Use the icon in code with the correct kebab-case name: + ```tsx + + ``` + +2. If used as a value (not JSX), use `IconName` type: + ```tsx + const config = { icon: "new-icon-name" as const }; + ``` + +3. Regenerate the sprite: + ```bash + bun run icons:generate + ``` + +4. The script scans all source files, reverse-maps to Remixicon names, extracts SVG paths, and regenerates `sprite.ts`. + +5. Verify: `bun run type-check` + +**Do NOT manually edit `sprite.ts`.** Always regenerate. + +### Type Safety for Icon Values + +When icons are stored in objects/arrays, change the type from `ComponentType` to `IconName` and render via ``: + +```tsx +// ❌ Old: component reference +const items = [{ icon: RiStackLine }]; +return ; + +// ✅ New: IconName string +import type { IconName } from "@/components/icon/icons"; +const items: { icon: IconName }[] = [{ icon: "stack" }]; +return ; +``` + ## Wrong vs Right ### Wrong ```tsx -// Hardcoded colors +// ❌ Importing from @remixicon/react +import { RiArrowDownSLine } from "@remixicon/react"; + + +// ❌ Hardcoded colors
diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index 392d4dc5..58526d9f 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -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 = ({ autoOpenDraft = tr ? t('chat.container.returnToParent.titleNamed', { title: parentSession.title }) : t('chat.container.returnToParent.title')} > - + {t('chat.container.returnToParent.label')} ) : null; @@ -657,7 +657,6 @@ export const ChatContainer: React.FC = ({ autoOpenDraft = tr }; }, [currentSessionId, isDesktopExpandedInput, navigation, scrollRef]); - React.useLayoutEffect(() => { const container = scrollRef.current; if (!container) return; diff --git a/packages/ui/src/components/chat/ChatErrorBoundary.tsx b/packages/ui/src/components/chat/ChatErrorBoundary.tsx index 29d3e120..edc2514f 100644 --- a/packages/ui/src/components/chat/ChatErrorBoundary.tsx +++ b/packages/ui/src/components/chat/ChatErrorBoundary.tsx @@ -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 - + {this.props.texts.title} @@ -83,7 +83,7 @@ class ChatErrorBoundaryView extends React.Component diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 54dd3f68..082b3746 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -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')} > - + ) : null} - + ) : ( @@ -323,7 +309,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl title={t('chat.chatInput.actions.addAttachment')} aria-label={t('chat.chatInput.actions.addAttachment')} > - + @@ -332,7 +318,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl requestAnimationFrame(handlePickLocalFiles); }} > - + {t('chat.chatInput.actions.attachFiles')} - + {t('chat.chatInput.actions.linkGithubIssue')} - + {t('chat.chatInput.actions.linkGithubPr')} @@ -364,7 +350,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl title={t('chat.chatInput.actions.modelAgentSettings')} aria-label={t('chat.chatInput.actions.modelAgentSettings')} > - + ) : null} @@ -427,9 +413,9 @@ const PermissionAutoAcceptButton = React.memo(function PermissionAutoAcceptButto title={ariaLabel} > {permissionAutoAcceptEnabled ? ( - + ) : ( - + )} ); @@ -480,7 +466,7 @@ const FocusModeButton = React.memo(function FocusModeButton(props: FocusModeButt aria-label={t('chat.chatInput.focusMode.toggleAria')} aria-pressed={isExpandedInput} > - + @@ -547,7 +533,7 @@ const ComposerActionButtons = React.memo(function ComposerActionButtons(props: C )} aria-label={t('chat.chatInput.actions.sendMessageAria')} > - + ); @@ -574,7 +560,7 @@ const ComposerActionButtons = React.memo(function ComposerActionButtons(props: C )} aria-label={t('chat.chatInput.actions.queueMessageAria')} > - + ) : null} ) : null} @@ -3398,7 +3384,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo aria-label={t('chat.chatInput.previewContextRemove')} title={t('chat.chatInput.previewContextRemove')} > - + ) : null} @@ -3439,7 +3425,7 @@ const ChatInputComponent: React.FC = ({ 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')} > - + @@ -3492,7 +3478,7 @@ const ChatInputComponent: React.FC = ({ 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')} > - + @@ -3623,7 +3609,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo title={t('chat.chatInput.actions.attachFiles')} aria-label={t('chat.chatInput.actions.attachFiles')} > - +

diff --git a/packages/ui/src/components/chat/CommandAutocomplete.tsx b/packages/ui/src/components/chat/CommandAutocomplete.tsx index 0b9625fc..ff5caceb 100644 --- a/packages/ui/src/components/chat/CommandAutocomplete.tsx +++ b/packages/ui/src/components/chat/CommandAutocomplete.tsx @@ -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; + return ; case 'undo': - return ; + return ; case 'redo': - return ; + return ; case 'timeline': - return ; + return ; case 'compact': - return ; + return ; case 'review': - return ; + return ; case 'test': case 'build': case 'run': - return ; + return ; default: if (command.isBuiltIn) { - return ; + return ; } - return ; + return ; } }; @@ -315,7 +315,7 @@ export const CommandAutocomplete = React.forwardRef {loading ? (

- +
) : (
diff --git a/packages/ui/src/components/chat/FileAttachment.tsx b/packages/ui/src/components/chat/FileAttachment.tsx index 1caecbb0..55f9ff9e 100644 --- a/packages/ui/src/components/chat/FileAttachment.tsx +++ b/packages/ui/src/components/chat/FileAttachment.tsx @@ -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')} > - + @@ -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 })} > - + ); @@ -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 })} > - +
); @@ -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 })} > - + ); @@ -301,7 +301,7 @@ const VSCodeFileChip = memo(({ file, onRemove }: FileChipProps) => { aria-label={t('chat.fileAttachment.activeEditor.remove')} title={t('chat.fileAttachment.activeEditor.remove')} > - + @@ -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" > - + {`${displayName}:${selectionRange}`} @@ -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" > - + {displayName} @@ -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' ? ( - + ) : ( - + )}
{fileName} @@ -621,7 +621,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false } ) : (
{file.mime?.includes('pdf') ? ( - + ) : ( )} @@ -668,7 +668,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false } /> ) : (
- +
)} {filename} @@ -735,9 +735,9 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false } >
{githubLinkKind === 'pr' ? ( - + ) : ( - + )}
@@ -779,11 +779,11 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false } >
{file.mime?.startsWith('image/') ? ( - + ) : file.mime?.includes('pdf') ? ( - + ) : ( - + )}
diff --git a/packages/ui/src/components/chat/FileMentionAutocomplete.tsx b/packages/ui/src/components/chat/FileMentionAutocomplete.tsx index f3bf02bd..e607fca4 100644 --- a/packages/ui/src/components/chat/FileMentionAutocomplete.tsx +++ b/packages/ui/src/components/chat/FileMentionAutocomplete.tsx @@ -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; + return ; case 'json': - return ; + return ; case 'md': case 'mdx': - return ; + return ; case 'png': case 'jpg': case 'jpeg': case 'gif': case 'svg': - return ; + return ; default: - return ; + return ; } }; @@ -499,7 +499,7 @@ export const FileMentionAutocomplete = React.forwardRef {(!scopeResultsToActiveTab || activeTab === 'files') && loading ? (
- +
) : (
@@ -550,7 +550,7 @@ export const FileMentionAutocomplete = React.forwardRef handleFileSelect(dir)} onMouseEnter={() => setSelectedIndex(rowIndex)} > - + {displayPath} diff --git a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx index 87c226ad..4cd70dbb 100644 --- a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx +++ b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx @@ -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 - {copied ? : } + {copied ? : } {showMenu && (
@@ -286,7 +286,7 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject - + {showMenu && (
@@ -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 ? : } + {copied ? : }
@@ -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 ? : } + {copied ? : }
@@ -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 ? : } + {copied ? : }
@@ -793,7 +793,7 @@ const MarkdownCodeBlock: React.FC<{ aria-pressed={viewMode === 'preview'} aria-label={viewMode === 'preview' ? 'Show code' : 'Preview HTML'} > - {viewMode === 'preview' ? : } + {viewMode === 'preview' ? : } ) : null} {canPreview ? ( @@ -804,7 +804,7 @@ const MarkdownCodeBlock: React.FC<{ title="Download HTML" aria-label="Download HTML" > - + ) : null}
@@ -959,7 +959,7 @@ const buildMarkdownComponents = ({ title={previewTitle ?? previewLabel ?? 'Open preview pane'} data-loopback-preview-trigger="true" > -
- ) : ProjectIcon && ( - @@ -492,10 +483,9 @@ function SessionStatusHeader({ className="flex-shrink-0" title={`Sub-session: ${child.session.title || 'Untitled'}`} > - + ); })} @@ -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} > - + {/* Project info */} @@ -668,8 +656,8 @@ function SortableProjectItem({ onError={() => setImageFailed(true)} /> - ) : ProjectIcon ? ( - @@ -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" > - +
@@ -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" > - + {/* 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" > - +
@@ -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 */}
{status.hasRunning && ( - + )} {!status.hasRunning && status.hasUnread && (
@@ -921,8 +909,8 @@ function ProjectButton({ onError={() => setImageFailed(true)} /> - ) : ProjectIcon && ( - @@ -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')} > - +
); @@ -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')} > - + {/* Delete confirmation dialog */} diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index c6357440..c7959c8c 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -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; + +type IconComponent = IconName; type ProviderModel = Record & { 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 = { - 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 = ({ const iconStyle = { color: iconColor }; if (mode === 'full') { - return ; + return ; } if (mode === 'allow') { - return ; + return ; } if (mode === 'deny') { - return ; + return ; } - return ; + return ; }, [editToggleIconClass]); const currentProvider = getCurrentProvider(); @@ -1359,7 +1335,7 @@ export const ModelControls: React.FC = ({ return name.charAt(0).toUpperCase() + name.slice(1); }; - const renderIconBadge = (IconComp: IconComponent, label: string, key: string) => ( + const renderIconBadge = (iconName: IconComponent, label: string, key: string) => ( = ({ aria-label={label} role="img" > - + ); @@ -1577,7 +1553,6 @@ export const ModelControls: React.FC = ({
)} - {}
{t('chat.modelControls.permissions')}
@@ -1617,7 +1592,7 @@ export const ModelControls: React.FC = ({
{t('chat.modelControls.customPrompt')} - +
)} @@ -1756,7 +1731,7 @@ export const ModelControls: React.FC = ({ {getModelDisplayName(model)} - {isSelected ? : null} + {isSelected ? : null}
{contextText || indicatorIcons.length > 0 ? (
@@ -1770,14 +1745,14 @@ export const ModelControls: React.FC = ({ ) : null} {indicatorIcons.length > 0 ? (
- {indicatorIcons.map(({ key, icon: IconComponent, label }) => ( + {indicatorIcons.map(({ key, icon: iconName, label }) => ( - + ))}
@@ -1795,7 +1770,7 @@ export const ModelControls: React.FC = ({ aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')} > {variantLabel} - {isExpanded ? : } + {isExpanded ? : } ) : null}
@@ -1818,9 +1793,9 @@ export const ModelControls: React.FC = ({ : t('chat.modelControls.addToFavorites')} > {isFavoriteModel(providerId, modelId) ? ( - + ) : ( - + )}
@@ -1875,7 +1850,7 @@ export const ModelControls: React.FC = ({
- + setMobileModelQuery(event.target.value)} @@ -1889,7 +1864,7 @@ export const ModelControls: React.FC = ({ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" aria-label={t('chat.modelControls.clearSearch')} > - + )}
@@ -1905,7 +1880,7 @@ export const ModelControls: React.FC = ({ {filteredFavorites.length > 0 && (
- + {t('chat.modelControls.favorites')}
@@ -1923,7 +1898,7 @@ export const ModelControls: React.FC = ({ {filteredRecents.length > 0 && (
- + {t('chat.modelControls.recent')}
@@ -1971,9 +1946,9 @@ export const ModelControls: React.FC = ({ )}
{isExpanded ? ( - + ) : ( - + )} @@ -2037,7 +2012,7 @@ export const ModelControls: React.FC = ({ onClick={handleBack} className="flex items-center gap-1 rounded-lg px-1.5 py-1 typography-meta text-muted-foreground hover:bg-interactive-hover" > - + {t('onboarding.common.actions.back')}

{t('chat.modelControls.thinking')}

@@ -2056,7 +2031,7 @@ export const ModelControls: React.FC = ({ onClick={() => handleSelect(undefined)} > {t('chat.modelControls.default')} - {isDefault && } + {isDefault && } {targetVariants.map((variant) => { @@ -2075,7 +2050,7 @@ export const ModelControls: React.FC = ({ onClick={() => handleSelect(variant)} > {label} - {selected && } + {selected && } ); })} @@ -2122,7 +2097,7 @@ export const ModelControls: React.FC = ({ {capitalizeAgentName(agent.name)} {isSelected && ( - + )}
{agent.description && ( @@ -2288,7 +2263,7 @@ export const ModelControls: React.FC = ({ if (hasCapabilities) { slides.push(
- {indicatorIcons.map(({ id, icon: Icon, label }) => ( + {indicatorIcons.map(({ id, icon: iconName, label }) => ( = ({ role="img" title={label} > - + ))}
@@ -2355,7 +2330,7 @@ export const ModelControls: React.FC = ({ aria-label={t('chat.modelControls.reorderFavoriteAria')} title={t('chat.modelControls.reorderFavoriteTitle')} > - + ) : null}
@@ -2395,7 +2370,7 @@ export const ModelControls: React.FC = ({
) : null} {isSelected && ( - + )}
@@ -2669,7 +2644,7 @@ export const ModelControls: React.FC = ({ > {!isReady ? ( <> - + = ({ providerId={currentProviderId} className={cn(controlIconSize, 'flex-shrink-0')} /> - + ) : ( - + )} {isReady && ( = ({ {/* Search Input */}
- + = ({ className="typography-meta group flex items-center gap-1 rounded-md px-2 py-1.5 cursor-pointer hover:bg-interactive-hover/50" > - + {t('chat.modelControls.addNewProvider')}
@@ -2763,7 +2738,7 @@ export const ModelControls: React.FC = ({ - + {t('chat.modelControls.favorites')} {favoriteSortingEnabled ? ( @@ -2813,7 +2788,7 @@ export const ModelControls: React.FC = ({ - + {t('chat.modelControls.recent')} {filteredRecents.map(({ model, providerID, modelID }) => { @@ -2879,9 +2854,9 @@ export const ModelControls: React.FC = ({ {provider.name} {isExpanded ? ( - + ) : ( - + )}
@@ -2923,7 +2898,7 @@ export const ModelControls: React.FC = ({ > {!isReady ? ( <> - + {readinessLabel} @@ -2936,7 +2911,7 @@ export const ModelControls: React.FC = ({ className={cn(controlIconSize, 'flex-shrink-0')} /> ) : ( - + )} = ({
)} -
{t('chat.modelControls.permissions')}
@@ -3078,7 +3052,7 @@ export const ModelControls: React.FC = ({ {hasCustomPrompt && (
{t('chat.modelControls.customPrompt')} - +
)}
@@ -3106,7 +3080,7 @@ export const ModelControls: React.FC = ({ 'cursor-pointer hover:bg-transparent hover:opacity-70', )} > - + = ({ buttonHeight, )} > - + = ({ handleVariantSelect(undefined)}>
{t('chat.modelControls.default')} - {isDefault && } + {isDefault && }
{availableVariants.length > 0 && } @@ -3166,7 +3140,7 @@ export const ModelControls: React.FC = ({ >
{label} - {selected && } + {selected && }
); @@ -3194,7 +3168,7 @@ export const ModelControls: React.FC = ({ )}> {!isReady ? ( <> - = ({ ) : ( <> - = ({
- + = ({ onSelect={() => handleAgentChange(defaultAgentName)} >
- + {t('chat.modelControls.resetToDefault')}
@@ -3323,7 +3297,7 @@ export const ModelControls: React.FC = ({ > {!isReady ? ( <> - = ({ ) : ( <> - { type="button" className="flex min-w-0 max-w-full items-center gap-1 text-left text-muted-foreground" > - + {labelHead} {t('chat.pendingChanges.changedInWorkspace')} @@ -113,9 +113,9 @@ export const PendingChangesBar: React.FC = React.memo(() => { {totalRemoved > 0 ? -{totalRemoved} : null} {isExpanded ? ( - + ) : ( - + )} } diff --git a/packages/ui/src/components/chat/PermissionCard.tsx b/packages/ui/src/components/chat/PermissionCard.tsx index 6b06bd14..dc91234a 100644 --- a/packages/ui/src/components/chat/PermissionCard.tsx +++ b/packages/ui/src/components/chat/PermissionCard.tsx @@ -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 ; + return ; } if (tool === 'write' || tool === 'create' || tool === 'file_write') { - return ; + return ; } if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal' || tool === 'shell_command') { - return ; + return ; } if (tool === 'webfetch' || tool === 'fetch' || tool === 'curl' || tool === 'wget') { - return ; + return ; } - return ; + return ; }; const getToolDisplayName = (toolName: string): string => { @@ -325,7 +325,7 @@ export const PermissionCard: React.FC = ({
- + Permission Required @@ -376,7 +376,7 @@ export const PermissionCard: React.FC = ({ e.currentTarget.style.backgroundColor = 'rgb(var(--status-success) / 0.1)'; }} > - + Allow Once @@ -399,7 +399,7 @@ export const PermissionCard: React.FC = ({ e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.5)'; }} > - + {(() => { 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 = ({ e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.5)'; }} > - + Always Allow )} @@ -455,7 +455,7 @@ export const PermissionCard: React.FC = ({ e.currentTarget.style.backgroundColor = 'rgb(var(--status-error) / 0.1)'; }} > - + Deny diff --git a/packages/ui/src/components/chat/PermissionRequest.tsx b/packages/ui/src/components/chat/PermissionRequest.tsx index e741cd33..c7c3e879 100644 --- a/packages/ui/src/components/chat/PermissionRequest.tsx +++ b/packages/ui/src/components/chat/PermissionRequest.tsx @@ -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 = ({ e.currentTarget.style.backgroundColor = 'transparent'; }} > - + {t('chat.permissionRequest.actions.once')} @@ -95,7 +95,7 @@ export const PermissionRequest: React.FC = ({ e.currentTarget.style.backgroundColor = 'transparent'; }} > - + {t('chat.permissionRequest.actions.always')} @@ -117,7 +117,7 @@ export const PermissionRequest: React.FC = ({ e.currentTarget.style.backgroundColor = 'transparent'; }} > - + {t('chat.permissionRequest.actions.reject')} diff --git a/packages/ui/src/components/chat/QuestionCard.tsx b/packages/ui/src/components/chat/QuestionCard.tsx index e2d696cf..7015e89c 100644 --- a/packages/ui/src/components/chat/QuestionCard.tsx +++ b/packages/ui/src/components/chat/QuestionCard.tsx @@ -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 = ({ question }) => { {/* Header */}
- + {t('chat.questionCard.inputNeeded')} {isFromSubagent ? ( @@ -257,7 +257,7 @@ export const QuestionCard: React.FC = ({ question }) => { : 'text-foreground/85 hover:text-foreground hover:bg-interactive-hover/20' )} > - {isSummary ? : null} + {isSummary ? : null} {tab.label} ); @@ -366,7 +366,7 @@ export const QuestionCard: React.FC = ({ question }) => { )} >
- @@ -427,7 +427,7 @@ export const QuestionCard: React.FC = ({ question }) => { 'disabled:opacity-50 disabled:cursor-not-allowed' )} > - {requiredSatisfied ? : } + {requiredSatisfied ? : } {requiredSatisfied ? t('chat.questionCard.submit') : t('chat.questionCard.next')} @@ -441,7 +441,7 @@ export const QuestionCard: React.FC = ({ question }) => { 'disabled:opacity-50 disabled:cursor-not-allowed' )} > - + {t('chat.questionCard.dismiss')} diff --git a/packages/ui/src/components/chat/QueuedMessageChips.tsx b/packages/ui/src/components/chat/QueuedMessageChips.tsx index 60029896..bf85b24b 100644 --- a/packages/ui/src/components/chat/QueuedMessageChips.tsx +++ b/packages/ui/src/components/chat/QueuedMessageChips.tsx @@ -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" > - + 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')} > - +
); diff --git a/packages/ui/src/components/chat/StatusRow.tsx b/packages/ui/src/components/chat/StatusRow.tsx index 6b2d1735..4dba3e65 100644 --- a/packages/ui/src/components/chat/StatusRow.tsx +++ b/packages/ui/src/components/chat/StatusRow.tsx @@ -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 = { }; const priorityIcon: Record = { - high:
diff --git a/packages/ui/src/components/chat/TurnChangedFilesDropdown.tsx b/packages/ui/src/components/chat/TurnChangedFilesDropdown.tsx index e7c81c5f..1b2cdf88 100644 --- a/packages/ui/src/components/chat/TurnChangedFilesDropdown.tsx +++ b/packages/ui/src/components/chat/TurnChangedFilesDropdown.tsx @@ -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 = onPointerDownCapture={syncPortalContainer} onFocusCapture={syncPortalContainer} > - + {label} {isExpanded ? ( - + ) : ( - + )} } diff --git a/packages/ui/src/components/chat/components/ScrollToBottomButton.tsx b/packages/ui/src/components/chat/components/ScrollToBottomButton.tsx index 814c4ce1..15726e76 100644 --- a/packages/ui/src/components/chat/components/ScrollToBottomButton.tsx +++ b/packages/ui/src/components/chat/components/ScrollToBottomButton.tsx @@ -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 = ({ 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')} > - +
); diff --git a/packages/ui/src/components/chat/message/DiffViewToggle.tsx b/packages/ui/src/components/chat/message/DiffViewToggle.tsx index dc883099..0e5409d5 100644 --- a/packages/ui/src/components/chat/message/DiffViewToggle.tsx +++ b/packages/ui/src/components/chat/message/DiffViewToggle.tsx @@ -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 = ({ mode, onModeChan title={mode === 'side-by-side' ? 'Switch to unified view' : 'Switch to side-by-side view'} > {mode === 'side-by-side' ? ( - + ) : ( - + )} ); diff --git a/packages/ui/src/components/chat/message/MessageBody.tsx b/packages/ui/src/components/chat/message/MessageBody.tsx index d98ce164..f014785f 100644 --- a/packages/ui/src/components/chat/message/MessageBody.tsx +++ b/packages/ui/src/components/chat/message/MessageBody.tsx @@ -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 ? : } + {copiedOutput ? : }
{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(); }} > - + {t('chat.messageBody.actions.revert')} @@ -484,7 +481,7 @@ const UserMessageBody = React.memo(({ messageId, parts, isMobile, alwaysShowActi effectiveOnFork(); }} > - + {t('chat.messageBody.actions.fork')} @@ -510,9 +507,9 @@ const UserMessageBody = React.memo(({ messageId, parts, isMobile, alwaysShowActi }} > {isMessageCopied ? ( - + ) : ( - + )} @@ -770,9 +767,9 @@ const AssistantMessageActionButtons = React.memo(({ }} > {isMessageCopied ? ( - + ) : ( - + )} @@ -796,9 +793,9 @@ const AssistantMessageActionButtons = React.memo(({ }} > {isSharing ? ( - + ) : ( - + )} @@ -820,9 +817,9 @@ const AssistantMessageActionButtons = React.memo(({ onClick={handleTTSClick} > {isTTSPlaying ? ( - + ) : ( - + )} @@ -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); }} > - + {t('chat.messageBody.actions.openPreview')} @@ -1750,7 +1743,7 @@ const AssistantMessageBody = React.memo(({ onPointerDown={(event) => event.stopPropagation()} onClick={handleSaveAsPlanClick} > - + {t('chat.messageBody.actions.saveAsPlan')} @@ -1766,7 +1759,7 @@ const AssistantMessageBody = React.memo(({ onPointerDown={(event) => event.stopPropagation()} onClick={handleForkClick} > - + {t('chat.messageBody.actions.startNewSession')} @@ -1824,7 +1817,7 @@ const AssistantMessageBody = React.memo(({ : 'bg-[var(--status-error-background)] border-[var(--status-error-border)]', )}>
- @@ -1862,7 +1855,7 @@ const AssistantMessageBody = React.memo(({ - + {turnDurationText} @@ -1876,7 +1869,7 @@ const AssistantMessageBody = React.memo(({ className={footerTimestampClassName} aria-label={`Message time: ${footerTimestamp}`} > - + {footerTimestamp} diff --git a/packages/ui/src/components/chat/message/MessageHeader.tsx b/packages/ui/src/components/chat/message/MessageHeader.tsx index 80ecd822..a70bf88d 100644 --- a/packages/ui/src/components/chat/message/MessageHeader.tsx +++ b/packages/ui/src/components/chat/message/MessageHeader.tsx @@ -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 = ({ isUser, providerID, agent
{isUser ? (
- +
) : (
@@ -38,10 +38,9 @@ const MessageHeader: React.FC = ({ isUser, providerID, agent onError={handleLogoError} /> ) : ( - + )}
)} @@ -64,7 +63,7 @@ const MessageHeader: React.FC = ({ isUser, providerID, agent getAgentColor(agentName).class )} > - + {agentName}
)} @@ -85,7 +84,7 @@ const MessageHeader: React.FC = ({ isUser, providerID, agent : undefined } > - + {variant.length > 0 ? variant[0].toLowerCase() + variant.slice(1) : variant}
)} diff --git a/packages/ui/src/components/chat/message/TextSelectionMenu.tsx b/packages/ui/src/components/chat/message/TextSelectionMenu.tsx index 4dcce714..07c061b3 100644 --- a/packages/ui/src/components/chat/message/TextSelectionMenu.tsx +++ b/packages/ui/src/components/chat/message/TextSelectionMenu.tsx @@ -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 = ({ containerR title={t('chat.textSelection.title.addToCurrentChat')} type="button" > - + {t('chat.textSelection.actions.addToChat')} @@ -608,7 +607,7 @@ export const TextSelectionMenu: React.FC = ({ containerR title={t('chat.textSelection.title.newSessionWithSelection')} type="button" > - + {t('chat.textSelection.actions.newSession')} @@ -624,7 +623,7 @@ export const TextSelectionMenu: React.FC = ({ containerR title={t('chat.textSelection.actions.copy')} type="button" > - + {t('chat.textSelection.actions.copy')} @@ -642,7 +641,7 @@ export const TextSelectionMenu: React.FC = ({ containerR title={t('chat.textSelection.title.saveInsightToNotes')} type="button" > - {isAddingToNotes ? : } + {isAddingToNotes ? : } {t('chat.textSelection.actions.addToNotes')} ) : null} @@ -685,7 +684,7 @@ export const TextSelectionMenu: React.FC = ({ containerR title={t('chat.textSelection.title.addToCurrentChat')} type="button" > - + {t('chat.textSelection.actions.addToChat')} @@ -703,7 +702,7 @@ export const TextSelectionMenu: React.FC = ({ containerR title={t('chat.textSelection.title.newSessionWithSelection')} type="button" > - + {t('chat.textSelection.actions.newSession')} @@ -724,7 +723,7 @@ export const TextSelectionMenu: React.FC = ({ containerR title={t('chat.textSelection.title.saveInsightToNotes')} type="button" > - {isAddingToNotes ? : } + {isAddingToNotes ? : } {t('chat.textSelection.actions.addToNotes')} diff --git a/packages/ui/src/components/chat/message/ToolOutputDialog.tsx b/packages/ui/src/components/chat/message/ToolOutputDialog.tsx index 5041c9aa..3a2cce18 100644 --- a/packages/ui/src/components/chat/message/ToolOutputDialog.tsx +++ b/packages/ui/src/components/chat/message/ToolOutputDialog.tsx @@ -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 ; + return ; } if (tool === 'image-preview') { - return ; + return ; } if (tool === 'mermaid-preview') { - return ; + return ; } if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') { - return ; + return ; } if (tool === 'write' || tool === 'create' || tool === 'file_write') { - return ; + return ; } if (tool === 'read' || tool === 'view' || tool === 'file_read' || tool === 'cat') { - return ; + return ; } if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal') { - return ; + return ; } if (tool === 'list' || tool === 'ls' || tool === 'dir' || tool === 'list_files') { - return ; + return ; } if (tool === 'search' || tool === 'grep' || tool === 'find' || tool === 'ripgrep') { - return ; + return ; } if (tool === 'glob') { - return ; + return ; } if (tool === 'fetch' || tool === 'curl' || tool === 'wget' || tool === 'webfetch') { - return ; + return ; } if (tool === 'web-search' || tool === 'websearch' || tool === 'search_web' || tool === 'google' || tool === 'bing' || tool === 'duckduckgo') { - return ; + return ; } if (tool === 'todowrite' || tool === 'todoread') { - return ; + return ; } if (tool === 'plan_enter') { - return ; + return ; } if (tool === 'plan_exit') { - return ; + return ; } if (tool.startsWith('git')) { - return ; + return ; } - return ; + return ; }; 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')} > - + )} @@ -480,7 +480,7 @@ const ImagePreviewDialog: React.FC<{ onClick={() => onOpenChange(false)} aria-label={t('chat.toolOutputDialog.image.closeAria')} > - +
@@ -927,7 +927,7 @@ const MermaidPreviewDialog: React.FC<{ onClick={() => onOpenChange(false)} aria-label={t('chat.toolOutputDialog.mermaid.closeAria')} > - +
{status === 'loading' && (
- + {t('chat.toolOutputDialog.mermaid.loading')}
)} @@ -1016,7 +1016,7 @@ const ToolOutputDialog: React.FC = ({ popup, onOpenChange
{popup.metadata?.tool ? getToolIcon(popup.metadata.tool as string) : ( - + )} {popup.title} {popup.isDiff && ( diff --git a/packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx b/packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx index 9e838999..7ec819df 100644 --- a/packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx +++ b/packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx @@ -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 = ({ onClick={onToggle} > - + ; - 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 = ({ 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 = ({ !isExpanded && (alwaysShowActions ? 'opacity-0' : 'group-hover/tool:opacity-0') )} > - +
= ({ !isExpanded && (alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/tool:opacity-100') )} > - {isExpanded ? : } + {isExpanded ? : }
{label} diff --git a/packages/ui/src/components/chat/message/parts/ToolPart.tsx b/packages/ui/src/components/chat/message/parts/ToolPart.tsx index d3b4ed2b..b060ff2b 100644 --- a/packages/ui/src/components/chat/message/parts/ToolPart.tsx +++ b/packages/ui/src/components/chat/message/parts/ToolPart.tsx @@ -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 = ({ disableHorizontal ? 'overflow-y-auto overflow-x-hidden' : 'overflow-auto', className, )} - size={24} + >
{children} @@ -1190,7 +1190,7 @@ const TaskToolSummary: React.FC<{ onPointerDown={(event) => event.stopPropagation()} onClick={handleOpenSession} > - + {t('chat.toolPart.openSubtask', { type: agentType.charAt(0).toUpperCase() + agentType.slice(1) })} )} @@ -1208,9 +1208,9 @@ const TaskToolSummary: React.FC<{ }} > {isOutputExpanded ? ( - + ) : ( - + )} {t('chat.toolPart.output')} @@ -1884,8 +1884,6 @@ const ToolPart: React.FC = ({ 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 = ({ taskSessionId, ]); - const taskSummaryLenRef = React.useRef(taskSummaryEntries.length); React.useEffect(() => { if (!isTaskTool) { @@ -2565,7 +2562,7 @@ const ToolPart: React.FC = ({ !isExpanded && (alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/tool:opacity-100') )} > - {isExpanded ? : } + {isExpanded ? : }
{isMultiFileApplyPatch ? ( diff --git a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx index 41f48956..c054b41e 100644 --- a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx +++ b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx @@ -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 = ({ 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" > - + )}
{ 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 ; + return ; } if (tool === 'write' || tool === 'create' || tool === 'file_write') { - return ; + return ; } if (tool === 'read' || tool === 'view' || tool === 'file_read' || tool === 'cat') { - return ; + return ; } if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal') { - return ; + return ; } if (tool === 'list' || tool === 'ls' || tool === 'dir' || tool === 'list_files') { - return ; + return ; } if (tool === 'search' || tool === 'grep' || tool === 'find' || tool === 'ripgrep') { - return ; + return ; } if (tool === 'glob') { - return ; + return ; } if (tool === 'fetch' || tool === 'curl' || tool === 'wget' || tool === 'webfetch') { - return ; + return ; } if ( tool === 'web-search' || @@ -57,31 +39,31 @@ export const getToolIcon = (toolName: string) => { tool === 'duckduckgo' || tool === 'perplexity' ) { - return ; + return ; } if (tool === 'todowrite' || tool === 'todoread') { - return ; + return ; } if (tool === 'structuredoutput' || tool === 'structured_output') { - return ; + return ; } if (tool === 'skill') { - return ; + return ; } if (tool === 'task') { - return ; + return ; } if (tool === 'question') { - return ; + return ; } if (tool === 'plan_enter') { - return ; + return ; } if (tool === 'plan_exit') { - return ; + return ; } if (tool.startsWith('git')) { - return ; + return ; } - return ; + return ; }; diff --git a/packages/ui/src/components/chat/message/toolRenderers.tsx b/packages/ui/src/components/chat/message/toolRenderers.tsx index 8344cd88..d63ab4bc 100644 --- a/packages/ui/src/components/chat/message/toolRenderers.tsx +++ b/packages/ui/src/components/chat/message/toolRenderers.tsx @@ -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(/^\s*\n?/, '').replace(/\n?<\/file>\s*$/, ''); @@ -473,13 +473,13 @@ export const renderTodoOutput = ( {todosByStatus.completed.length > 0 && (
- + {labels.completed}
{todosByStatus.completed.map((todo, idx) => (
- + {todo.content}
))} diff --git a/packages/ui/src/components/comments/InlineCommentCard.tsx b/packages/ui/src/components/comments/InlineCommentCard.tsx index 72b39351..6eae933e 100644 --- a/packages/ui/src/components/comments/InlineCommentCard.tsx +++ b/packages/ui/src/components/comments/InlineCommentCard.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import { RiMoreLine, RiDeleteBinLine, RiEditLine, RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'; import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore'; import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; import { @@ -11,6 +10,7 @@ import { import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface InlineCommentCardProps { @@ -76,12 +76,12 @@ export function InlineCommentCard({ > {isOpen ? ( <> - + {t('inlineComment.actions.showLess')} ) : ( <> - + {t('inlineComment.actions.showMore')} )} @@ -102,16 +102,16 @@ export function InlineCommentCard({ size="icon" className="h-6 w-6 -mr-1 text-muted-foreground hover:text-foreground" > - + - + {t('inlineComment.actions.editComment')} - + {t('inlineComment.actions.deleteComment')} diff --git a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx index 6fcc8b71..8fdd616e 100644 --- a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx +++ b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx @@ -15,26 +15,9 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { - RiAddLine, - RiCheckLine, - RiCloudOffLine, - RiEarthLine, - RiLoader4Line, - RiMore2Line, - RiPencilLine, - RiPlug2Line, - RiRefreshLine, - RiServerLine, - RiSettings3Line, - RiShieldKeyholeLine, - RiStarFill, - RiStarLine, - RiDeleteBinLine, - RiWindowLine, -} from '@remixicon/react'; import { cn } from '@/lib/utils'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { isTauriShell, isDesktopShell } from '@/lib/desktop'; import { useUIStore } from '@/stores/useUIStore'; import { useI18n } from '@/lib/i18n'; @@ -117,11 +100,11 @@ const statusLabelKey = (status: HostProbeResult['status'] | null): }; const statusIcon = (status: HostProbeResult['status'] | null) => { - if (status === 'ok') return ; - if (status === 'auth') return ; - if (status === 'wrong-service') return ; - if (status === 'unreachable') return ; - return ; + if (status === 'ok') return ; + if (status === 'auth') return ; + if (status === 'wrong-service') return ; + if (status === 'unreachable') return ; + return ; }; const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); @@ -732,14 +715,14 @@ export function DesktopHostSwitcherDialog({ disabled={!tauriAvailable || isLoading || isProbing} aria-label={t('desktopHostSwitcher.actions.refreshInstancesAria')} > - +
) : ( - + {t('desktopHostSwitcher.title')} @@ -764,7 +747,7 @@ export function DesktopHostSwitcherDialog({ onClick={() => void probeAll(allHosts)} disabled={!tauriAvailable || isLoading || isProbing} > - + {t('desktopHostSwitcher.actions.refresh')}
@@ -775,7 +758,7 @@ export function DesktopHostSwitcherDialog({
{t('desktopHostSwitcher.ssh.needInstancesHint')}
@@ -868,7 +851,7 @@ export function DesktopHostSwitcherDialog({ disabled={isSaving} onClick={(e) => e.stopPropagation()} > - + @@ -879,7 +862,7 @@ export function DesktopHostSwitcherDialog({ }} disabled={isSaving} > - + {t('desktopHostSwitcher.actions.edit')} - + {t('desktopHostSwitcher.actions.delete')} @@ -917,7 +900,7 @@ export function DesktopHostSwitcherDialog({ void connectSshHostInPlace(host); }} > - {switchingHostId === host.id ? : } + {switchingHostId === host.id ? : } {t('desktopHostSwitcher.actions.connect')} ) : ( @@ -942,7 +925,7 @@ export function DesktopHostSwitcherDialog({ aria-label={isDefault ? t('desktopHostSwitcher.actions.defaultInstanceAria') : t('desktopHostSwitcher.actions.setAsDefaultAria')} disabled={isSaving || (!isDefault && (statusKind === 'unreachable' || statusKind === 'wrong-service'))} > - {isDefault ? : } + {isDefault ? : } @@ -967,7 +950,7 @@ export function DesktopHostSwitcherDialog({ disabled={statusKind === 'unreachable' || statusKind === 'wrong-service'} aria-label={t('desktopHostSwitcher.actions.openInNewWindowAria')} > - + @@ -993,7 +976,7 @@ export function DesktopHostSwitcherDialog({ {t('desktopHostSwitcher.actions.cancel')}
@@ -1025,7 +1008,7 @@ export function DesktopHostSwitcherDialog({ onClick={() => setIsAddFormOpen(true)} disabled={!tauriAvailable || isSaving} > - + {t('desktopHostSwitcher.actions.addInstance')}
@@ -1056,7 +1039,7 @@ export function DesktopHostSwitcherDialog({ onClick={() => void addHost()} disabled={!tauriAvailable || isSaving || !newUrl.trim()} > - {isSaving ? : null} + {isSaving ? : null} {t('desktopHostSwitcher.actions.add')}
@@ -1104,7 +1087,7 @@ export function DesktopHostSwitcherDialog({ - + {t('desktopHostSwitcher.ssh.connectingTo', { host: sshSwitchModal.hostLabel || t('desktopHostSwitcher.ssh.instanceFallback') })} @@ -1337,7 +1320,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost data-oc-host-switcher className={cn(headerIconButtonClass, 'relative w-auto px-3')} > - + {safeEffectiveLabel} @@ -1397,7 +1380,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost onClick={retryStartupSsh} disabled={startupSshModal.connecting || !startupSshModal.hostId} > - {startupSshModal.connecting ? : null} + {startupSshModal.connecting ? : null} {t('desktopHostSwitcher.actions.retry')}
@@ -1425,7 +1408,7 @@ export function DesktopHostSwitcherInline() { className="w-full justify-center" onClick={() => setOpen(true)} > - + {t('desktopHostSwitcher.actions.switchInstance')} diff --git a/packages/ui/src/components/desktop/OpenInAppButton.tsx b/packages/ui/src/components/desktop/OpenInAppButton.tsx index 2dd8fda4..f890fb70 100644 --- a/packages/ui/src/components/desktop/OpenInAppButton.tsx +++ b/packages/ui/src/components/desktop/OpenInAppButton.tsx @@ -7,12 +7,12 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { copyTextToClipboard } from '@/lib/clipboard'; import { cn } from '@/lib/utils'; import { isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop'; import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS } from '@/lib/openInApps'; import { useOpenInAppsStore, type OpenInAppOption } from '@/stores/useOpenInAppsStore'; -import { RiArrowDownSLine, RiCheckLine, RiFileCopyLine, RiRefreshLine } from '@remixicon/react'; import { useI18n } from '@/lib/i18n'; const FINDER_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAB+C9pSAAAACXBIWXMAABYlAAAWJQFJUiTwAAABnWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yNTY8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MjU2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl6wHhsAAAXaSURBVFgJ7VddbBRVFP5mdme6dOnu2tZawOAPjfxU+bGQYCRAsvw8qNGEQPTRJ0I0amL0wfjgA/HBR8KLD8YgDxJEUkVFxSYYTSRii6DQQCMGIQqlW7p0u+zMzo/fuTuzO9Nt0Td94CRn7pl7z5zznZ977y5wh/7jDGiz+T979qD5Ujbfd90xlll+stOF1uI40B1+4HhkjnZk9CgLQ9iXp2/BdcbgVc/h0sAgduywudJEMwLY9Of4ugtW5p3CpL7W1jTN88VmjdQYvnDKF1mczkYuNZLeCVg3X8fa9u+nqzUB2HRpdN2pSseRQknPoUL1Jo2ICTrPGcCzdwPdHENcAnicKRqcAk7cpL5J1r0JlAtPYV1XDETM/FtH3m19r+f5by+XjNX/xnmCX3/cCzydi4CKiC7lw+PArhGgoPPFq/6E0+9vwM6d5VBNpuv03cLNfeNTRh9KnJIiV2/PvSngycC5RD+dE5zb3g7s6QESzAZc2l6wuY9SnWIAxv10r81uU85Vt1FvtpEtlc/SMFUkUofeZ2IBta0DWDmXgkfbyTRz1qAYAMczOz3p1elOxYPyEllj421hdELViPO6Kudk3ia3UGe5ABDbvtnJZ52SdYmCZ3stdeexBabFdeAbYopEowtagVUZqFapBrtAGqpiVaFrGgyjZlrmTD5yEqoEJj4iFMuA62i6L3WPZkAiuHgarZ/vbWSBkTzO2rfTR4XOJVJhjfX44MBn+OTocVWbcF5MalxXPeVL6zYonoGo44YOtDI7qHC1lkL5nHnOc+tJRi3K6iygLNGMjt1A1XVV6iUzOvVtAvMlS2I/yBYlRf8MgA6szmXQ1jDfKhSgjft6DRtrkgarAiAw5nI9v2WDSn+Zxfd9DawGxIlPPQUg0A2HGABfEIYlCDU4+q0d8O+jRzHCCFYy+nu4BaeYAoksBCDrPYsXQQ6iitgiSQaS1FHHtMzFil4DpxTl4UhORSn4WOaaiGsbu4iFRkMnYQlEV0oSJQGQ4FyYgSRDjpqPZcCR6EOOWonIEsBqArAIQOMLzw0VXRRERF2VoA6Atk1+MzsASekMJYgaFEeHR4Cr85lNGntYzgKCYd/NSNIDCXr0ZJ2jwTsjSvEMzFQCCVmKHBRahn2DNb4rDRx8pnbXOOIg0JELLMHOF1AUkaRj1V8c2TookkMS83WK9QCVpRwtf5wCykQWRKDyJ44Ytc452QUV6inmN9IDIv/6y2+YLDuqTywBEHxv8rsoxQC4Fpf4cZ2pbJ4/huxXr0EvFmoRCrAIVymLQ3Eid0GJYPsPfISBLwdwi79YQnCqBNS7LQDP5qYSAKEDypOrX4WVWYLsFy+i9cwh6CUmUKIJI2Gq5cSbnLLw849D2Ld3L4olC1u3P0c1ow5Ozgixa3puWChONG1D3eLZUQOglvng+Vp5dBfseesx5/yHyI4cBTL3wsssRGs2g6/ppHijiMLoNSSMNHofy6Nn6SPsAR02nUoTtrDTSrdoi8CTni55rlOsCf1ypaDxlFMNU1epCV5XL6Y6dmOq+BeS48NIlq7Anpjg5dOFbPdDWLQyj/aubnUKSkMKi3NhkUd4kieYtbRbYS0bFAOQKI8NO363z1RJHmamtnlwhGksxV2w/gl29WRtm8kWtWUnRShLnQvXgDOXmLg2HzlvbDiyHD8Y517YP2i4FtueFPbB9FFqKcyobk4A5y7zquUFa7IXojyHoeXmAFcY755vaI6A56Xsofm/7+cmblBTpOldQ5vs3PJDVS+RVSAaus2SpJTO80t4NTNSOQfCDrtFkBevA0ME6HGvPdDpFlekzm7rf3nFQNRQEwBZTL9warObWfx21Uv1+fx1ERqVNampGoOHpF1tsdp07RnoGMxK1vT97rbK4IP6+Tc+fWXVsahaYGL6VO09d//GXHXr7jVeqmuppqU6ff4x0RO6lqRxgxHJpWKSlcw5eWfjq5rq/CdhaL5l6JWxjDc6bP7w5sn+/uMs2B36H2bgb6v9raK0+o9IAAAAAElFTkSuQmCC'; @@ -167,7 +167,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps) )} aria-label={t('openInApp.actions.chooseAppAria')} > - + void handleCopyPath()}> - + {t('openInApp.actions.copyPath')} @@ -195,7 +195,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps) /> {app.label} {selectedApp.id === app.id ? ( - + ) : null} ); @@ -205,7 +205,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps) className="flex items-center gap-2" onClick={() => void loadInstalledApps(true)} > - + {t('openInApp.actions.refreshApps')} ) : null} diff --git a/packages/ui/src/components/icon/Icon.tsx b/packages/ui/src/components/icon/Icon.tsx new file mode 100644 index 00000000..3956cfba --- /dev/null +++ b/packages/ui/src/components/icon/Icon.tsx @@ -0,0 +1,57 @@ +import React from "react" +import { cn } from "@/lib/utils" +import { iconSpriteData } from "./sprite" +import type { IconName } from "./icons" + +const SPRITE_ID = "openchamber-icon-sprite" + +let spriteInjected = false + +function ensureSpriteOnce() { + if (spriteInjected) return + if (typeof document === "undefined") return + const body = document.body + if (!body) return + + const existing = document.getElementById(SPRITE_ID) + if (existing) { + spriteInjected = true + return + } + + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg") + svg.id = SPRITE_ID + svg.setAttribute("aria-hidden", "true") + svg.style.display = "none" + svg.innerHTML = Object.entries(iconSpriteData) + .map(([name, content]) => `${content}`) + .join("") + body.insertBefore(svg, body.firstChild) + spriteInjected = true +} + +export interface IconProps extends React.ComponentPropsWithoutRef<"svg"> { + name: IconName +} + +export const Icon = React.memo(({ name, className, ...rest }: IconProps) => { + // Inline sprite injection during render – must run before tries + // to resolve the #oc-* reference during the same commit. + if (typeof document !== "undefined") { + ensureSpriteOnce() + } + + return ( + + ) +}) + +Icon.displayName = "Icon" diff --git a/packages/ui/src/components/icon/README.md b/packages/ui/src/components/icon/README.md new file mode 100644 index 00000000..71f2765d --- /dev/null +++ b/packages/ui/src/components/icon/README.md @@ -0,0 +1,78 @@ +# Icon System + +OpenChamber uses an SVG sprite-based icon system for performance. All icons are rendered via a single hidden SVG sprite injected into the DOM, referenced by ``. + +## Usage + +```tsx +import { Icon } from "@/components/icon/Icon"; + + + +``` + +## Icon Names + +Icons use kebab-case names based on Remixicon. To find an icon name: + +1. Start from the Remixicon component name: `RiArrowDownSLine` +2. Remove `Ri` prefix → `ArrowDownSLine` +3. Convert to kebab-case → `arrow-down-s` + +Common suffixes: +- `Line` / `Fill` are dropped from the sprite name +- Numbers are preserved: `RiChat4Line` → `chat-4` + +## Adding a New Icon + +1. Import and use it in your code: `` +2. Run `bun run icons:sprite` to regenerate the sprite with the new icon +3. The script scans `packages/ui/src` for all `RiX` usages and extracts SVG paths + +If the icon doesn't exist in the sprite, the script will warn you. + +## Sizing + +The `Icon` component does not have a `size` prop. Use Tailwind classes instead: + +```tsx + {/* 16px */} + {/* 20px */} + {/* 24px */} +``` + +## Type Safety + +```tsx +import type { IconName } from "@/components/icon/icons"; + +const icon: IconName = "arrow-down-s"; // type-checked +``` + +## Architecture + +- `sprite.ts` — Auto-generated SVG path data (run `bun run generate-icon-sprite` to regenerate) +- `Icon.tsx` — The `` component, injects sprite on first mount +- `icons.ts` — TypeScript type `IconName` + +The sprite is injected as a hidden `` element containing `` elements. Each `` renders ``. + +## Migration from @remixicon/react + +Old: +```tsx +import { RiArrowDownSLine } from "@remixicon/react"; + +``` + +New: +```tsx +import { Icon } from "@/components/icon/Icon"; + +``` + +If an icon is used as a component reference (not JSX): +```tsx +// Old: const icon = RiStackLine; return ; +// New: const icon: IconName = "stack"; return ; +``` diff --git a/packages/ui/src/components/icon/icons.ts b/packages/ui/src/components/icon/icons.ts new file mode 100644 index 00000000..abd0fe49 --- /dev/null +++ b/packages/ui/src/components/icon/icons.ts @@ -0,0 +1 @@ +export type IconName = keyof typeof import("./sprite").iconSpriteData; diff --git a/packages/ui/src/components/icon/sprite.ts b/packages/ui/src/components/icon/sprite.ts new file mode 100644 index 00000000..862e545f --- /dev/null +++ b/packages/ui/src/components/icon/sprite.ts @@ -0,0 +1,228 @@ +// This file is auto-generated by scripts/generate-icon-sprite.mjs +// Do not edit manually. Run the script to update. + +export const iconSpriteData: Record = { + "add-circle": ``, + "add": ``, + "ai-agent-fill": ``, + "ai-agent": ``, + "ai-generate-2": ``, + "alert": ``, + "align-justify": ``, + "apple": ``, + "archive": ``, + "archive-stack": ``, + "arrow-down": ``, + "arrow-down-s": ``, + "arrow-go-back": ``, + "arrow-go-forward": ``, + "arrow-left": ``, + "arrow-left-long": ``, + "arrow-left-right": ``, + "arrow-left-s": ``, + "arrow-right": ``, + "arrow-right-s": ``, + "arrow-up-double": ``, + "arrow-up": ``, + "arrow-up-s": ``, + "attachment-2": ``, + "bar-chart-2": ``, + "bar-chart-box": ``, + "book": ``, + "book-open": ``, + "booklet": ``, + "brain-ai-3": ``, + "brain": ``, + "briefcase": ``, + "bug": ``, + "calendar": ``, + "calendar-schedule": ``, + "camera": ``, + "chat-1": ``, + "chat-3": ``, + "chat-4": ``, + "chat-ai-3": ``, + "chat-history": ``, + "chat-new": ``, + "check": ``, + "checkbox-blank-circle-fill": ``, + "checkbox-blank": ``, + "checkbox-circle": ``, + "checkbox": ``, + "checkbox-multiple": ``, + "clipboard": ``, + "close-circle": ``, + "close": ``, + "cloud": ``, + "cloud-off": ``, + "code-ai": ``, + "code-box": ``, + "code": ``, + "code-sslash": ``, + "command": ``, + "computer": ``, + "contract-up-down": ``, + "corner-down-left": ``, + "cursor": ``, + "database-2": ``, + "delete-bin": ``, + "discord-fill": ``, + "donut-chart-fill": ``, + "donut-chart": ``, + "download-cloud": ``, + "download": ``, + "drag-move-2": ``, + "draggable": ``, + "earth": ``, + "edit-2": ``, + "edit": ``, + "emotion-happy": ``, + "equalizer-2": ``, + "error-warning": ``, + "expand-up-down": ``, + "external-link": ``, + "eye": ``, + "eye-off": ``, + "file-add": ``, + "file-check-fill": ``, + "file-check": ``, + "file-code": ``, + "file-copy-2": ``, + "file-copy": ``, + "file-edit": ``, + "file-image": ``, + "file": ``, + "file-list-2": ``, + "file-music": ``, + "file-pdf": ``, + "file-search": ``, + "file-text": ``, + "file-transfer": ``, + "file-video": ``, + "flashlight": ``, + "flask": ``, + "folder-3-fill": ``, + "folder-3": ``, + "folder-6": ``, + "folder-add": ``, + "folder": ``, + "folder-open-fill": ``, + "folder-open": ``, + "folder-received": ``, + "folders": ``, + "fullscreen-exit": ``, + "fullscreen": ``, + "gamepad": ``, + "git-branch": ``, + "git-close-pull-request": ``, + "git-commit": ``, + "git-merge": ``, + "git-pr-draft": ``, + "git-pull-request": ``, + "git-repository": ``, + "github-fill": ``, + "github": ``, + "global": ``, + "graduation-cap": ``, + "hammer": ``, + "heart": ``, + "history": ``, + "home": ``, + "hourglass-fill": ``, + "hourglass": ``, + "image-download": ``, + "inbox-archive": ``, + "inbox-unarchive-fill": ``, + "inbox-unarchive": ``, + "information": ``, + "key": ``, + "layout-column": ``, + "layout-left": ``, + "layout-right": ``, + "leaf": ``, + "lightbulb": ``, + "link-unlink-m": ``, + "list-check-2": ``, + "list-check-3": ``, + "list-unordered": ``, + "loader-4": ``, + "loader": ``, + "lock-2": ``, + "lock": ``, + "lock-unlock": ``, + "loop-right-ai": ``, + "macbook": ``, + "menu-fold-2": ``, + "menu-search": ``, + "message-2": ``, + "mic": ``, + "mic-off": ``, + "more-2-fill": ``, + "more-2": ``, + "more": ``, + "music": ``, + "node-tree": ``, + "notification-3": ``, + "palette": ``, + "pencil-ai": ``, + "pencil": ``, + "picture-in-picture-2": ``, + "pie-chart": ``, + "play": ``, + "play-list-add": ``, + "plug-2": ``, + "plug": ``, + "pulse": ``, + "pushpin-2-fill": ``, + "pushpin-2": ``, + "pushpin": ``, + "question": ``, + "record-circle": ``, + "refresh": ``, + "restart": ``, + "robot-2": ``, + "robot": ``, + "rocket": ``, + "save-3": ``, + "scissors": ``, + "search-eye": ``, + "search": ``, + "send-plane-2": ``, + "send-plane": ``, + "server": ``, + "settings-3": ``, + "share-2": ``, + "shield-check": ``, + "shield-keyhole": ``, + "shield": ``, + "shield-user": ``, + "shuffle": ``, + "slash-commands-2": ``, + "smartphone": ``, + "sparkling": ``, + "split-cells-horizontal": ``, + "stack": ``, + "star-fill": ``, + "star": ``, + "sticky-note": ``, + "stop-circle": ``, + "stop": ``, + "subtract": ``, + "survey": ``, + "task": ``, + "terminal-box": ``, + "terminal": ``, + "terminal-window": ``, + "text": ``, + "text-wrap": ``, + "time": ``, + "timer": ``, + "tools": ``, + "twitter-xfill": ``, + "unpin": ``, + "user-3": ``, + "user": ``, + "voice-recognition": ``, + "volume-up": ``, + "window": ``, +}; diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx index 22e2406e..7f12ba0d 100644 --- a/packages/ui/src/components/layout/ContextPanel.tsx +++ b/packages/ui/src/components/layout/ContextPanel.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RiArrowLeftRightLine, RiChat4Line, RiCloseLine, RiDonutChartFill, RiFileTextLine, RiFullscreenExitLine, RiFullscreenLine, RiGlobalLine, RiRefreshLine, RiExternalLinkLine, RiTerminalBoxLine, RiCursorLine } from '@remixicon/react'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; import { Button } from '@/components/ui/button'; @@ -20,6 +19,7 @@ import { useSessionUIStore } from '@/sync/session-ui-store'; import { useInputStore } from '@/sync/input-store'; import { ContextPanelContent } from './ContextSidebarTab'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; const CONTEXT_PANEL_MIN_WIDTH = 360; const CONTEXT_PANEL_MAX_WIDTH = 1400; @@ -296,23 +296,23 @@ const getTabIcon = (tab: { mode: 'diff' | 'file' | 'context' | 'plan' | 'chat' | } if (tab.mode === 'diff') { - return ; + return ; } if (tab.mode === 'plan') { - return ; + return ; } if (tab.mode === 'context') { - return ; + return ; } if (tab.mode === 'chat') { - return ; + return ; } if (tab.mode === 'preview') { - return ; + return ; } return undefined; @@ -919,7 +919,7 @@ const PreviewPane: React.FC = ({ rawUrl, onNavigate }) => { aria-label={t('contextPanel.preview.actions.reload')} disabled={!effectiveSrc} > - + {isLoopback ? ( ) : null} {isLoopback ? ( @@ -961,7 +961,7 @@ const PreviewPane: React.FC = ({ rawUrl, onNavigate }) => { aria-label={bridgeReady ? t('contextPanel.preview.console.open') : t('contextPanel.preview.console.waiting')} disabled={!bridgeReady && consoleEvents.length === 0} > - + {consoleErrorCount > 0 ? ( {consoleErrorCount} ) : null} @@ -1380,7 +1380,7 @@ export const ContextPanel: React.FC = () => { ? openContextPreview(effectiveDirectory, url)} /> : (
- +
{t('contextPanel.preview.title')}
{t('contextPanel.preview.description')}
@@ -1433,7 +1433,7 @@ export const ContextPanel: React.FC = () => { title={isExpanded ? t('contextPanel.actions.collapsePanel') : t('contextPanel.actions.expandPanel')} aria-label={isExpanded ? t('contextPanel.actions.collapsePanel') : t('contextPanel.actions.expandPanel')} > - {isExpanded ? : } + {isExpanded ? : }
diff --git a/packages/ui/src/components/layout/ContextSidebarTab.tsx b/packages/ui/src/components/layout/ContextSidebarTab.tsx index f0613d1d..15879613 100644 --- a/packages/ui/src/components/layout/ContextSidebarTab.tsx +++ b/packages/ui/src/components/layout/ContextSidebarTab.tsx @@ -1,9 +1,9 @@ import React from 'react'; import type { Message, Part } from '@opencode-ai/sdk/v2'; -import { RiCheckLine, RiFileCopyLine } from '@remixicon/react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { deriveMessageRole } from '@/components/chat/message/messageRole'; +import { Icon } from "@/components/icon/Icon"; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator'; import { useConfigStore } from '@/stores/useConfigStore'; @@ -565,7 +565,7 @@ export const ContextPanelContent: React.FC = () => { aria-label={isCopied ? t('contextSidebar.actions.copied') : t('contextSidebar.actions.copyJson')} title={isCopied ? t('contextSidebar.actions.copied') : t('contextSidebar.actions.copy')} > - {isCopied ? : } + {isCopied ? : }
void; className?: string; - Icon: RemixiconComponentType; + Icon: IconName; iconClassName?: string; }; @@ -90,7 +90,7 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({ ariaLabel, onClick, className, - Icon, + Icon: iconName, iconClassName, }: HeaderIconActionButtonProps) { if (!visible) { @@ -106,7 +106,7 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({ aria-label={ariaLabel} className={className ?? DESKTOP_HEADER_ICON_BUTTON_CLASS} > - + @@ -162,7 +162,7 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({ referrerPolicy="no-referrer" /> ) : ( - + )} @@ -195,7 +195,7 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({ /> ) : (
- +
)} @@ -208,7 +208,7 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({ ) : null} - {isCurrent ? : null} + {isCurrent ? : null} ); })} @@ -231,7 +231,7 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({ referrerPolicy="no-referrer" /> ) : ( - + )} ); @@ -321,7 +321,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ isDesktopApp ? 'w-auto max-w-[14rem] justify-start gap-1.5 px-2.5' : 'h-8 w-8' )} > - + {isDesktopApp ? ( {compactCurrentInstanceLabel} ) : null} @@ -411,7 +411,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ disabled={isQuotaLoading || isUsageRefreshSpinning} aria-label={t('header.services.refreshRateLimitsAria')} > - + @@ -485,7 +485,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ > {family.familyLabel} - {isExpanded ? : } + {isExpanded ? : }
@@ -552,7 +552,6 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ ); }); - const isSameContextUsage = ( a: SessionContextUsage | null, b: SessionContextUsage | null, @@ -623,7 +622,7 @@ const getActiveContextMode = (panelState: { interface TabConfig { id: MainTab; label: string; - icon: RemixiconComponentType | 'diff'; + icon: IconName | 'diff'; badge?: number; showDot?: boolean; } @@ -1179,7 +1178,6 @@ export const Header: React.FC = ({ return lastProjectActionsContextRef.current; }, [actionDirectory, activeProjectRef]); - const planModeEnabled = useFeatureFlagsStore((state) => state.planModeEnabled); const isSessionPlanAvailable = useSessionUIStore((state) => state.isSessionPlanAvailable); const planTabAvailable = planModeEnabled && currentSessionId ? isSessionPlanAvailable(currentSessionId) : false; @@ -1508,17 +1506,17 @@ export const Header: React.FC = ({ const tabs: TabConfig[] = React.useMemo(() => { if (isMobile) { const base: TabConfig[] = [ - { id: 'chat', label: t('layout.mainTab.chat'), icon: RiChat4Line }, + { id: 'chat', label: t('layout.mainTab.chat'), icon: "chat-4" }, ]; if (showPlanTab) { - base.push({ id: 'plan', label: t('layout.mainTab.plan'), icon: RiFileTextLine }); + base.push({ id: 'plan', label: t('layout.mainTab.plan'), icon: "file-text" }); } base.push( { id: 'diff', label: t('layout.mainTab.diff'), icon: 'diff' }, - { id: 'files', label: t('layout.mainTab.files'), icon: RiFolder6Line }, - { id: 'terminal', label: t('layout.mainTab.terminal'), icon: RiTerminalBoxLine }, + { id: 'files', label: t('layout.mainTab.files'), icon: "folder-6" }, + { id: 'terminal', label: t('layout.mainTab.terminal'), icon: "terminal-box" }, ); return base; @@ -1539,13 +1537,13 @@ export const Header: React.FC = ({ }, [activeMainTab, isMobile, setActiveMainTab]); const servicesTabs = React.useMemo(() => { - const base: Array<{ value: 'instance' | 'usage' | 'mcp'; label: string; icon: RemixiconComponentType }> = []; + const base: Array<{ value: 'instance' | 'usage' | 'mcp'; label: string; icon: IconName }> = []; if (isDesktopApp) { - base.push({ value: 'instance', label: t('layout.services.instance'), icon: RiServerLine }); + base.push({ value: 'instance', label: t('layout.services.instance'), icon: "server" }); } base.push( - { value: 'usage', label: t('layout.services.usage'), icon: RiTimerLine }, - { value: 'mcp', label: 'MCP', icon: McpIcon as unknown as RemixiconComponentType } + { value: 'usage', label: t('layout.services.usage'), icon: "timer" }, + { value: 'mcp', label: 'MCP', icon: McpIcon as unknown as IconName } ); return base; }, [isDesktopApp, t]); @@ -1554,7 +1552,7 @@ export const Header: React.FC = ({ return servicesTabs.map((tab) => ({ id: tab.value, label: tab.label, - icon: , + icon: , })); }, [servicesTabs]); @@ -1628,8 +1626,8 @@ export const Header: React.FC = ({ const mobileServicesTabItems = React.useMemo(() => { return [ - { id: 'usage', label: t('layout.services.usage'), icon: }, - { id: 'mcp', label: 'MCP', icon: }, + { id: 'usage', label: t('layout.services.usage'), icon: }, + { id: 'mcp', label: 'MCP', icon: }, ]; }, [t]); @@ -1709,14 +1707,14 @@ export const Header: React.FC = ({ const renderTab = (tab: TabConfig) => { const isActive = activeMainTab === tab.id; const isDiffTab = tab.icon === 'diff'; - const Icon = isDiffTab ? null : (tab.icon as RemixiconComponentType); + const tabIconName = isDiffTab ? null : (tab.icon as IconName); const isChatTab = tab.id === 'chat'; const renderIcon = (iconSize: number) => { if (isDiffTab) { return ; } - return Icon ? : null; + return tabIconName ? : null; }; const tabButton = ( @@ -1766,7 +1764,7 @@ export const Header: React.FC = ({ onClick={handleOpenContextPlan} className={cn(desktopHeaderIconButtonClass, isContextPlanActive && 'bg-[var(--interactive-hover)]')} > - + @@ -1807,13 +1805,13 @@ export const Header: React.FC = ({ title={t('header.actions.terminalPanelWithShortcut', { shortcut: shortcutLabel('toggle_terminal') })} ariaLabel={t('header.actions.toggleTerminalPanelAria')} onClick={toggleBottomTerminal} - Icon={RiTerminalBoxLine} + Icon={'terminal-box'} /> = ({ ariaLabel={t('header.actions.openSessionsAria')} onClick={handleOpenSessionSwitcher} className={`${desktopHeaderIconButtonClass} shrink-0`} - Icon={RiLayoutLeftLine} + Icon={'layout-left'} />
@@ -1861,7 +1859,7 @@ export const Header: React.FC = ({ onClick={handleHeaderNewSession} className={cn(desktopHeaderIconButtonClass, 'mr-6 shrink-0')} > - + @@ -1886,7 +1884,7 @@ export const Header: React.FC = ({ {activeProjectLabel ? {activeProjectLabel} : null} {currentBranchLabel ? ( - + {currentBranchLabel} ) : null} @@ -1902,7 +1900,7 @@ export const Header: React.FC = ({ "inline-flex min-w-0 items-center gap-0.5", worktreeBadgeKind === 'attention' || worktreeBadgeKind === 'invalid' || worktreeBadgeKind === 'missing' ? 'text-status-warning' : 'text-muted-foreground/60' )}> - + {worktreeBadge} ) : null} @@ -1943,7 +1941,7 @@ export const Header: React.FC = ({ ariaLabel={isNewSessionDraftOpen ? t('header.actions.newMiniChatAria') : t('header.actions.openSessionMiniChatAria')} onClick={handleOpenCurrentMiniChat} className={cn(desktopHeaderIconButtonClass, desktopSidebarActionsInline && showDesktopHeaderContextUsage ? 'mr-3.5' : 'mr-1')} - Icon={RiPictureInPicture2Line} + Icon={'picture-in-picture-2'} /> {desktopSidebarActionsInline ? desktopSidebarActions : null} {!desktopSidebarActionsInline && desktopRightSidebarActionsHost @@ -1968,7 +1966,7 @@ export const Header: React.FC = ({ )} aria-label={leftDrawerOpen ? t('header.actions.closeSessionsAria') : t('header.actions.openSessionsAria')} > - + ) : isSessionSwitcherOpen ? ( ) : ( )} @@ -2009,7 +2007,7 @@ export const Header: React.FC = ({ {tabs.map((tab) => { const isActive = activeMainTab === tab.id; const isDiffTab = tab.icon === 'diff'; - const Icon = isDiffTab ? null : (tab.icon as RemixiconComponentType); + const tabIconName = isDiffTab ? null : (tab.icon as IconName); return ( @@ -2032,8 +2030,8 @@ export const Header: React.FC = ({ > {isDiffTab ? ( - ) : Icon ? ( - + ) : tabIconName ? ( + ) : null} {tab.badge !== undefined && tab.badge > 0 && ( @@ -2088,7 +2086,7 @@ export const Header: React.FC = ({ aria-label={t('header.services.viewAria')} className={mobileHeaderIconButtonClass} > - + @@ -2128,7 +2126,7 @@ export const Header: React.FC = ({ className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover" aria-label={t('header.services.closeAria')} > - +
@@ -2187,7 +2185,7 @@ export const Header: React.FC = ({ disabled={isQuotaLoading || isUsageRefreshSpinning} aria-label={t('header.services.refreshRateLimitsAria')} > - + @@ -2279,9 +2277,9 @@ export const Header: React.FC = ({ {family.familyLabel} {isExpanded ? ( - + ) : ( - + )} @@ -2348,7 +2346,7 @@ export const Header: React.FC = ({ )} aria-label={rightDrawerOpen ? 'Close git sidebar' : 'Open git sidebar'} > - + diff --git a/packages/ui/src/components/layout/ProjectActionsButton.tsx b/packages/ui/src/components/layout/ProjectActionsButton.tsx index 31e33324..54b08b75 100644 --- a/packages/ui/src/components/layout/ProjectActionsButton.tsx +++ b/packages/ui/src/components/layout/ProjectActionsButton.tsx @@ -1,13 +1,4 @@ import React from 'react'; -import { - RiAddLine, - RiArrowDownSLine, - RiGlobalLine, - RiLoader4Line, - RiPlayLine, - RiSearchLine, - RiStopLine, -} from '@remixicon/react'; import { DropdownMenu, DropdownMenuContent, @@ -17,6 +8,7 @@ import { } from '@/components/ui/dropdown-menu'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useDeviceInfo } from '@/lib/device'; @@ -737,9 +729,9 @@ export const ProjectActionsButton = ({ } const selectedIconKey = (resolvedSelected.icon || 'play') as keyof typeof PROJECT_ACTION_ICON_MAP; - const SelectedIcon = resolvedSelected.id === AUTO_DISCOVER_ACTION_ID - ? RiSearchLine - : PROJECT_ACTION_ICON_MAP[selectedIconKey] || RiPlayLine; + const selectedIconName = resolvedSelected.id === AUTO_DISCOVER_ACTION_ID + ? 'search' + : PROJECT_ACTION_ICON_MAP[selectedIconKey] || 'play'; const selectedButtonLabel = formatActionButtonLabel( resolvedSelected.name, t('projectActions.label.fallbackAction'), @@ -778,10 +770,10 @@ export const ProjectActionsButton = ({ : t('projectActions.actions.runNamedAria', { name: resolvedSelected.name })} > {isStoppingSelected || isWaitingForSelectedPreview - ? + ? : selectedRunning - ? - : } + ? + : } {showSelectedPreviewButton ? ( @@ -792,7 +784,7 @@ export const ProjectActionsButton = ({ aria-label={t('projectActions.actions.openPreview')} onClick={handleOpenSelectedPreview} > - + {t('projectActions.actions.openPreview')} @@ -805,20 +797,20 @@ export const ProjectActionsButton = ({ className="app-region-no-drag -ml-1 inline-flex h-9 w-5 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-interactive-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" aria-label={t('projectActions.actions.chooseActionAria')} > - + - + {t('projectActions.actions.addNewAction')} {displayActions.map((entry) => { const iconKey = (entry.icon || 'play') as keyof typeof PROJECT_ACTION_ICON_MAP; - const Icon = entry.id === AUTO_DISCOVER_ACTION_ID - ? RiSearchLine - : PROJECT_ACTION_ICON_MAP[iconKey] || RiPlayLine; + const iconName = entry.id === AUTO_DISCOVER_ACTION_ID + ? 'search' + : PROJECT_ACTION_ICON_MAP[iconKey] || 'play'; const runKey = toProjectActionRunKey(normalizedDirectory, entry.id); const runState = projectActionRuns[runKey]; const isRunning = Boolean(runState); @@ -832,12 +824,12 @@ export const ProjectActionsButton = ({ handleSelectAction(entry, true); }} > - + {entry.name} {isStopping || runState?.status === 'waiting-for-preview' - ? + ? : isRunning - ? + ? : null} ); @@ -873,10 +865,10 @@ export const ProjectActionsButton = ({ > {isStoppingSelected || isWaitingForSelectedPreview - ? + ? : selectedRunning - ? - : } + ? + : } {!compact ? {selectedButtonLabel} : null} @@ -894,7 +886,7 @@ export const ProjectActionsButton = ({ )} aria-label={t('projectActions.actions.openPreview')} > - + {t('projectActions.actions.openPreview')} @@ -912,20 +904,20 @@ export const ProjectActionsButton = ({ )} aria-label={t('projectActions.actions.chooseActionAria')} > - + - + {t('projectActions.actions.addNewAction')} {displayActions.map((entry) => { const iconKey = (entry.icon || 'play') as keyof typeof PROJECT_ACTION_ICON_MAP; - const Icon = entry.id === AUTO_DISCOVER_ACTION_ID - ? RiSearchLine - : PROJECT_ACTION_ICON_MAP[iconKey] || RiPlayLine; + const iconName = entry.id === AUTO_DISCOVER_ACTION_ID + ? 'search' + : PROJECT_ACTION_ICON_MAP[iconKey] || 'play'; const runKey = toProjectActionRunKey(normalizedDirectory, entry.id); const runState = projectActionRuns[runKey]; const isRunning = Boolean(runState); @@ -939,12 +931,12 @@ export const ProjectActionsButton = ({ handleSelectAction(entry); }} > - + {entry.name} {isStopping || runState?.status === 'waiting-for-preview' - ? + ? : isRunning - ? + ? : null} ); diff --git a/packages/ui/src/components/layout/ProjectEditDialog.tsx b/packages/ui/src/components/layout/ProjectEditDialog.tsx index 511d9fe6..06ee364e 100644 --- a/packages/ui/src/components/layout/ProjectEditDialog.tsx +++ b/packages/ui/src/components/layout/ProjectEditDialog.tsx @@ -14,6 +14,7 @@ import { PROJECT_ICONS, PROJECT_COLORS, PROJECT_COLOR_MAP, getProjectIconImageUr import { useProjectsStore } from '@/stores/useProjectsStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useI18n } from '@/lib/i18n'; +import { Icon } from "@/components/icon/Icon"; interface ProjectEditDialogProps { open: boolean; @@ -329,7 +330,7 @@ export const ProjectEditDialog: React.FC = ({ {PROJECT_ICONS.map((i) => { - const IconComponent = i.Icon; + const iconName = i.Icon; return ( setContextMenuPath(null)}> {canRename && ( { e.stopPropagation(); onOpenDialog('rename', node); }}> - {t('sidebarFilesTree.menu.rename')} + {t('sidebarFilesTree.menu.rename')} )} { @@ -271,19 +256,19 @@ const FileRow: React.FC = ({ toast.error(t('sidebarFilesTree.toast.copyFailed')); }); }}> - {t('sidebarFilesTree.menu.copyPath')} + {t('sidebarFilesTree.menu.copyPath')} {!isDir && downloadFile && ( { e.stopPropagation(); void downloadFile(node.path); }}> - {t('sidebarFilesTree.menu.save')} + {t('sidebarFilesTree.menu.save')} )} {canReveal && ( { e.stopPropagation(); onRevealPath(node.path); }}> - {t(getRevealLabelKey())} + {t(getRevealLabelKey())} )} {isDir && (canCreateFile || canCreateFolder) && ( @@ -291,12 +276,12 @@ const FileRow: React.FC = ({ {canCreateFile && ( { e.stopPropagation(); onOpenDialog('createFile', node); }}> - {t('sidebarFilesTree.menu.newFile')} + {t('sidebarFilesTree.menu.newFile')} )} {canCreateFolder && ( { e.stopPropagation(); onOpenDialog('createFolder', node); }}> - {t('sidebarFilesTree.menu.newFolder')} + {t('sidebarFilesTree.menu.newFolder')} )} @@ -308,7 +293,7 @@ const FileRow: React.FC = ({ onClick={(e) => { e.stopPropagation(); onOpenDialog('delete', node); }} className="text-destructive focus:text-destructive" > - {t('sidebarFilesTree.menu.delete')} + {t('sidebarFilesTree.menu.delete')} )} @@ -817,7 +802,7 @@ export const SidebarFilesTree: React.FC = () => {
- + { searchInputRef.current?.focus(); }} > - + ) : null}
@@ -847,7 +832,7 @@ export const SidebarFilesTree: React.FC = () => { className="h-8 w-8 p-0 flex-shrink-0" title={t('sidebarFilesTree.actions.newFileTitle')} > - + )} {canCreateFolder && ( @@ -858,11 +843,11 @@ export const SidebarFilesTree: React.FC = () => { className="h-8 w-8 p-0 flex-shrink-0" title={t('sidebarFilesTree.actions.newFolderTitle')} > - + )}
@@ -870,7 +855,7 @@ export const SidebarFilesTree: React.FC = () => {
    {searching ? (
  • - + {t('sidebarFilesTree.state.searching')}
  • ) : searchResults.length > 0 ? ( @@ -956,7 +941,7 @@ export const SidebarFilesTree: React.FC = () => { onClick={() => void handleDialogSubmit()} disabled={isDialogSubmitting || (activeDialog !== 'delete' && !dialogInputValue.trim())} > - {isDialogSubmitting ? : ( + {isDialogSubmitting ? : ( activeDialog === 'delete' ? t('sidebarFilesTree.dialog.delete.confirm') : t('sidebarFilesTree.dialog.confirm') )} diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index 02eb82fe..5cf478c9 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -23,6 +23,7 @@ import { useI18n } from '@/lib/i18n'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar'; import { PaceIndicator } from '@/components/sections/usage/PaceIndicator'; +import { Icon } from "@/components/icon/Icon"; import { formatQuotaValueLabel, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota'; import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore'; import { useUpdateStore } from '@/stores/useUpdateStore'; @@ -30,7 +31,6 @@ import { updateDesktopSettings } from '@/lib/persistence'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; import type { UsageWindow } from '@/types'; import type { SessionContextUsage } from '@/stores/types/sessionTypes'; -import { RiAddLine, RiArrowLeftLine, RiRefreshLine, RiRobot2Line, RiSettings3Line, RiTimerLine } from '@remixicon/react'; const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView }))); @@ -220,7 +220,6 @@ export const VSCodeLayout: React.FC = () => { setCurrentView('sessions'); }, []); - // Listen for connection status changes React.useEffect(() => { // Catch up with the latest status even if the extension posted the connection message @@ -695,7 +694,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on className="inline-flex h-7 w-7 items-center justify-center text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" aria-label={t('vscodeLayout.actions.backToSessionsAria')} > - + )}

    {title}

    @@ -705,7 +704,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" aria-label={t('vscodeLayout.actions.newSessionAria')} > - + )} {onAgentManager && ( @@ -714,7 +713,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" aria-label={t('vscodeLayout.actions.openAgentManagerAria')} > - + )} {showMcp && ( @@ -737,7 +736,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" disabled={isQuotaLoading} > - + = ({ title, showBack, onBack, on disabled={isQuotaLoading} aria-label={t('vscodeLayout.quota.actions.refreshAria')} > - + @@ -870,7 +869,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" aria-label={t('vscodeLayout.actions.settingsAria')} > - + )} {showContextUsage && stableContextUsage && stableContextUsage.totalTokens > 0 && ( diff --git a/packages/ui/src/components/mcp/McpDropdown.tsx b/packages/ui/src/components/mcp/McpDropdown.tsx index 0acc8b62..d6976c89 100644 --- a/packages/ui/src/components/mcp/McpDropdown.tsx +++ b/packages/ui/src/components/mcp/McpDropdown.tsx @@ -1,6 +1,5 @@ import React from 'react'; import type { McpStatus } from '@opencode-ai/sdk/v2'; -import { RiRefreshLine } from '@remixicon/react'; import { DropdownMenu, @@ -20,6 +19,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useMcpConfigStore } from '@/stores/useMcpConfigStore'; import { computeMcpHealth, useMcpStore } from '@/stores/useMcpStore'; import { McpIcon } from '@/components/icons/McpIcon'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; const statusTooltip = ( @@ -132,7 +132,7 @@ export const McpDropdownContent: React.FC = ({ active, onClick={handleRefresh} aria-label={t('mcpDropdown.actions.refreshAria')} > - + @@ -402,7 +402,7 @@ export const McpDropdown: React.FC = ({ headerIconButtonClass onClick={handleRefresh} aria-label={t('mcpDropdown.actions.refreshAria')} > - + {closeButton} diff --git a/packages/ui/src/components/mini-chat/MiniChatLayout.tsx b/packages/ui/src/components/mini-chat/MiniChatLayout.tsx index fb8ec216..c3fc967a 100644 --- a/packages/ui/src/components/mini-chat/MiniChatLayout.tsx +++ b/packages/ui/src/components/mini-chat/MiniChatLayout.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RiExternalLinkLine, RiGitBranchLine, RiPushpin2Fill, RiPushpin2Line } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { ChatContainer } from '@/components/chat/ChatContainer'; import { ChatSurfaceProvider } from '@/components/chat/ChatSurfaceContext'; @@ -15,6 +14,7 @@ import { useProjectsStore } from '@/stores/useProjectsStore'; import { useGitBranchLabel, useGitStore } from '@/stores/useGitStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { resolveSessionDiffStats } from '@/components/session/sidebar/utils'; +import { Icon } from "@/components/icon/Icon"; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import type { SessionContextUsage } from '@/stores/types/sessionTypes'; @@ -266,7 +266,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => { {projectLabel} {branchLabel ? ( - + {branchLabel} ) : null} @@ -302,7 +302,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => { title={pinned ? t('miniChat.actions.unpin') : t('miniChat.actions.pin')} style={noDragRegionStyle} > - {pinned ? : } + {pinned ? : } ); diff --git a/packages/ui/src/components/multirun/ModelMultiSelect.tsx b/packages/ui/src/components/multirun/ModelMultiSelect.tsx index e10fcc22..c0b0a4e1 100644 --- a/packages/ui/src/components/multirun/ModelMultiSelect.tsx +++ b/packages/ui/src/components/multirun/ModelMultiSelect.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { RiAddLine, RiBrainAi3Line, RiCloseLine, RiSearchLine, RiStarFill, RiTimeLine } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { isIMECompositionEvent } from '@/lib/ime'; import { useConfigStore } from '@/stores/useConfigStore'; @@ -79,7 +79,7 @@ export const ModelChip: React.FC<{ onClick={onRemove} className="text-muted-foreground hover:text-foreground ml-0.5" > - + ); @@ -349,7 +349,7 @@ export const ModelMultiSelect: React.FC = ({ setIsOpen(!isOpen); }} > - + {addButtonLabel ?? t('multirun.modelMultiSelect.actions.addModel')} @@ -426,7 +426,7 @@ export const ModelMultiSelect: React.FC = ({ {/* Search input */}
    - + = ({ {filteredFavorites.length > 0 && ( <>
    - + {t('multirun.modelMultiSelect.sections.favorites')}
    {filteredFavorites.map(({ model, providerID, modelID }) => { @@ -470,7 +470,7 @@ export const ModelMultiSelect: React.FC = ({ <> {filteredFavorites.length > 0 &&
    }
    - + {t('multirun.modelMultiSelect.sections.recent')}
    {filteredRecents.map(({ model, providerID, modelID }) => { @@ -554,7 +554,7 @@ export const ModelMultiSelect: React.FC = ({ size="chip" className="px-2 gap-1.5 rounded-md !border-border/80 !bg-[var(--surface-subtle)] hover:!bg-[var(--interactive-hover)]/70 typography-meta font-medium text-foreground" > - = ({ children }) => ( @@ -168,7 +168,7 @@ export const MultiRunLauncher: React.FC = ({ iconColor: currentTheme.colors.surface.foreground, }, ); - const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null; + const projectIconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null; const iconColor = project.color ? PROJECT_COLOR_MAP[project.color] : undefined; return ( @@ -180,10 +180,10 @@ export const MultiRunLauncher: React.FC = ({ > - ) : ProjectIcon ? ( - + ) : projectIconName ? ( + ) : ( - + )} {displayLabel} @@ -615,7 +615,7 @@ export const MultiRunLauncher: React.FC = ({ aria-label={t('multirun.launcher.actions.closeEsc')} className="inline-flex h-9 w-9 items-center justify-center p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag" > - + @@ -716,7 +716,7 @@ export const MultiRunLauncher: React.FC = ({ {/* ── Setup commands (collapsible, full width) ── */} - + {t('multirun.launcher.setupCommands.label')} @@ -733,7 +733,7 @@ export const MultiRunLauncher: React.FC = ({ {configuredSetupCount} )} - @@ -765,7 +765,7 @@ export const MultiRunLauncher: React.FC = ({ className="flex-shrink-0 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50" aria-label={t('multirun.launcher.setupCommands.removeCommandAria')} > - +
    ))} @@ -774,7 +774,7 @@ export const MultiRunLauncher: React.FC = ({ onClick={() => setSetupCommands([...setupCommands, ''])} className="flex items-center gap-1 typography-meta text-muted-foreground hover:text-foreground transition-colors px-1" > - + {t('multirun.launcher.setupCommands.addCommand')} @@ -854,7 +854,7 @@ export const MultiRunLauncher: React.FC = ({ onClick={() => fileInputRef.current?.click()} className="inline-flex items-center gap-1 h-6 px-2 rounded-md typography-micro text-muted-foreground hover:text-foreground hover:bg-[var(--interactive-hover)]/50 transition-colors" > - + {t('multirun.launcher.attachments.attach')} @@ -871,9 +871,9 @@ export const MultiRunLauncher: React.FC = ({ }} > {file.mimeType.startsWith('image/') ? ( - + ) : ( - + )} {file.filename} @@ -883,7 +883,7 @@ export const MultiRunLauncher: React.FC = ({ onClick={() => handleRemoveFile(file.id)} className="text-muted-foreground hover:text-destructive" > - +
    ))} diff --git a/packages/ui/src/components/onboarding/ChooserScreen.tsx b/packages/ui/src/components/onboarding/ChooserScreen.tsx index 955e027c..ddf68cf8 100644 --- a/packages/ui/src/components/onboarding/ChooserScreen.tsx +++ b/packages/ui/src/components/onboarding/ChooserScreen.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { RiFileCopyLine, RiCheckLine, RiExternalLinkLine, RiArrowDownSLine } from '@remixicon/react'; import { isDesktopShell, isTauriShell, startDesktopWindowDrag } from '@/lib/desktop'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; +import { Icon } from "@/components/icon/Icon"; import { updateDesktopSettings } from '@/lib/persistence'; import { copyTextToClipboard } from '@/lib/clipboard'; import { restartDesktopApp } from '@/lib/desktop'; @@ -39,7 +39,7 @@ function BashCommand({ onCopy, copyTitle }: { onCopy: () => void; copyTitle: str title={copyTitle} aria-label={copyTitle} > - +
    ); @@ -306,7 +306,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
    {copied ? (
    - + {t('onboarding.common.status.copiedToClipboard')}
    ) : ( @@ -322,7 +322,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) { className="text-xs text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1" > {platform === 'windows' ? t('onboarding.localSetup.docs.windows') : t('onboarding.localSetup.docs.default')} - + )} @@ -126,7 +126,7 @@ export function DesktopConnectionRecovery({ disabled={isRetrying} className="flex-1" > - + {t(config.useRemoteLabelKey as Parameters[0])} )} diff --git a/packages/ui/src/components/onboarding/LocalSetupScreen.tsx b/packages/ui/src/components/onboarding/LocalSetupScreen.tsx index c2780389..f33ae588 100644 --- a/packages/ui/src/components/onboarding/LocalSetupScreen.tsx +++ b/packages/ui/src/components/onboarding/LocalSetupScreen.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { RiFileCopyLine, RiCheckLine, RiExternalLinkLine } from '@remixicon/react'; import { isDesktopShell, isTauriShell } from '@/lib/desktop'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; +import { Icon } from "@/components/icon/Icon"; import { updateDesktopSettings } from '@/lib/persistence'; import { copyTextToClipboard } from '@/lib/clipboard'; import { restartDesktopApp } from '@/lib/desktop'; @@ -40,7 +40,7 @@ function BashCommand({ onCopy, copyTitle }: { onCopy: () => void; copyTitle: str className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors" title={copyTitle} > - +
    ); @@ -264,7 +264,7 @@ export function LocalSetupScreen({
    {copied ? (
    - + {t('onboarding.common.status.copiedToClipboard')}
    ) : ( @@ -280,7 +280,7 @@ export function LocalSetupScreen({ className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1 justify-center" > {platform === 'windows' ? t('onboarding.localSetup.docs.windows') : t('onboarding.localSetup.docs.default')} - + {checkError && ( diff --git a/packages/ui/src/components/sections/SectionPlaceholder.tsx b/packages/ui/src/components/sections/SectionPlaceholder.tsx index 41d360a5..0cbeca1e 100644 --- a/packages/ui/src/components/sections/SectionPlaceholder.tsx +++ b/packages/ui/src/components/sections/SectionPlaceholder.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { SIDEBAR_SECTION_CONFIG_MAP, SIDEBAR_SECTION_DESCRIPTIONS } from '@/constants/sidebar'; import type { SidebarSection } from '@/constants/sidebar'; import { useI18n } from '@/lib/i18n'; +import { Icon } from "@/components/icon/Icon"; interface SectionPlaceholderProps { sectionId: SidebarSection; @@ -11,13 +12,13 @@ interface SectionPlaceholderProps { export const SectionPlaceholder: React.FC = ({ sectionId, variant }) => { const { t } = useI18n(); const config = SIDEBAR_SECTION_CONFIG_MAP[sectionId]; - const Icon = config.icon; + const icon = config.icon; if (variant === 'sidebar') { return (
    - +

    {config.label}

    @@ -30,7 +31,7 @@ export const SectionPlaceholder: React.FC = ({ sectionI return (

    - +

    {config.label}

    diff --git a/packages/ui/src/components/sections/agents/AgentsPage.tsx b/packages/ui/src/components/sections/agents/AgentsPage.tsx index 092270fb..99c55b48 100644 --- a/packages/ui/src/components/sections/agents/AgentsPage.tsx +++ b/packages/ui/src/components/sections/agents/AgentsPage.tsx @@ -10,7 +10,6 @@ import { useDirectorySync } from '@/sync/sync-context'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useDeviceInfo } from '@/lib/device'; import { opencodeClient } from '@/lib/opencode/client'; -import { RiCloseLine, RiInformationLine, RiRobot2Line, RiSubtractLine, RiUser3Line, RiFolderLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import { ModelSelector } from './ModelSelector'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; @@ -24,6 +23,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Icon } from "@/components/icon/Icon"; type PermissionAction = 'allow' | 'ask' | 'deny'; type PermissionRule = { permission: string; pattern: string; action: PermissionAction }; @@ -182,7 +182,6 @@ const buildPermissionConfigWithGlobal = ( return result as AgentConfig['permission']; }; - export const AgentsPage: React.FC = () => { const { t } = useI18n(); const { isMobile } = useDeviceInfo(); @@ -305,7 +304,6 @@ export const AgentsPage: React.FC = () => { currentRuleMap.get(buildRuleKey(permissionName, '*'))?.action ), [currentRuleMap]); - const getPatternRules = React.useCallback((permissionName: string): PermissionRule[] => ( permissionRules .filter((rule) => rule.permission === permissionName && rule.pattern !== '*') @@ -323,7 +321,6 @@ export const AgentsPage: React.FC = () => { return Array.from(names).sort((a, b) => a.localeCompare(b)); }, [knownPermissionNames]); - const getPermissionSummary = React.useCallback((permissionName: string) => { const defaultAction = permissionName === '*' ? globalPermission @@ -608,12 +605,11 @@ export const AgentsPage: React.FC = () => { } }; - if (!selectedAgentName) { return (
    - +

    {t('settings.agents.page.empty.title')}

    {t('settings.agents.page.empty.description')}

    @@ -669,13 +665,13 @@ export const AgentsPage: React.FC = () => {
    - + {t('settings.common.scope.global')}
    - + {t('settings.common.scope.project')}
    @@ -704,7 +700,7 @@ export const AgentsPage: React.FC = () => { {t('settings.agents.page.field.mode')} - + {t('settings.agents.page.field.modeTooltip')} @@ -781,7 +777,7 @@ export const AgentsPage: React.FC = () => { {t('settings.agents.page.field.temperature')} - + {t('settings.agents.page.field.temperatureTooltip')} @@ -813,7 +809,7 @@ export const AgentsPage: React.FC = () => { aria-label={t('settings.agents.page.field.clearTemperatureAria')} title={t('settings.common.actions.clear')} > - + )}
    @@ -825,7 +821,7 @@ export const AgentsPage: React.FC = () => { {t('settings.agents.page.field.topP')} - + {t('settings.agents.page.field.topPTooltip')} @@ -857,7 +853,7 @@ export const AgentsPage: React.FC = () => { aria-label={t('settings.agents.page.field.clearTopPAria')} title={t('settings.common.actions.clear')} > - + )}
    @@ -981,7 +977,7 @@ export const AgentsPage: React.FC = () => { onClick={() => revertRule(permissionName, '*')} className="px-1.5 py-0 h-5" > - + )}
    @@ -1028,7 +1024,7 @@ export const AgentsPage: React.FC = () => { onClick={() => isAdded ? removeRule(rule.permission, rule.pattern) : revertRule(rule.permission, rule.pattern)} className="px-1.5 py-0 h-5" > - + )}
    diff --git a/packages/ui/src/components/sections/agents/AgentsSidebar.tsx b/packages/ui/src/components/sections/agents/AgentsSidebar.tsx index 7517ac92..c15c7ad9 100644 --- a/packages/ui/src/components/sections/agents/AgentsSidebar.tsx +++ b/packages/ui/src/components/sections/agents/AgentsSidebar.tsx @@ -17,7 +17,6 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { RiAddLine, RiAiAgentFill, RiAiAgentLine, RiDeleteBinLine, RiFileCopyLine, RiMore2Line, RiRobot2Line, RiRobotLine, RiRestartLine, RiEditLine } from '@remixicon/react'; import { useAgentsStore, isAgentBuiltIn, isAgentHidden, type AgentScope, type AgentDraft } from '@/stores/useAgentsStore'; import { useShallow } from 'zustand/react/shallow'; import { cn } from '@/lib/utils'; @@ -25,6 +24,7 @@ import type { Agent } from '@opencode-ai/sdk/v2'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector'; import { SidebarGroup } from '@/components/sections/shared/SidebarGroup'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface AgentsSidebarProps { @@ -228,7 +228,6 @@ export const AgentsSidebar: React.FC = ({ onItemSelect }) => }); setSelectedAgent(newName); - }; const handleOpenRenameDialog = (agent: Agent) => { @@ -293,11 +292,11 @@ export const AgentsSidebar: React.FC = ({ onItemSelect }) => const getAgentModeIcon = (mode?: string) => { switch (mode) { case 'primary': - return ; + return ; case 'all': - return ; + return ; case 'subagent': - return ; + return ; default: return null; } @@ -339,7 +338,7 @@ export const AgentsSidebar: React.FC = ({ onItemSelect }) => className="h-7 w-7 px-0 -my-1 text-muted-foreground" onClick={handleCreateNew} > - +
    @@ -347,7 +346,7 @@ export const AgentsSidebar: React.FC = ({ onItemSelect }) => {visibleAgents.length === 0 ? (
    - +

    {t('settings.agents.sidebar.empty.title')}

    {t('settings.agents.sidebar.empty.description')}

    @@ -580,7 +579,7 @@ const AgentListItem: React.FC = ({ variant="ghost" className="h-6 w-6 px-0 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100" > - + @@ -591,7 +590,7 @@ const AgentListItem: React.FC = ({ onRename(); }} > - + {t('settings.common.actions.rename')} )} @@ -602,7 +601,7 @@ const AgentListItem: React.FC = ({ onDuplicate(); }} > - + {t('settings.common.actions.duplicate')} @@ -613,7 +612,7 @@ const AgentListItem: React.FC = ({ onReset(); }} > - + {t('settings.common.actions.reset')} )} @@ -626,7 +625,7 @@ const AgentListItem: React.FC = ({ }} className="text-destructive focus:text-destructive" > - + {t('settings.common.actions.delete')} )} diff --git a/packages/ui/src/components/sections/agents/ModelSelector.tsx b/packages/ui/src/components/sections/agents/ModelSelector.tsx index 4348056a..e0e61666 100644 --- a/packages/ui/src/components/sections/agents/ModelSelector.tsx +++ b/packages/ui/src/components/sections/agents/ModelSelector.tsx @@ -11,11 +11,11 @@ import { Input } from '@/components/ui/input'; import { useConfigStore } from '@/stores/useConfigStore'; import { useUIStore } from '@/stores/useUIStore'; import { useDeviceInfo } from '@/lib/device'; -import { RiArrowDownSLine, RiArrowRightSLine, RiCheckLine, RiCloseLine, RiLoader4Line, RiPencilAiLine, RiSearchLine, RiStarFill, RiStarLine, RiTimeLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { Icon } from "@/components/icon/Icon"; import { useModelLists } from '@/hooks/useModelLists'; import type { ModelMetadata } from '@/types'; import { useI18n } from '@/lib/i18n'; @@ -204,7 +204,7 @@ export const ModelSelector: React.FC = ({
    {isSelected && ( - + )}
    @@ -319,7 +319,7 @@ export const ModelSelector: React.FC = ({ className="model-favorite-button flex h-8 w-8 items-center justify-center text-primary hover:text-primary/80 active:scale-95 touch-manipulation" aria-label={t('settings.agents.modelSelector.actions.unfavorite')} > - + ); @@ -373,7 +373,7 @@ export const ModelSelector: React.FC = ({ className="model-favorite-button flex h-8 w-8 items-center justify-center text-muted-foreground/50 hover:text-primary/80 active:scale-95 touch-manipulation" aria-label={t('settings.agents.modelSelector.actions.favorite')} > - + ); @@ -409,9 +409,9 @@ export const ModelSelector: React.FC = ({ )} {isExpanded ? ( - + ) : ( - + )} @@ -458,9 +458,9 @@ export const ModelSelector: React.FC = ({ : t('settings.agents.modelSelector.actions.favorite')} > {isFavoriteModel(provider.id as string, modelItem.id as string) ? ( - + ) : ( - + )} @@ -508,7 +508,7 @@ export const ModelSelector: React.FC = ({
    {!isReady ? ( <> - + {isUnavailable ? t('common.unavailable') : t('common.loading')} ) : providerId ? ( @@ -517,7 +517,7 @@ export const ModelSelector: React.FC = ({ className="h-3.5 w-3.5" /> ) : ( - + )} {isReady && ( @@ -525,7 +525,7 @@ export const ModelSelector: React.FC = ({ )}
    - + ) : ( @@ -536,7 +536,7 @@ export const ModelSelector: React.FC = ({ )}> {!isReady ? ( <> - + {isUnavailable ? t('common.unavailable') : t('common.loading')} @@ -549,17 +549,17 @@ export const ModelSelector: React.FC = ({ providerId={providerId} className="h-3.5 w-3.5 flex-shrink-0" /> - + ) : ( - + )} {providerId && modelId ? `${providerId}/${modelId}` : (placeholder || t('settings.agents.modelSelector.notSelected'))} )} - + @@ -619,7 +619,7 @@ export const ModelSelector: React.FC = ({ {/* Search Input */}
    - + = ({ )} onSelect={() => handleProviderAndModelChange('', '')} > - + {placeholder || t('settings.agents.modelSelector.notSelected')} {!providerId && !modelId && ( - + )} @@ -661,7 +661,7 @@ export const ModelSelector: React.FC = ({ {filteredFavorites.length > 0 && (
    - + {t('settings.agents.modelSelector.section.favorites')} {filteredFavorites.map(({ model, providerID, modelID }) => { @@ -676,7 +676,7 @@ export const ModelSelector: React.FC = ({
    {filteredFavorites.length > 0 && } - + {t('settings.agents.modelSelector.section.recent')} {filteredRecents.map(({ model, providerID, modelID }) => { diff --git a/packages/ui/src/components/sections/behavior/BehaviorPage.tsx b/packages/ui/src/components/sections/behavior/BehaviorPage.tsx index a2bfff92..2db8d88b 100644 --- a/packages/ui/src/components/sections/behavior/BehaviorPage.tsx +++ b/packages/ui/src/components/sections/behavior/BehaviorPage.tsx @@ -13,7 +13,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { RiInformationLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { getResponseStylePresetInstructions, isResponseStylePreset, @@ -250,7 +250,7 @@ export const BehaviorPage: React.FC = () => { - +
    @@ -295,7 +295,7 @@ export const BehaviorPage: React.FC = () => { - + {t('settings.behavior.page.responseStyle.tooltip')} diff --git a/packages/ui/src/components/sections/commands/AgentSelector.tsx b/packages/ui/src/components/sections/commands/AgentSelector.tsx index 8e84a005..93cf7a7e 100644 --- a/packages/ui/src/components/sections/commands/AgentSelector.tsx +++ b/packages/ui/src/components/sections/commands/AgentSelector.tsx @@ -10,9 +10,9 @@ import { useAgentsStore, filterVisibleAgents } from '@/stores/useAgentsStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useUIStore } from '@/stores/useUIStore'; import { useDeviceInfo } from '@/lib/device'; -import { RiArrowDownSLine, RiLoader4Line, RiRobot2Line } from '@remixicon/react'; import { cn } from '@/lib/utils'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; import { useOpenCodeReadiness } from '@/hooks/useOpenCodeReadiness'; @@ -138,26 +138,26 @@ export const AgentSelector: React.FC = ({
    {!isReady ? ( <> - + {isUnavailable ? t('common.unavailable') : t('common.loading')} ) : ( <> - + {agentName || t('settings.commands.agentSelector.selectAgentPlaceholder')} )}
    - + ) : !isReady ? (
    - + {isUnavailable ? t('common.unavailable') : t('common.loading')} @@ -169,11 +169,11 @@ export const AgentSelector: React.FC = ({ 'flex items-center gap-2 px-2 rounded-lg bg-interactive-selection/20 border border-border/20 cursor-pointer hover:bg-interactive-hover/30 h-6 w-fit', className )}> - + {agentName || t('settings.commands.agentSelector.notSelected')} - +
    diff --git a/packages/ui/src/components/sections/commands/CommandsPage.tsx b/packages/ui/src/components/sections/commands/CommandsPage.tsx index a87cdc3a..4be6a3bf 100644 --- a/packages/ui/src/components/sections/commands/CommandsPage.tsx +++ b/packages/ui/src/components/sections/commands/CommandsPage.tsx @@ -5,7 +5,6 @@ import { Textarea } from '@/components/ui/textarea'; import { toast } from '@/components/ui'; import { useCommandsStore, type CommandConfig, type CommandScope } from '@/stores/useCommandsStore'; import { useShallow } from 'zustand/react/shallow'; -import { RiTerminalBoxLine, RiUser3Line, RiFolderLine } from '@remixicon/react'; import { ModelSelector } from '../agents/ModelSelector'; import { AgentSelector } from './AgentSelector'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; @@ -16,6 +15,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; import { parseModelIdentifier } from '@/lib/modelIdentifier'; @@ -180,7 +180,7 @@ export const CommandsPage: React.FC = () => { return (
    - +

    {t('settings.commands.page.empty.title')}

    {t('settings.commands.page.empty.description')}

    @@ -236,13 +236,13 @@ export const CommandsPage: React.FC = () => {
    - + {t('settings.common.scope.global')}
    - + {t('settings.common.scope.project')}
    diff --git a/packages/ui/src/components/sections/commands/CommandsSidebar.tsx b/packages/ui/src/components/sections/commands/CommandsSidebar.tsx index f6860de4..8ea75914 100644 --- a/packages/ui/src/components/sections/commands/CommandsSidebar.tsx +++ b/packages/ui/src/components/sections/commands/CommandsSidebar.tsx @@ -17,13 +17,13 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { RiAddLine, RiTerminalBoxLine, RiMore2Line, RiDeleteBinLine, RiFileCopyLine, RiRestartLine, RiEditLine } from '@remixicon/react'; import { useCommandsStore, isCommandBuiltIn, type Command } from '@/stores/useCommandsStore'; import { useSkillsStore } from '@/stores/useSkillsStore'; import { useShallow } from 'zustand/react/shallow'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { cn } from '@/lib/utils'; import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface CommandsSidebarProps { @@ -97,7 +97,6 @@ export const CommandsSidebar: React.FC = ({ onItemSelect } setSelectedCommand(newName); onItemSelect?.(); - }; const handleDeleteCommand = async (command: Command) => { @@ -169,7 +168,6 @@ export const CommandsSidebar: React.FC = ({ onItemSelect } }); setSelectedCommand(newName); - }; const handleOpenRenameDialog = (command: Command) => { @@ -237,7 +235,7 @@ export const CommandsSidebar: React.FC = ({ onItemSelect } className="h-7 w-7 px-0 -my-1 text-muted-foreground" onClick={handleCreateNew} > - +
    @@ -245,7 +243,7 @@ export const CommandsSidebar: React.FC = ({ onItemSelect } {commandOnlyItems.length === 0 ? (
    - +

    {t('settings.commands.sidebar.empty.title')}

    {t('settings.commands.sidebar.empty.description')}

    @@ -440,7 +438,7 @@ const CommandListItem: React.FC = ({ variant="ghost" className="h-6 w-6 px-0 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100" > - + @@ -451,7 +449,7 @@ const CommandListItem: React.FC = ({ onRename(); }} > - + {t('settings.common.actions.rename')} )} @@ -462,7 +460,7 @@ const CommandListItem: React.FC = ({ onDuplicate(); }} > - + {t('settings.common.actions.duplicate')} @@ -473,7 +471,7 @@ const CommandListItem: React.FC = ({ onReset(); }} > - + {t('settings.common.actions.reset')} )} @@ -486,7 +484,7 @@ const CommandListItem: React.FC = ({ }} className="text-destructive focus:text-destructive" > - + {t('settings.common.actions.delete')} )} diff --git a/packages/ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx b/packages/ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx index e085faf2..70d64cde 100644 --- a/packages/ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx +++ b/packages/ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx @@ -11,18 +11,8 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { useGitIdentitiesStore, type GitIdentityProfile, type GitIdentityAuthType } from '@/stores/useGitIdentitiesStore'; -import { - RiDeleteBinLine, - RiGitBranchLine, - RiBriefcaseLine, - RiHomeLine, - RiGraduationCapLine, - RiCodeLine, - RiInformationLine, - RiKeyLine, - RiLock2Line, -} from '@remixicon/react'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; @@ -35,11 +25,11 @@ const PROFILE_COLORS = [ ]; const PROFILE_ICONS = [ - { key: 'branch', Icon: RiGitBranchLine, label: 'Branch' }, - { key: 'briefcase', Icon: RiBriefcaseLine, label: 'Work' }, - { key: 'house', Icon: RiHomeLine, label: 'Personal' }, - { key: 'graduation', Icon: RiGraduationCapLine, label: 'School' }, - { key: 'code', Icon: RiCodeLine, label: 'Code' }, + { key: 'branch', Icon: 'git-branch', label: 'Branch' }, + { key: 'briefcase', Icon: 'briefcase', label: 'Work' }, + { key: 'house', Icon: 'home', label: 'Personal' }, + { key: 'graduation', Icon: 'graduation-cap', label: 'School' }, + { key: 'code', Icon: 'code', label: 'Code' }, ]; interface GitIdentityEditorDialogProps { @@ -261,7 +251,7 @@ export const GitIdentityEditorDialog: React.FC = ( {t('settings.gitIdentities.editor.field.icon')}
    {PROFILE_ICONS.map((i) => { - const IconComponent = i.Icon; + const iconName = i.Icon; return (
    @@ -375,7 +365,7 @@ export const GitIdentityEditorDialog: React.FC = ( - + {t('settings.gitIdentities.editor.field.sshKeyPathTooltip')} @@ -398,7 +388,7 @@ export const GitIdentityEditorDialog: React.FC = ( * - + {t('settings.gitIdentities.editor.field.hostTooltip')} @@ -427,7 +417,7 @@ export const GitIdentityEditorDialog: React.FC = ( onClick={() => setIsDeleteDialogOpen(true)} className="text-[var(--status-error)] hover:text-[var(--status-error)] border-[var(--status-error)]/30 hover:bg-[var(--status-error)]/10 mr-auto" > - {t('settings.common.actions.delete')} + {t('settings.common.actions.delete')} )}
    @@ -170,7 +158,7 @@ export const GitPage: React.FC = () => { {/* Empty state */} {!globalIdentity && profiles.length === 0 && unimportedCredentials.length === 0 && (
    - +

    {t('settings.gitIdentities.page.empty.title')}

    {t('settings.gitIdentities.page.empty.description')}

    @@ -257,7 +245,7 @@ const IdentityRow: React.FC = ({ hasBorder, }) => { const { t } = useI18n(); - const IconComponent = ICON_MAP[profile.icon || 'branch'] || RiGitBranchLine; + const iconName = ICON_MAP[profile.icon || 'branch'] || 'git-branch'; const iconColor = COLOR_MAP[profile.color || '']; const authType = profile.authType || 'ssh'; @@ -273,7 +261,7 @@ const IdentityRow: React.FC = ({ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onEdit(); }} >
    - +
    {profile.name} @@ -305,7 +293,7 @@ const IdentityRow: React.FC = ({ className="h-6 w-6 shrink-0 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100" onClick={(e) => e.stopPropagation()} > - + @@ -317,7 +305,7 @@ const IdentityRow: React.FC = ({ onClick={(e) => { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive" > - + {t('settings.common.actions.delete')} )} @@ -355,7 +343,7 @@ const DiscoveredRow: React.FC = ({ credential, onImport, has
    diff --git a/packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx b/packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx index 967af3e6..9e66ce24 100644 --- a/packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx +++ b/packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { toast } from '@/components/ui'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { RiInformationLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { fetchMagicPromptOverrides, getDefaultMagicPromptTemplate, @@ -269,7 +269,7 @@ export const MagicPromptsPage: React.FC = () => {

    {tUnsafe(pageConfig.titleKey)}

    - + {tUnsafe(pageConfig.descriptionKey)} @@ -306,7 +306,7 @@ export const MagicPromptsPage: React.FC = () => {

    {tUnsafe(block.titleKey)}

    - + {definition.description} diff --git a/packages/ui/src/components/sections/mcp/McpPage.tsx b/packages/ui/src/components/sections/mcp/McpPage.tsx index a273d68a..404c9fd6 100644 --- a/packages/ui/src/components/sections/mcp/McpPage.tsx +++ b/packages/ui/src/components/sections/mcp/McpPage.tsx @@ -20,20 +20,6 @@ import { } from './mcpImport'; import { useMcpStore } from '@/stores/useMcpStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; -import { - RiAddLine, - RiArrowDownSLine, - RiArrowRightSLine, - RiClipboardLine, - RiDeleteBinLine, - RiEyeLine, - RiEyeOffLine, - RiExternalLinkLine, - RiFileCodeLine, - RiFolderLine, - RiPlugLine, - RiUser3Line, -} from '@remixicon/react'; import { cn } from '@/lib/utils'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { MCP_OAUTH_CALLBACK_PATH, parseMcpOAuthCallbackContext, parseMcpOAuthCallbackStateKey } from '@/components/sections/mcp/mcpOAuth'; @@ -52,6 +38,7 @@ import { SelectItem, SelectTrigger, } from '@/components/ui/select'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; // ───────────────────────────────────────────────────────────── @@ -175,7 +162,7 @@ const CommandTextarea: React.FC = ({ type="button" title={pasteCommandTitle} > - + {pasteCommandLabel}
    @@ -354,7 +341,7 @@ const EnvEditor: React.FC = ({ type="button" title={pasteTitle} > - + {pasteLabel}
    @@ -395,8 +382,8 @@ const EnvEditor: React.FC = ({ title={revealedKeys.has(idx) ? hideValueTitle : showValueTitle} > {revealedKeys.has(idx) - ? - : } + ? + : } {/* Remove */} @@ -406,7 +393,7 @@ const EnvEditor: React.FC = ({ onClick={() => removeRow(idx)} aria-label={removeVariableAria} > - + ))} @@ -419,7 +406,7 @@ const EnvEditor: React.FC = ({ onClick={addRow} type="button" > - + {addVariable} @@ -1299,7 +1286,7 @@ export const McpPage: React.FC = () => { return (
    - +

    {t('settings.mcp.page.empty.selectServer')}

    {t('settings.mcp.page.empty.addNewOne')}

    @@ -1447,11 +1434,11 @@ export const McpPage: React.FC = () => {
    {authUrl}
    @@ -1526,18 +1513,18 @@ export const McpPage: React.FC = () => { /> )} @@ -638,7 +638,7 @@ export const VoiceSettings: React.FC = () => { )} @@ -657,7 +657,7 @@ export const VoiceSettings: React.FC = () => { )} @@ -734,7 +734,7 @@ export const VoiceSettings: React.FC = () => { {t('settings.voice.page.field.provider')} - +
      @@ -795,7 +795,7 @@ export const VoiceSettings: React.FC = () => { onClick={() => setSttServerUrl('')} className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" > - + )}
    diff --git a/packages/ui/src/components/sections/openchamber/WorktreeSectionContent.tsx b/packages/ui/src/components/sections/openchamber/WorktreeSectionContent.tsx index 105c9091..355d3726 100644 --- a/packages/ui/src/components/sections/openchamber/WorktreeSectionContent.tsx +++ b/packages/ui/src/components/sections/openchamber/WorktreeSectionContent.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { RiAddLine, RiCloseLine, RiDeleteBinLine, RiInformationLine } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessions } from '@/sync/sync-context'; @@ -241,8 +241,6 @@ export const WorktreeSectionContent: React.FC = ({ }); }, [sessions, getWorktreeMetadata]); - - // Refresh worktrees when sessions change (after deletion) const sessionsKey = React.useMemo(() => sessions.map(s => s.id).join(','), [sessions]); React.useEffect(() => { @@ -276,7 +274,7 @@ export const WorktreeSectionContent: React.FC = ({

    {t('settings.openchamber.worktrees.setup.title')}

    - + {t('settings.openchamber.worktrees.setup.tooltipPrefix')} @@ -310,7 +308,7 @@ export const WorktreeSectionContent: React.FC = ({ className="flex-shrink-0 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50" aria-label={t('settings.openchamber.worktrees.setup.removeCommandAria')} > - + ))} @@ -321,7 +319,7 @@ export const WorktreeSectionContent: React.FC = ({ className="!font-normal" onClick={handleAddCommand} > - + {t('settings.openchamber.worktrees.setup.addCommand')} @@ -335,7 +333,7 @@ export const WorktreeSectionContent: React.FC = ({

    {t('settings.openchamber.worktrees.list.title')}

    - + {t('settings.openchamber.worktrees.list.tooltip')} @@ -379,7 +377,7 @@ export const WorktreeSectionContent: React.FC = ({ )} aria-label={t('settings.openchamber.worktrees.list.deleteWorktreeAria', { name: worktree.branch || worktree.label || worktree.path })} > - + ))} diff --git a/packages/ui/src/components/sections/projects/ProjectActionsSection.tsx b/packages/ui/src/components/sections/projects/ProjectActionsSection.tsx index e69f9f60..96643562 100644 --- a/packages/ui/src/components/sections/projects/ProjectActionsSection.tsx +++ b/packages/ui/src/components/sections/projects/ProjectActionsSection.tsx @@ -1,12 +1,4 @@ import React from 'react'; -import { - RiAddLine, - RiArrowDownSLine, - RiArrowRightSLine, - RiDeleteBinLine, - RiInformationLine, - RiPlayLine, -} from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { @@ -30,6 +22,7 @@ import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { useDesktopSshStore } from '@/stores/useDesktopSshStore'; import { isDesktopShell } from '@/lib/desktop'; import { @@ -201,7 +194,7 @@ export const ProjectActionsSection: React.FC = ({ pr

    {t('settings.projects.actions.description')}

    @@ -217,7 +210,7 @@ export const ProjectActionsSection: React.FC = ({ pr
    {actions.map((action) => { const selectedIconKey = (action.icon as keyof typeof PROJECT_ACTION_ICON_MAP) || 'play'; - const SelectedIcon = PROJECT_ACTION_ICON_MAP[selectedIconKey] || RiPlayLine; + const selectedIconName = PROJECT_ACTION_ICON_MAP[selectedIconKey] || 'play'; const isOpen = expandedActions[action.id] ?? false; const title = action.name.trim() || t('settings.projects.actions.state.untitled'); @@ -238,11 +231,11 @@ export const ProjectActionsSection: React.FC = ({ pr
    {isOpen ? ( - + ) : ( - + )} - +
    {title} @@ -257,7 +250,7 @@ export const ProjectActionsSection: React.FC = ({ pr className="!font-normal h-7 w-7 px-0 text-muted-foreground hover:text-[var(--status-error)]" onClick={() => handleRemoveAction(action.id)} > - +
    @@ -271,13 +264,13 @@ export const ProjectActionsSection: React.FC = ({ pr className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-[var(--interactive-border)] text-foreground hover:bg-[var(--interactive-hover)]" aria-label={t('settings.projects.actions.field.selectIconAria')} > - +
    {PROJECT_ACTION_ICONS.map((entry) => { - const Icon = entry.Icon; + const iconName = entry.Icon; const selected = (action.icon || 'play') === entry.key; return ( ); })} @@ -364,7 +357,7 @@ export const ProjectActionsSection: React.FC = ({ pr /> - + {t('settings.projects.actions.field.overrideUrlTooltip')} diff --git a/packages/ui/src/components/sections/projects/ProjectsPage.tsx b/packages/ui/src/components/sections/projects/ProjectsPage.tsx index c8d9584c..ec58255e 100644 --- a/packages/ui/src/components/sections/projects/ProjectsPage.tsx +++ b/packages/ui/src/components/sections/projects/ProjectsPage.tsx @@ -7,9 +7,9 @@ import { cn } from '@/lib/utils'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useUIStore } from '@/stores/useUIStore'; import { PROJECT_COLORS, PROJECT_ICONS, PROJECT_COLOR_MAP as COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta'; -import { RiCloseLine } from '@remixicon/react'; import { WorktreeSectionContent } from '@/components/sections/openchamber/WorktreeSectionContent'; import { ProjectActionsSection } from '@/components/sections/projects/ProjectActionsSection'; +import { Icon } from "@/components/icon/Icon"; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useI18n } from '@/lib/i18n'; @@ -298,7 +298,7 @@ export const ProjectsPage: React.FC = () => { )} title={t('settings.projects.page.field.none')} > - + {PROJECT_COLORS.map((c) => ( {PROJECT_ICONS.map((i) => { - const IconComponent = i.Icon; + const iconName = i.Icon; return ( ); })} @@ -412,7 +412,7 @@ export const ProjectsPage: React.FC = () => { title={t('settings.projects.page.field.clearBackground')} disabled={!iconBackground} > - +
    )} diff --git a/packages/ui/src/components/sections/projects/ProjectsSidebar.tsx b/packages/ui/src/components/sections/projects/ProjectsSidebar.tsx index 35126443..0c0d0b49 100644 --- a/packages/ui/src/components/sections/projects/ProjectsSidebar.tsx +++ b/packages/ui/src/components/sections/projects/ProjectsSidebar.tsx @@ -4,9 +4,9 @@ import { useUIStore } from '@/stores/useUIStore'; import { Button } from '@/components/ui/button'; import { SettingsSidebarLayout } from '@/components/sections/shared/SettingsSidebarLayout'; import { SettingsSidebarItem } from '@/components/sections/shared/SettingsSidebarItem'; +import { Icon } from "@/components/icon/Icon"; import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, getProjectIconImageUrl } from '@/lib/projectMeta'; import { cn } from '@/lib/utils'; -import { RiAddLine, RiFolderLine } from '@remixicon/react'; import { isVSCodeRuntime } from '@/lib/desktop'; import { sessionEvents } from '@/lib/sessionEvents'; import { useThemeSystem } from '@/contexts/useThemeSystem'; @@ -56,7 +56,7 @@ export const ProjectsSidebar: React.FC<{ onItemSelect?: () => void }> = ({ onIte onClick={handleAddProject} aria-label={t('settings.projects.sidebar.actions.addProject')} > - + )}
    @@ -65,7 +65,7 @@ export const ProjectsSidebar: React.FC<{ onItemSelect?: () => void }> = ({ onIte > {projects.map((project) => { const selected = project.id === selectedId; - const Icon = project.icon ? PROJECT_ICON_MAP[project.icon] : null; + const iconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null; const imageFailureKey = `${project.id}:${project.iconImage?.updatedAt ?? 0}`; const imageUrl = brokenIconIds.has(imageFailureKey) ? null @@ -98,12 +98,12 @@ export const ProjectsSidebar: React.FC<{ onItemSelect?: () => void }> = ({ onIte /> ) - : Icon + : iconName ? ( - + ) : ( - + ); return ( diff --git a/packages/ui/src/components/sections/providers/ProvidersPage.tsx b/packages/ui/src/components/sections/providers/ProvidersPage.tsx index b42e9388..2a56dc98 100644 --- a/packages/ui/src/components/sections/providers/ProvidersPage.tsx +++ b/packages/ui/src/components/sections/providers/ProvidersPage.tsx @@ -12,8 +12,8 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { toast } from '@/components/ui'; -import { RiStackLine, RiToolsLine, RiBrainAi3Line, RiFileImageLine, RiArrowDownSLine, RiCheckLine, RiSearchLine, RiInformationLine, RiEyeLine, RiEyeOffLine } from '@remixicon/react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore'; import { cn } from '@/lib/utils'; import { copyTextToClipboard } from '@/lib/clipboard'; @@ -511,7 +511,7 @@ export const ProvidersPage: React.FC = () => { return (
    - +

    {t('settings.providers.page.empty.noProvidersDetected')}

    {t('settings.providers.page.empty.checkOpenCodeConfiguration')}

    @@ -561,7 +561,7 @@ export const ProvidersPage: React.FC = () => { : t('settings.providers.page.connect.selectProviderPlaceholder')} - + { className="flex items-center gap-2 border-b border-[var(--surface-subtle)] px-3 py-2" onKeyDown={(e) => e.stopPropagation()} > - + { {provider.name || provider.id} {candidateProviderId === provider.id && ( - + )} )); @@ -636,7 +636,7 @@ export const ProvidersPage: React.FC = () => { {t('settings.providers.page.auth.apiKeyLabel')} - + {t('settings.providers.page.auth.apiKeyTooltip')} @@ -772,7 +772,7 @@ export const ProvidersPage: React.FC = () => { return (
    - +

    {t('settings.providers.page.empty.selectProviderFromSidebar')}

    {t('settings.providers.page.empty.reviewDetailsAndConfigureAuth')}

    @@ -826,7 +826,7 @@ export const ProvidersPage: React.FC = () => {
    {!showAuthPanel ? (
    - + {t('settings.providers.page.auth.connected')} {t('settings.providers.page.auth.useReconnectHint')}
    @@ -839,7 +839,7 @@ export const ProvidersPage: React.FC = () => { {t('settings.providers.page.auth.apiKeyLabel')} - + {t('settings.providers.page.auth.apiKeyTooltip')} @@ -1031,7 +1031,7 @@ export const ProvidersPage: React.FC = () => {
    - + setModelQuery(event.target.value)} @@ -1055,10 +1055,10 @@ export const ProvidersPage: React.FC = () => { const contextTokens = formatTokens(metadata?.limit?.context); const outputTokens = formatTokens(metadata?.limit?.output); - const capabilityIcons: Array<{ key: string; icon: typeof RiToolsLine; label: string }> = []; - if (metadata?.tool_call) capabilityIcons.push({ key: 'tools', icon: RiToolsLine, label: t('settings.providers.page.models.capability.toolCalling') }); - if (metadata?.reasoning) capabilityIcons.push({ key: 'reasoning', icon: RiBrainAi3Line, label: t('settings.providers.page.models.capability.reasoning') }); - if (metadata?.attachment) capabilityIcons.push({ key: 'image', icon: RiFileImageLine, label: t('settings.providers.page.models.capability.imageInput') }); + const capabilityIcons: Array<{ key: string; icon: string; label: string }> = []; + if (metadata?.tool_call) capabilityIcons.push({ key: 'tools', icon: "tools", label: t('settings.providers.page.models.capability.toolCalling') }); + if (metadata?.reasoning) capabilityIcons.push({ key: 'reasoning', icon: "brain-ai-3", label: t('settings.providers.page.models.capability.reasoning') }); + if (metadata?.attachment) capabilityIcons.push({ key: 'image', icon: "file-image", label: t('settings.providers.page.models.capability.imageInput') }); return (
    @@ -1081,14 +1081,14 @@ export const ProvidersPage: React.FC = () => { )} {capabilityIcons.length > 0 && (
    - {capabilityIcons.map(({ key, icon: Icon, label }) => ( + {capabilityIcons.map(({ key, icon: iconName, label }) => ( - + ))}
    @@ -1100,7 +1100,7 @@ export const ProvidersPage: React.FC = () => { title={isHidden ? t('settings.providers.page.models.actions.showModelInSelectors') : t('settings.providers.page.models.actions.hideModelFromSelectors')} aria-label={isHidden ? t('settings.providers.page.models.actions.showModel') : t('settings.providers.page.models.actions.hideModel')} > - {isHidden ? : } + {isHidden ? : }
    diff --git a/packages/ui/src/components/sections/providers/ProvidersSidebar.tsx b/packages/ui/src/components/sections/providers/ProvidersSidebar.tsx index 219b3a34..59957c7b 100644 --- a/packages/ui/src/components/sections/providers/ProvidersSidebar.tsx +++ b/packages/ui/src/components/sections/providers/ProvidersSidebar.tsx @@ -4,9 +4,9 @@ import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { Button } from '@/components/ui/button'; import { useConfigStore } from '@/stores/useConfigStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; -import { RiAddLine, RiStackLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector'; +import { Icon } from "@/components/icon/Icon"; import { opencodeClient } from '@/lib/opencode/client'; import { useI18n } from '@/lib/i18n'; @@ -122,7 +122,7 @@ export const ProvidersSidebar: React.FC = ({ onItemSelect aria-label={t('settings.providers.sidebar.actions.connectProviderAria')} title={t('settings.providers.sidebar.actions.connectProviderTitle')} > - +
    @@ -130,7 +130,7 @@ export const ProvidersSidebar: React.FC = ({ onItemSelect {providers.length === 0 ? (
    - +

    {t('settings.providers.sidebar.empty.title')}

    {t('settings.providers.sidebar.empty.description')}

    diff --git a/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx b/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx index ae0c544b..6a6461d9 100644 --- a/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx +++ b/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx @@ -10,22 +10,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - RiAddLine, - RiArrowDownSLine, - RiArrowRightLine, - RiComputerLine, - RiExternalLinkLine, - RiFileCopyLine, - RiInformationLine, - RiPlug2Line, - RiRefreshLine, - RiServerLine, - RiShuffleLine, - RiTerminalWindowLine, - RiDeleteBinLine, - RiStopLine, -} from '@remixicon/react'; import { Dialog, DialogContent, @@ -39,6 +23,7 @@ import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLay import { useDesktopSshStore } from '@/stores/useDesktopSshStore'; import { useUIStore } from '@/stores/useUIStore'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { copyTextToClipboard } from '@/lib/clipboard'; import { openExternalUrl } from '@/lib/url'; import { useI18n, type I18nKey } from '@/lib/i18n'; @@ -152,7 +137,7 @@ const HintLabel: React.FC<{ label: string; hint: React.ReactNode }> = ({ label, {label} - +
    {hint}
    @@ -808,7 +793,7 @@ export const RemoteInstancesPage: React.FC = () => { onClick={handlePrimaryConnectionAction} disabled={isPrimaryActionPending || isRetryPending} > - {canDisconnect ? : } + {canDisconnect ? : } {primaryButtonLabel}
    @@ -1148,7 +1133,7 @@ export const RemoteInstancesPage: React.FC = () => { })) } > - +
    @@ -1265,7 +1250,7 @@ export const RemoteInstancesPage: React.FC = () => {
    - + {buildForwardLabel(forward)} {typeLabel} @@ -1284,7 +1269,7 @@ export const RemoteInstancesPage: React.FC = () => { })) } > - +
    @@ -1406,27 +1391,27 @@ export const RemoteInstancesPage: React.FC = () => {
    {forward.type === 'dynamic' ? ( <> - + {localEndpoint} {t('settings.remoteInstances.page.preview.localSocks5')} ) : forward.type === 'remote' ? ( <> - + {remoteEndpoint} {t('settings.remoteInstances.page.preview.remote')} - - + + {localEndpoint} {t('settings.remoteInstances.page.preview.local')} ) : ( <> - + {localEndpoint} {t('settings.remoteInstances.page.preview.local')} - - + + {remoteEndpoint} {t('settings.remoteInstances.page.preview.remote')} @@ -1447,7 +1432,7 @@ export const RemoteInstancesPage: React.FC = () => { }); }} > - + {t('settings.remoteInstances.page.actions.openLocal')} ) : null} @@ -1475,7 +1460,7 @@ export const RemoteInstancesPage: React.FC = () => { })); }} > - + {t('settings.remoteInstances.page.actions.addForward')}
@@ -1540,7 +1525,7 @@ export const RemoteInstancesPage: React.FC = () => { }); }} > - + {t('settings.remoteInstances.page.actions.copyLocalUrl')} @@ -1571,11 +1556,11 @@ export const RemoteInstancesPage: React.FC = () => {
diff --git a/packages/ui/src/components/sections/remote-instances/RemoteInstancesSidebar.tsx b/packages/ui/src/components/sections/remote-instances/RemoteInstancesSidebar.tsx index f0d56766..5abf1034 100644 --- a/packages/ui/src/components/sections/remote-instances/RemoteInstancesSidebar.tsx +++ b/packages/ui/src/components/sections/remote-instances/RemoteInstancesSidebar.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { RiAddLine, RiDeleteBinLine, RiPlug2Line, RiRefreshLine, RiStopLine } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { SettingsSidebarLayout } from '@/components/sections/shared/SettingsSidebarLayout'; import { SettingsSidebarItem } from '@/components/sections/shared/SettingsSidebarItem'; import { useDesktopSshStore } from '@/stores/useDesktopSshStore'; import { useUIStore } from '@/stores/useUIStore'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import type { DesktopSshInstance } from '@/lib/desktopSsh'; import { useI18n } from '@/lib/i18n'; @@ -145,7 +145,7 @@ export const RemoteInstancesSidebar: React.FC = ({ onClick={() => void handleAdd()} aria-label={t('settings.remoteInstances.sidebar.actions.addSshInstance')} > - + @@ -172,7 +172,7 @@ export const RemoteInstancesSidebar: React.FC = ({ actions={[ { label: isReady ? t('settings.remoteInstances.sidebar.actions.disconnect') : t('settings.remoteInstances.sidebar.actions.connect'), - icon: isReady ? RiStopLine : RiPlug2Line, + icon: isReady ? 'stop' : 'plug-2', onClick: () => { const op = isReady ? disconnect(instance.id) : connectWithPortRecovery(instance); void op.catch((error) => { @@ -189,7 +189,7 @@ export const RemoteInstancesSidebar: React.FC = ({ }, { label: t('settings.remoteInstances.sidebar.actions.retry'), - icon: RiRefreshLine, + icon: "refresh", onClick: () => { if (!canRetry) return; void retry(instance.id).catch((error) => { @@ -201,7 +201,7 @@ export const RemoteInstancesSidebar: React.FC = ({ }, { label: t('settings.remoteInstances.sidebar.actions.remove'), - icon: RiDeleteBinLine, + icon: "delete-bin", destructive: true, onClick: () => { void removeInstance(instance.id).then(() => { diff --git a/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx b/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx index 22c98a16..709d5c50 100644 --- a/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx +++ b/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx @@ -6,7 +6,7 @@ import { DropdownMenuRadioItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { RiArrowDownSLine, RiFolderLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { useProjectsStore } from '@/stores/useProjectsStore'; import { isVSCodeRuntime } from '@/lib/desktop'; import { cn } from '@/lib/utils'; @@ -59,9 +59,9 @@ export const SettingsProjectSelector: React.FC<{ className?: string }> = ({ clas 'flex items-center gap-1.5 text-left' )} > - + {label} - + diff --git a/packages/ui/src/components/sections/shared/SettingsSidebarHeader.tsx b/packages/ui/src/components/sections/shared/SettingsSidebarHeader.tsx index 5092ff49..bc4fee1b 100644 --- a/packages/ui/src/components/sections/shared/SettingsSidebarHeader.tsx +++ b/packages/ui/src/components/sections/shared/SettingsSidebarHeader.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from '@/components/ui/button'; -import { RiAddLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { useDeviceInfo } from '@/lib/device'; import { cn } from '@/lib/utils'; @@ -54,7 +54,7 @@ export const SettingsSidebarHeader: React.FC = ({ onClick={onAdd} aria-label={addButtonLabel} > - + )} diff --git a/packages/ui/src/components/sections/shared/SettingsSidebarItem.tsx b/packages/ui/src/components/sections/shared/SettingsSidebarItem.tsx index 433a029d..358c16d5 100644 --- a/packages/ui/src/components/sections/shared/SettingsSidebarItem.tsx +++ b/packages/ui/src/components/sections/shared/SettingsSidebarItem.tsx @@ -6,14 +6,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { RiMore2Line } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; export interface SettingsSidebarItemAction { /** Label shown in dropdown menu */ label: string; /** Icon component to show before label */ - icon?: React.ComponentType<{ className?: string }>; + icon?: string; /** Callback when action is clicked */ onClick: () => void; /** If true, uses destructive styling (red text) */ @@ -47,10 +47,10 @@ interface SettingsSidebarItemProps { * metadata={agent.description} * selected={selectedId === agent.id} * onSelect={() => setSelectedId(agent.id)} - * icon={} + * icon={<'robot' className="h-4 w-4" />} * actions={[ - * { label: 'Duplicate', icon: RiFileCopyLine, onClick: handleDuplicate }, - * { label: 'Delete', icon: RiDeleteBinLine, onClick: handleDelete, destructive: true }, + * { label: 'Duplicate', icon: 'file-copy', onClick: handleDuplicate }, + * { label: 'Delete', icon: 'delete-bin', onClick: handleDelete, destructive: true }, * ]} * /> */ @@ -103,12 +103,12 @@ export const SettingsSidebarItem: React.FC = ({ variant="ghost" className="h-6 w-6 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100" > - + {actions.map((action) => { - const Icon = action.icon; + const iconName = action.icon; return ( = ({ action.destructive && 'text-destructive focus:text-destructive' )} > - {Icon && } + {iconName && } {action.label} ); diff --git a/packages/ui/src/components/sections/shared/SidebarGroup.tsx b/packages/ui/src/components/sections/shared/SidebarGroup.tsx index 1d14f7db..74409699 100644 --- a/packages/ui/src/components/sections/shared/SidebarGroup.tsx +++ b/packages/ui/src/components/sections/shared/SidebarGroup.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { RiArrowDownSLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; +import { Icon } from "@/components/icon/Icon"; interface SidebarGroupProps { /** Group display label (e.g. "business", "automation-ai") */ @@ -64,7 +64,7 @@ export const SidebarGroup: React.FC = ({ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50', )} > - { return (
- +

{t('settings.skills.page.empty.title')}

{t('settings.skills.page.empty.description')}

@@ -433,11 +433,11 @@ const SkillsInstalledPage: React.FC = () => { > {draftScope === 'user' ? ( - + ) : ( - + )} - {draftSource === 'agents' ? : null} + {draftSource === 'agents' ? : null} {locationLabelText(locationValueFrom(draftScope, draftSource))} @@ -445,8 +445,8 @@ const SkillsInstalledPage: React.FC = () => {
- {option.scope === 'user' ? : } - {option.source === 'agents' ? : null} + {option.scope === 'user' ? : } + {option.source === 'agents' ? : null} {locationLabelText(option.value)}
{locationDescriptionText(option.value)} @@ -501,7 +501,7 @@ const SkillsInstalledPage: React.FC = () => { {t('settings.skills.page.section.supportingFiles')}
@@ -525,7 +525,7 @@ const SkillsInstalledPage: React.FC = () => { className="flex items-center gap-2 py-1.5 cursor-pointer group" onClick={() => handleEditFile(file.path)} > - + {file.path} {isNewSkill && ( @@ -540,7 +540,7 @@ const SkillsInstalledPage: React.FC = () => { handleDeleteFile(file.path); }} > - +
))} diff --git a/packages/ui/src/components/sections/skills/SkillsSidebar.tsx b/packages/ui/src/components/sections/skills/SkillsSidebar.tsx index b2e334e0..48fd71ce 100644 --- a/packages/ui/src/components/sections/skills/SkillsSidebar.tsx +++ b/packages/ui/src/components/sections/skills/SkillsSidebar.tsx @@ -17,13 +17,13 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { RiAddLine, RiDeleteBinLine, RiFileCopyLine, RiMore2Line, RiEditLine, RiBookOpenLine } from '@remixicon/react'; import { useSkillsStore, type DiscoveredSkill } from '@/stores/useSkillsStore'; import { useShallow } from 'zustand/react/shallow'; import { cn } from '@/lib/utils'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector'; import { SidebarGroup } from '@/components/sections/shared/SidebarGroup'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface SkillsSidebarProps { @@ -75,7 +75,6 @@ export const SkillsSidebar: React.FC = ({ onItemSelect }) => setSelectedSkill(newName); onItemSelect?.(); - }; const handleDeleteSkill = async (skill: DiscoveredSkill) => { @@ -125,7 +124,6 @@ export const SkillsSidebar: React.FC = ({ onItemSelect }) => }); setSelectedSkill(newName); - }; const handleOpenRenameDialog = (skill: DiscoveredSkill) => { @@ -226,7 +224,7 @@ export const SkillsSidebar: React.FC = ({ onItemSelect }) => className="h-7 w-7 px-0 -my-1 text-muted-foreground" onClick={handleCreateNew} > - + @@ -234,7 +232,7 @@ export const SkillsSidebar: React.FC = ({ onItemSelect }) => {skills.length === 0 ? (
- +

{t('settings.skills.sidebar.empty.title')}

{t('settings.skills.sidebar.empty.description')}

@@ -482,7 +480,7 @@ const SkillListItem: React.FC = ({ variant="ghost" className="h-6 w-6 px-0 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100" > - + @@ -492,7 +490,7 @@ const SkillListItem: React.FC = ({ onRename(); }} > - + {t('settings.common.actions.rename')} @@ -502,7 +500,7 @@ const SkillListItem: React.FC = ({ onDuplicate(); }} > - + {t('settings.common.actions.duplicate')} @@ -513,7 +511,7 @@ const SkillListItem: React.FC = ({ }} className="text-destructive focus:text-destructive" > - + {t('settings.common.actions.delete')} diff --git a/packages/ui/src/components/sections/skills/catalog/AddCatalogDialog.tsx b/packages/ui/src/components/sections/skills/catalog/AddCatalogDialog.tsx index 2c2c0c88..9f8ba939 100644 --- a/packages/ui/src/components/sections/skills/catalog/AddCatalogDialog.tsx +++ b/packages/ui/src/components/sections/skills/catalog/AddCatalogDialog.tsx @@ -17,8 +17,7 @@ import { SelectItem, SelectTrigger, } from '@/components/ui/select'; - -import { RiGitRepositoryLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { isVSCodeRuntime } from '@/lib/desktop'; @@ -354,7 +353,7 @@ export const AddCatalogDialog: React.FC = ({ open, onOpen onClick={() => void handleScan()} disabled={isScanning || !source.trim()} > - + {isScanning ? t('settings.skills.catalog.shared.actions.scanning') : t('settings.skills.catalog.shared.actions.scan')} @@ -401,8 +401,8 @@ export const InstallFromRepoDialog: React.FC = ({ op }} > - {scope === 'user' ? : } - {targetSource === 'agents' ? : null} + {scope === 'user' ? : } + {targetSource === 'agents' ? : null} {locationLabelText(locationValueFrom(scope, targetSource))} @@ -410,8 +410,8 @@ export const InstallFromRepoDialog: React.FC = ({ op
- {option.scope === 'user' ? : } - {option.source === 'agents' ? : null} + {option.scope === 'user' ? : } + {option.source === 'agents' ? : null} {locationLabelText(option.value)}
{locationDescriptionText(option.value)} diff --git a/packages/ui/src/components/sections/skills/catalog/InstallSkillDialog.tsx b/packages/ui/src/components/sections/skills/catalog/InstallSkillDialog.tsx index fbfca117..60b7a0cf 100644 --- a/packages/ui/src/components/sections/skills/catalog/InstallSkillDialog.tsx +++ b/packages/ui/src/components/sections/skills/catalog/InstallSkillDialog.tsx @@ -17,7 +17,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { RiFolderLine, RiRobot2Line, RiUser3Line } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; import type { SkillsCatalogItem } from '@/lib/api/types'; @@ -208,8 +208,8 @@ export const InstallSkillDialog: React.FC = ({ open, on }} > - {scope === 'user' ? : } - {targetSource === 'agents' ? : null} + {scope === 'user' ? : } + {targetSource === 'agents' ? : null} {locationLabelText(locationValueFrom(scope, targetSource))} @@ -217,8 +217,8 @@ export const InstallSkillDialog: React.FC = ({ open, on
- {option.scope === 'user' ? : } - {option.source === 'agents' ? : null} + {option.scope === 'user' ? : } + {option.source === 'agents' ? : null} {locationLabelText(option.value)}
{locationDescriptionText(option.value)} diff --git a/packages/ui/src/components/sections/skills/catalog/SkillsCatalogPage.tsx b/packages/ui/src/components/sections/skills/catalog/SkillsCatalogPage.tsx index 80dbc207..01ce60fd 100644 --- a/packages/ui/src/components/sections/skills/catalog/SkillsCatalogPage.tsx +++ b/packages/ui/src/components/sections/skills/catalog/SkillsCatalogPage.tsx @@ -19,8 +19,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; - -import { RiAddLine, RiDeleteBinLine, RiRefreshLine, RiDownloadLine, RiStarLine, RiSearchLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore'; import { useShallow } from 'zustand/react/shallow'; @@ -225,7 +224,7 @@ export const SkillsCatalogPage: React.FC = ({ mode, onMo disabled={isLoadingCatalog || isLoadingSource} title={t('settings.skills.catalog.page.actions.refreshTitle')} > - + {isCustomSource && ( @@ -237,7 +236,7 @@ export const SkillsCatalogPage: React.FC = ({ mode, onMo disabled={isRemovingCatalog} title={t('settings.skills.catalog.page.actions.removeCatalogTitle')} > - + )} @@ -246,13 +245,13 @@ export const SkillsCatalogPage: React.FC = ({ mode, onMo className="!font-normal gap-1" onClick={() => setAddCatalogOpen(true)} > - {t('settings.skills.catalog.page.actions.addCatalog')} + {t('settings.skills.catalog.page.actions.addCatalog')}
- + setSearch(e.target.value)} @@ -287,7 +286,7 @@ export const SkillsCatalogPage: React.FC = ({ mode, onMo
) : isLoadingSource ? (
- +

{t('settings.skills.catalog.page.loading.skills')}

) : ( @@ -326,12 +325,12 @@ export const SkillsCatalogPage: React.FC = ({ mode, onMo {t('settings.skills.catalog.page.byOwnerPrefix')} {item.clawdhub.owner} )} - + {item.clawdhub.downloads?.toLocaleString() ?? 0} {(item.clawdhub.stars ?? 0) > 0 && ( - + {item.clawdhub.stars} )} diff --git a/packages/ui/src/components/sections/usage/UsagePage.tsx b/packages/ui/src/components/sections/usage/UsagePage.tsx index fb0222f6..446cbd92 100644 --- a/packages/ui/src/components/sections/usage/UsagePage.tsx +++ b/packages/ui/src/components/sections/usage/UsagePage.tsx @@ -11,10 +11,10 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; -import { RiArrowDownSLine, RiArrowRightSLine, RiInformationLine } from '@remixicon/react'; import type { UsageWindows, QuotaProviderId } from '@/types'; import { getAllModelFamilies, getDisplayModelName, sortModelFamilies, groupModelsByFamilyWithGetter } from '@/lib/quota/model-families'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; const formatTime = (timestamp: number | null) => { @@ -57,7 +57,6 @@ export const UsagePage: React.FC = () => { void fetchAllQuotas(); }, [loadSettings, fetchAllQuotas]); - React.useEffect(() => { if (selectedProviderId) { return; @@ -194,7 +193,7 @@ export const UsagePage: React.FC = () => { {t('settings.usage.page.options.showInHeader')} - + {t('settings.usage.page.options.showInHeaderTooltip')} @@ -269,9 +268,9 @@ export const UsagePage: React.FC = () => {
{isCollapsed ? ( - + ) : ( - + )} @@ -322,9 +321,9 @@ export const UsagePage: React.FC = () => {
{isCollapsed ? ( - + ) : ( - + )} diff --git a/packages/ui/src/components/sections/usage/UsageSidebar.tsx b/packages/ui/src/components/sections/usage/UsageSidebar.tsx index 7852c85b..f2d9d86b 100644 --- a/packages/ui/src/components/sections/usage/UsageSidebar.tsx +++ b/packages/ui/src/components/sections/usage/UsageSidebar.tsx @@ -5,11 +5,11 @@ import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { QUOTA_PROVIDERS, resolveUsageTone } from '@/lib/quota'; import { useQuotaStore } from '@/stores/useQuotaStore'; import { updateDesktopSettings } from '@/lib/persistence'; -import { RiRefreshLine } from '@remixicon/react'; import { useI18n } from '@/lib/i18n'; interface UsageSidebarProps { @@ -121,7 +121,7 @@ export const UsageSidebar: React.FC = ({ onItemSelect }) => { title={t('settings.usage.sidebar.actions.refreshTitle')} disabled={isLoading} > - + diff --git a/packages/ui/src/components/session/BranchPickerDialog.tsx b/packages/ui/src/components/session/BranchPickerDialog.tsx index 385350b2..ba9b0684 100644 --- a/packages/ui/src/components/session/BranchPickerDialog.tsx +++ b/packages/ui/src/components/session/BranchPickerDialog.tsx @@ -9,16 +9,7 @@ import { import { Input } from '@/components/ui/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui'; -import { - RiCheckLine, - RiCloseLine, - RiDeleteBinLine, - RiGitBranchLine, - RiLoader4Line, - RiPencilLine, - RiSearchLine, - RiSplitCellsHorizontal, -} from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { deleteGitBranch, getGitBranches, git, renameBranch } from '@/lib/gitApi'; import type { GitBranch, GitWorktreeInfo } from '@/lib/api/types'; @@ -304,7 +295,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker - + {t('branchPickerDialog.title')} @@ -313,7 +304,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
- + - +
@@ -445,9 +436,9 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker aria-label={t('branchPickerDialog.actions.createWorktreeAria')} > {isCreatingWorktree ? ( - + ) : ( - + )} @@ -465,7 +456,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" aria-label={t('branchPickerDialog.actions.renameAria')} > - + @@ -489,9 +480,9 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker aria-label={hasAttachedWorktree ? t('branchPickerDialog.actions.deleteWorktreeAria') : t('branchPickerDialog.actions.deleteAria')} > {isDeleting ? ( - + ) : ( - + )} @@ -520,9 +511,9 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker aria-label={t('branchPickerDialog.actions.confirmRenameAria')} > {isRenaming ? ( - + ) : ( - + )}
) : null} @@ -557,9 +548,9 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker aria-label={t('branchPickerDialog.actions.confirmDeleteAria')} > {isDeleting ? ( - + ) : ( - + )}
) : null} diff --git a/packages/ui/src/components/session/DirectoryAutocomplete.tsx b/packages/ui/src/components/session/DirectoryAutocomplete.tsx index 48d146fa..348f09a0 100644 --- a/packages/ui/src/components/session/DirectoryAutocomplete.tsx +++ b/packages/ui/src/components/session/DirectoryAutocomplete.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { RiFolderLine, RiRefreshLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import { opencodeClient, type FilesystemEntry } from '@/lib/opencode/client'; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; +import { Icon } from "@/components/icon/Icon"; interface DirectoryAutocompleteProps { inputValue: string; @@ -280,7 +280,7 @@ export const DirectoryAutocomplete = React.forwardRef {loading ? (
- +
) : (
@@ -297,7 +297,7 @@ export const DirectoryAutocomplete = React.forwardRef { handleSelectSuggestion(entry); onClose(); }} onMouseEnter={() => setSelectedIndex(index)} > - + {entry.name}
); diff --git a/packages/ui/src/components/session/DirectoryExplorerDialog.tsx b/packages/ui/src/components/session/DirectoryExplorerDialog.tsx index 14e04fae..73cf26c1 100644 --- a/packages/ui/src/components/session/DirectoryExplorerDialog.tsx +++ b/packages/ui/src/components/session/DirectoryExplorerDialog.tsx @@ -16,18 +16,9 @@ import { useFileSystemAccess } from '@/hooks/useFileSystemAccess'; import { cn } from '@/lib/utils'; import { toast } from '@/components/ui'; import { IdentityDropdown } from '@/components/views/git/GitHeader'; -import { - RiArrowDownSLine, - RiArrowLeftSLine, - RiArrowUpSLine, - RiCheckboxBlankLine, - RiCheckboxLine, - RiCornerDownLeftLine, - RiFolder6Line, - RiFolderAddLine, -} from '@remixicon/react'; import { useDeviceInfo } from '@/lib/device'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; +import { Icon } from "@/components/icon/Icon"; import { opencodeClient } from '@/lib/opencode/client'; import { setDirectoryShowHidden, @@ -523,7 +514,7 @@ export const DirectoryExplorerDialog: React.FC = ( onClick={() => setDirectoryShowHidden(!showHidden)} className="flex flex-shrink-0 items-center gap-2 rounded-lg px-2 py-1 typography-meta text-muted-foreground transition-colors hover:bg-interactive-hover/40" > - {showHidden ? : } + {showHidden ? : } {t('directoryExplorerDialog.toggle.showHidden')} ); @@ -552,7 +543,7 @@ export const DirectoryExplorerDialog: React.FC = (
) : null}
- + = ( )} > {row.type === 'up' ? ( - + ) : ( - + )} {row.name} @@ -657,17 +648,17 @@ export const DirectoryExplorerDialog: React.FC = ( const footerHints = (
- - + + {t('directoryExplorerDialog.footer.navigate')} - + {t('directoryExplorerDialog.footer.select')} {submitModifierLabel} - + {t('directoryExplorerDialog.footer.add')}
diff --git a/packages/ui/src/components/session/DirectoryTree.tsx b/packages/ui/src/components/session/DirectoryTree.tsx index 19bd1d61..5c78b43e 100644 --- a/packages/ui/src/components/session/DirectoryTree.tsx +++ b/packages/ui/src/components/session/DirectoryTree.tsx @@ -8,7 +8,6 @@ import { DropdownMenuTrigger, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; -import { RiAddLine, RiArrowDownSLine, RiArrowRightSLine, RiCheckLine, RiCloseLine, RiFolder6Line, RiPushpin2Line, RiPushpinLine } from '@remixicon/react'; import { cn, formatPathForDisplay } from '@/lib/utils'; import { opencodeClient } from '@/lib/opencode/client'; import { useDeviceInfo } from '@/lib/device'; @@ -16,6 +15,7 @@ import type { DesktopSettings } from '@/lib/desktop'; import { updateDesktopSettings } from '@/lib/persistence'; import { useFileSystemAccess } from '@/hooks/useFileSystemAccess'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface DirectoryItem { @@ -695,9 +695,9 @@ export const DirectoryTree: React.FC = ({ className={cn("hover:bg-interactive-hover rounded", isMobile ? "p-0.5" : "p-0.5")} > {isExpanded ? ( - + ) : ( - + )} )} @@ -728,7 +728,7 @@ export const DirectoryTree: React.FC = ({ isInlineVariant ? (isSelected ? 'text-primary' : 'text-foreground') : 'text-foreground' )} > - = ({ )} title={t('directoryTree.actions.createNewDirectory')} > - + @@ -809,7 +809,7 @@ export const DirectoryTree: React.FC = ({ style={{ paddingLeft: `${(level + 1) * 14 + 6}px` }} >
- + = ({ className="p-1 hover:bg-interactive-hover rounded" title={t('directoryTree.actions.createDirectory')} > - +
)} @@ -881,9 +881,9 @@ export const DirectoryTree: React.FC = ({ className="p-0.5 hover:bg-interactive-hover rounded" > {isExpanded ? ( - + ) : ( - + )} )} @@ -898,7 +898,7 @@ export const DirectoryTree: React.FC = ({ style={{ paddingLeft: `${(level + 1) * 12 + 8}px` }} >
- + = ({ className="p-1 hover:bg-interactive-hover rounded" title={t('directoryTree.actions.createDirectory')} > - +
)} @@ -968,9 +968,9 @@ export const DirectoryTree: React.FC = ({ className={cn("hover:bg-interactive-hover rounded", isMobile ? "p-0.5" : "p-0.5")} > {isExpanded ? ( - + ) : ( - + )} )} @@ -996,7 +996,7 @@ export const DirectoryTree: React.FC = ({ isInlineVariant ? (isSelected ? 'text-primary' : 'text-foreground') : 'text-foreground' )} > - = ({ title={isPinned ? t('directoryTree.actions.unpinDirectory') : t('directoryTree.actions.pinDirectory')} > {isPinned ? ( - + ) : ( - + )} @@ -1105,9 +1105,9 @@ export const DirectoryTree: React.FC = ({ className={cn("hover:bg-interactive-hover rounded flex-shrink-0", isMobile ? "p-0.5" : "p-0.5")} > {isExpanded ? ( - + ) : ( - + )} @@ -1124,7 +1124,7 @@ export const DirectoryTree: React.FC = ({ isSelected ? 'text-primary' : 'text-foreground' )} > - = ({ )} title={t('directoryTree.actions.unpinDirectory')} > - +
{isExpanded && children && children.map((child) => renderPinnedTreeItem(child, 1))} @@ -1183,12 +1183,12 @@ export const DirectoryTree: React.FC = ({ className="p-0.5 hover:bg-interactive-hover rounded flex-shrink-0" > {isExpanded ? ( - + ) : ( - + )} - +
{name}
@@ -1207,7 +1207,7 @@ export const DirectoryTree: React.FC = ({ )} title={t('directoryTree.actions.unpinDirectory')} > - + {isExpanded && children && ( @@ -1238,9 +1238,9 @@ export const DirectoryTree: React.FC = ({ )} > {isPinnedExpanded ? ( - + ) : ( - + )} {t('directoryTree.section.pinned')} @@ -1302,12 +1302,12 @@ export const DirectoryTree: React.FC = ({ aria-label={t('directoryTree.actions.selectWorkingDirectoryAria')} > - + {formatPathForDisplay(currentPath, homeDirectory)} - + diff --git a/packages/ui/src/components/session/GitHubIntegrationDialog.tsx b/packages/ui/src/components/session/GitHubIntegrationDialog.tsx index e80b537a..8a4e3004 100644 --- a/packages/ui/src/components/session/GitHubIntegrationDialog.tsx +++ b/packages/ui/src/components/session/GitHubIntegrationDialog.tsx @@ -9,16 +9,6 @@ import { import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; -import { - RiGithubLine, - RiLoader4Line, - RiSearchLine, - RiErrorWarningLine, - RiCheckLine, - RiGitPullRequestLine, - RiGitBranchLine, - RiCloseLine, -} from '@remixicon/react'; import { cn } from '@/lib/utils'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useProjectsStore } from '@/stores/useProjectsStore'; @@ -27,6 +17,7 @@ import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { validateWorktreeCreate } from '@/lib/worktrees/worktreeManager'; import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; +import { Icon } from "@/components/icon/Icon"; import type { GitHubIssue, GitHubIssueSummary, @@ -299,7 +290,7 @@ export function GitHubIntegrationDialog({ <> {!isGitHubConnected ? (
- +

{t('session.githubIntegration.connect.title')}

@@ -312,7 +303,7 @@ export function GitHubIntegrationDialog({ <> {/* Search */}

- + setSearchQuery(e.target.value)} @@ -329,7 +320,7 @@ export function GitHubIntegrationDialog({ {/* Loading */} {loading && (
- +
)} @@ -337,7 +328,7 @@ export function GitHubIntegrationDialog({ {error && (
- + {error}
@@ -391,7 +382,7 @@ export function GitHubIntegrationDialog({ )} {loadingMore && (
- +
)}
@@ -463,7 +454,7 @@ export function GitHubIntegrationDialog({ )} {loadingMore && (
- +
)}
@@ -489,7 +480,7 @@ export function GitHubIntegrationDialog({ {/* Selected Issue/PR display - hidden on mobile (shown in header instead) */} {!isMobile && (selectedIssue || selectedPr) && (
- + {selectedIssue ? t('session.githubIntegration.selected.issueNumber', { number: selectedIssue.number }) @@ -499,7 +490,7 @@ export function GitHubIntegrationDialog({ onClick={handleClear} className="text-muted-foreground hover:text-foreground shrink-0 p-0.5 rounded hover:bg-muted transition-colors" > - +
)} @@ -562,8 +553,8 @@ export function GitHubIntegrationDialog({
}, - { id: 'prs', label: t('session.githubIntegration.tabs.pullRequests'), icon: }, + { id: 'issues', label: t('session.githubIntegration.tabs.issues'), icon: }, + { id: 'prs', label: t('session.githubIntegration.tabs.pullRequests'), icon: }, ]} activeId={activeTab} onSelect={(id) => { @@ -578,7 +569,7 @@ export function GitHubIntegrationDialog({ {/* Selected Item Inline Display */} {(selectedIssue || selectedPr) && (
- + {selectedIssue ? t('session.githubIntegration.selected.issueNumber', { number: selectedIssue.number }) @@ -588,7 +579,7 @@ export function GitHubIntegrationDialog({ onClick={handleClear} className="text-muted-foreground hover:text-foreground shrink-0 p-0.5 rounded hover:bg-muted transition-colors" > - +
)} @@ -603,7 +594,7 @@ export function GitHubIntegrationDialog({
- + {t('session.githubIntegration.title')} @@ -611,8 +602,8 @@ export function GitHubIntegrationDialog({
}, - { id: 'prs', label: t('session.githubIntegration.tabs.pullRequests'), icon: }, + { id: 'issues', label: t('session.githubIntegration.tabs.issues'), icon: }, + { id: 'prs', label: t('session.githubIntegration.tabs.pullRequests'), icon: }, ]} activeId={activeTab} onSelect={(id) => { diff --git a/packages/ui/src/components/session/GitHubIssuePickerDialog.tsx b/packages/ui/src/components/session/GitHubIssuePickerDialog.tsx index 4115e50e..e60e76fa 100644 --- a/packages/ui/src/components/session/GitHubIssuePickerDialog.tsx +++ b/packages/ui/src/components/session/GitHubIssuePickerDialog.tsx @@ -10,14 +10,7 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { toast } from '@/components/ui'; -import { - RiCheckboxBlankLine, - RiCheckboxLine, - RiExternalLinkLine, - RiGithubLine, - RiLoader4Line, - RiSearchLine, -} from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useProjectsStore } from '@/stores/useProjectsStore'; @@ -499,7 +492,7 @@ export function GitHubIssuePickerDialog({ const content = ( <>
- + - + {t('session.githubIssuePicker.loading.issues')}
) : null} @@ -553,7 +546,7 @@ export function GitHubIssuePickerDialog({

{startingIssueNumber === directNumber ? ( - + ) : null}
@@ -588,7 +581,7 @@ export function GitHubIssuePickerDialog({ @@ -621,7 +614,7 @@ export function GitHubIssuePickerDialog({ > {isLoadingMore ? ( - + {t('session.githubIssuePicker.loading.more')} ) : ( @@ -660,9 +653,9 @@ export function GitHubIssuePickerDialog({ className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" > {createInWorktree ? ( - + ) : ( - + )} {t('session.githubIssuePicker.actions.createInWorktree')} @@ -673,7 +666,7 @@ export function GitHubIssuePickerDialog({ {repoUrl ? ( @@ -714,7 +707,7 @@ export function GitHubIssuePickerDialog({ - + {title} diff --git a/packages/ui/src/components/session/GitHubPrPickerDialog.tsx b/packages/ui/src/components/session/GitHubPrPickerDialog.tsx index f9792709..5bbe57bc 100644 --- a/packages/ui/src/components/session/GitHubPrPickerDialog.tsx +++ b/packages/ui/src/components/session/GitHubPrPickerDialog.tsx @@ -11,12 +11,7 @@ import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { toast } from '@/components/ui'; -import { - RiGithubLine, - RiLoader4Line, - RiSearchLine, - RiExternalLinkLine, -} from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useProjectsStore } from '@/stores/useProjectsStore'; @@ -269,7 +264,7 @@ export function GitHubPrPickerDialog({ <>
- + - + {t('session.githubPrPicker.loading.pullRequests')}
) : null} @@ -340,7 +335,7 @@ export function GitHubPrPickerDialog({

{loadingPrNumber === directNumber ? ( - + ) : null}
@@ -374,7 +369,7 @@ export function GitHubPrPickerDialog({ @@ -407,7 +402,7 @@ export function GitHubPrPickerDialog({ > {isLoadingMore ? ( - + {t('session.githubPrPicker.loading.more')} ) : ( @@ -446,7 +441,7 @@ export function GitHubPrPickerDialog({ - + {title} diff --git a/packages/ui/src/components/session/NewWorktreeDialog.tsx b/packages/ui/src/components/session/NewWorktreeDialog.tsx index 15bff4f4..7c4a0b88 100644 --- a/packages/ui/src/components/session/NewWorktreeDialog.tsx +++ b/packages/ui/src/components/session/NewWorktreeDialog.tsx @@ -23,18 +23,6 @@ import { CommandList, CommandSeparator, } from '@/components/ui/command'; -import { - RiGitBranchLine, - RiGitRepositoryLine, - RiGithubLine, - RiLoader4Line, - RiRefreshLine, - RiErrorWarningLine, - RiCheckLine, - RiExternalLinkLine, - RiCloseLine, - RiArrowDownSLine, -} from '@remixicon/react'; import { cn } from '@/lib/utils'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; @@ -58,6 +46,7 @@ import { useGitBranches, useGitStore, useGitLoadingBranches } from '@/stores/use import { GitHubIntegrationDialog } from './GitHubIntegrationDialog'; import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; +import { Icon } from "@/components/icon/Icon"; import type { GitHubIssue, GitHubIssueComment, @@ -1037,7 +1026,7 @@ export function NewWorktreeDialog({
{validation.touched && (validation.branchError || validation.worktreeError) && ( <> - + {validation.branchError || validation.worktreeError} @@ -1062,7 +1051,7 @@ export function NewWorktreeDialog({ disabled={!canCreate || isCreating} className={cn('gap-1.5', isMobile && 'flex-1')} > - {isCreating && } + {isCreating && } {isCreating ? t('session.newWorktree.actions.creating') : t('session.newWorktree.actions.createWorktree')}
@@ -1082,8 +1071,8 @@ export function NewWorktreeDialog({
}, - { id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: }, + { id: 'new-branch', label: t('session.newWorktree.mode.newBranch'), icon: }, + { id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: }, ]} activeId={mode} onSelect={(id) => handleModeChange(id as Mode)} @@ -1110,7 +1099,7 @@ export function NewWorktreeDialog({ {existingBranchState.selectedBranch || t('session.newWorktree.chooseBranch')} - +
@@ -1266,7 +1255,7 @@ export function NewWorktreeDialog({ onClick={() => setGithubDialogOpen(true)} className="gap-1.5 h-7" > - + {newBranchState.linkedIssue || newBranchState.linkedPr ? t('session.newWorktree.actions.change') : t('session.newWorktree.actions.startFromGitHubIssuePr')} )} @@ -1293,7 +1282,7 @@ export function NewWorktreeDialog({ /> {newBranchState.linkedPr && (
- + {t('session.newWorktree.usingPrBranch', { branch: newBranchState.linkedPr.head })} @@ -1301,7 +1290,7 @@ export function NewWorktreeDialog({ )} {newBranchState.linkedIssue && !newBranchState.linkedPr && (
- + {t('session.newWorktree.fromIssue', { number: newBranchState.linkedIssue.number, title: newBranchState.linkedIssue.title })} @@ -1335,7 +1324,7 @@ export function NewWorktreeDialog({ )} title={t('session.newWorktree.resetToMatchBranchName')} > - + {t('session.newWorktree.actions.reset')} )} @@ -1380,7 +1369,7 @@ export function NewWorktreeDialog({ {newBranchState.sourceBranch || t('session.newWorktree.selectSourceBranchPlaceholder')} - + {newBranchState.sourceBranch && (
@@ -1509,7 +1498,7 @@ export function NewWorktreeDialog({
{/* Row 1: Type, number, title, actions */}
- + {newBranchState.linkedIssue && ( @@ -1533,14 +1522,14 @@ export function NewWorktreeDialog({ className="text-muted-foreground hover:text-foreground shrink-0" onClick={(e) => e.stopPropagation()} > - +
@@ -1567,7 +1556,7 @@ export function NewWorktreeDialog({
- + {t('session.newWorktree.title')} @@ -1575,8 +1564,8 @@ export function NewWorktreeDialog({
}, - { id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: }, + { id: 'new-branch', label: t('session.newWorktree.mode.newBranch'), icon: }, + { id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: }, ]} activeId={mode} onSelect={(id) => handleModeChange(id as Mode)} @@ -1602,7 +1591,7 @@ export function NewWorktreeDialog({ {existingBranchState.selectedBranch || t('session.newWorktree.chooseBranch')} - + @@ -1714,7 +1703,7 @@ export function NewWorktreeDialog({ disabled={!canFetchBranches || isLoadingBranches} title={t('session.newWorktree.fetchBranches')} > - {isLoadingBranches ? : } + {isLoadingBranches ? : }
@@ -1731,7 +1720,7 @@ export function NewWorktreeDialog({ onClick={() => setGithubDialogOpen(true)} className="gap-1.5 h-7" > - + {newBranchState.linkedIssue || newBranchState.linkedPr ? t('session.newWorktree.actions.change') : t('session.newWorktree.actions.startFromGitHubIssuePr')} )} @@ -1758,7 +1747,7 @@ export function NewWorktreeDialog({ /> {newBranchState.linkedPr && (
- + {t('session.newWorktree.usingPrBranch', { branch: newBranchState.linkedPr.head })} @@ -1766,7 +1755,7 @@ export function NewWorktreeDialog({ )} {newBranchState.linkedIssue && !newBranchState.linkedPr && (
- + {t('session.newWorktree.fromIssue', { number: newBranchState.linkedIssue.number, title: newBranchState.linkedIssue.title })} @@ -1800,7 +1789,7 @@ export function NewWorktreeDialog({ )} title={t('session.newWorktree.resetToMatchBranchName')} > - + {t('session.newWorktree.actions.reset')} )} @@ -1842,7 +1831,7 @@ export function NewWorktreeDialog({ {newBranchState.sourceBranch || t('session.newWorktree.selectSourceBranchPlaceholder')} - + @@ -1944,7 +1933,7 @@ export function NewWorktreeDialog({
{/* Row 1: Type, number, title, actions */}
- + {newBranchState.linkedIssue && ( @@ -1968,14 +1957,14 @@ export function NewWorktreeDialog({ className="text-muted-foreground hover:text-foreground shrink-0" onClick={(e) => e.stopPropagation()} > - +
@@ -2002,7 +1991,7 @@ export function NewWorktreeDialog({
{validation.touched && (validation.branchError || validation.worktreeError) && ( <> - + {validation.branchError || validation.worktreeError} @@ -2025,7 +2014,7 @@ export function NewWorktreeDialog({ disabled={!canCreate || isCreating} className="gap-1.5" > - {isCreating && } + {isCreating && } {isCreating ? t('session.newWorktree.actions.creating') : t('session.newWorktree.actions.createWorktree')}
diff --git a/packages/ui/src/components/session/ProjectNotesTodoPanel.tsx b/packages/ui/src/components/session/ProjectNotesTodoPanel.tsx index 13ba89a7..7d7f3837 100644 --- a/packages/ui/src/components/session/ProjectNotesTodoPanel.tsx +++ b/packages/ui/src/components/session/ProjectNotesTodoPanel.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RiAddLine, RiDeleteBinLine, RiSendPlaneLine } from '@remixicon/react'; import { toast } from '@/components/ui'; import { Checkbox } from '@/components/ui/checkbox'; import { @@ -10,6 +9,7 @@ import { } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; +import { Icon } from "@/components/icon/Icon"; import { deleteProjectPlanFile, getProjectContextData, @@ -575,7 +575,7 @@ export const ProjectNotesTodoPanel: React.FC = ({ aria-label={t('rightSidebar.contextNotesTodo.todo.addAria')} title={t('rightSidebar.contextNotesTodo.todo.addAria')} > - +
@@ -623,7 +623,7 @@ export const ProjectNotesTodoPanel: React.FC = ({ aria-label={t('rightSidebar.contextNotesTodo.todo.actions.delete', { text: todo.text })} title={t('rightSidebar.contextNotesTodo.todo.actions.delete', { text: todo.text })} > - + @@ -634,7 +634,7 @@ export const ProjectNotesTodoPanel: React.FC = ({ aria-label={t('rightSidebar.contextNotesTodo.todo.actions.send', { text: todo.text })} title={t('rightSidebar.contextNotesTodo.todo.actions.send', { text: todo.text })} > - + @@ -692,7 +692,7 @@ export const ProjectNotesTodoPanel: React.FC = ({ aria-label={t('rightSidebar.contextNotesTodo.plans.importFromFile')} title={t('rightSidebar.contextNotesTodo.plans.importFromFile')} > - +
@@ -723,7 +723,7 @@ export const ProjectNotesTodoPanel: React.FC = ({ title={t('rightSidebar.contextNotesTodo.plans.deletePlan')} aria-label={t('rightSidebar.contextNotesTodo.plans.deletePlanWithTitle', { title: plan.title })} > - + ))} diff --git a/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx b/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx index 69aaf2eb..29ea5045 100644 --- a/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx +++ b/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx @@ -9,12 +9,12 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Button } from '@/components/ui/button'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { toast } from '@/components/ui'; -import { RiAddLine, RiCloseLine, RiCalendarLine, RiArrowLeftSLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/react'; import { ModelSelector } from '@/components/sections/agents/ModelSelector'; import { AgentSelector } from '@/components/sections/commands/AgentSelector'; import { isPrimaryMode } from '@/components/chat/mobileControlsUtils'; import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete'; import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete'; +import { Icon } from "@/components/icon/Icon"; import { useConfigStore } from '@/stores/useConfigStore'; import { useUIStore } from '@/stores/useUIStore'; import type { ScheduledTask } from '@/lib/scheduledTasksApi'; @@ -700,7 +700,6 @@ export function ScheduledTaskEditorDialog(props: { }; }, [isDatePickerOpen]); - const variantOptions = React.useMemo(() => { const provider = providers.find((item) => item.id === draft.execution.providerID); const model = provider?.models?.find((item) => item.id === draft.execution.modelID) as { variants?: Record } | undefined; @@ -1066,10 +1065,10 @@ export function ScheduledTaskEditorDialog(props: { onClick={() => setIsDatePickerOpen((prev) => !prev)} > - + {formatDateLabel(draft.schedule.onceDate, t('sessions.scheduledTasks.editor.date.placeholder'), locale)} - + {isDatePickerOpen ? ( @@ -1082,7 +1081,7 @@ export function ScheduledTaskEditorDialog(props: { aria-label={t('sessions.scheduledTasks.editor.date.previousMonth')} disabled={isAtCurrentMonth} > - +
{new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(calendarMonth)} @@ -1093,7 +1092,7 @@ export function ScheduledTaskEditorDialog(props: { onClick={() => setCalendarMonth((prev) => shiftMonth(prev, 1))} aria-label={t('sessions.scheduledTasks.editor.date.nextMonth')} > - +
@@ -1255,7 +1254,7 @@ export function ScheduledTaskEditorDialog(props: { onClick={() => removeTimeAt(index)} aria-label={t('sessions.scheduledTasks.editor.times.removeAria')} > - + ) : null}
@@ -1263,7 +1262,7 @@ export function ScheduledTaskEditorDialog(props: {
diff --git a/packages/ui/src/components/session/ScheduledTasksDialog.tsx b/packages/ui/src/components/session/ScheduledTasksDialog.tsx index a4a59fb2..5423137f 100644 --- a/packages/ui/src/components/session/ScheduledTasksDialog.tsx +++ b/packages/ui/src/components/session/ScheduledTasksDialog.tsx @@ -11,19 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Checkbox } from '@/components/ui/checkbox'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { toast } from '@/components/ui'; -import { - RiLoader4Line, - RiPlayLine, - RiEdit2Line, - RiDeleteBinLine, - RiAddLine, - RiFolderLine, - RiTimerLine, - RiHistoryLine, - RiCheckboxCircleLine, - RiErrorWarningLine, - RiPulseLine, -} from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { useUIStore } from '@/stores/useUIStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; @@ -159,14 +147,14 @@ const STATUS_META: Record< ScheduledTaskStatus, { tone: StatusTone; - Icon: React.ComponentType<{ className?: string }>; + Icon: string; spin?: boolean; } > = { - success: { tone: 'success', Icon: RiCheckboxCircleLine }, - error: { tone: 'error', Icon: RiErrorWarningLine }, - running: { tone: 'warning', Icon: RiLoader4Line, spin: true }, - idle: { tone: 'muted', Icon: RiPulseLine }, + success: { tone: 'success', Icon: 'checkbox-circle' }, + error: { tone: 'error', Icon: 'error-warning' }, + running: { tone: 'warning', Icon: 'loader-4', spin: true }, + idle: { tone: 'muted', Icon: 'pulse' }, }; const toneStyle = (tone: StatusTone): React.CSSProperties => { @@ -213,7 +201,7 @@ export function ScheduledTasksDialog() { iconColor: currentTheme.colors.surface.foreground, }, ); - const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null; + const projectIconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null; const iconColor = project.color ? PROJECT_COLOR_MAP[project.color] : undefined; return ( @@ -225,10 +213,10 @@ export function ScheduledTasksDialog() { > - ) : ProjectIcon ? ( - + ) : projectIconName ? ( + ) : ( - + )} {displayLabel} @@ -423,7 +411,7 @@ export function ScheduledTasksDialog() {
{projectSelector}
) : ( @@ -433,7 +421,7 @@ export function ScheduledTasksDialog() {
{loading ? (
- {t('sessions.scheduledTasks.dialog.loading')} + {t('sessions.scheduledTasks.dialog.loading')}
) : tasks.length === 0 ? (
@@ -474,7 +462,7 @@ export function ScheduledTasksDialog() {
- + {t('sessions.scheduledTasks.dialog.nextRun.label')} {nextAt ? ( <> @@ -487,14 +475,14 @@ export function ScheduledTasksDialog() { )} - + {t('sessions.scheduledTasks.dialog.lastRun.label')} {status === 'running' ? ( - + {t('sessions.scheduledTasks.dialog.lastRun.runningNow')} ) : lastAt ? ( @@ -504,7 +492,7 @@ export function ScheduledTasksDialog() { className="inline-flex items-center gap-1" style={{ color: `var(--status-${meta.tone})` }} > - + {statusLabel} ) : null} @@ -522,7 +510,7 @@ export function ScheduledTasksDialog() { className="mt-3 flex items-start gap-2 rounded-md border p-2 typography-micro" style={toneStyle('error')} > - + {task.state.lastError}
) : null} @@ -553,7 +541,7 @@ export function ScheduledTasksDialog() { onClick={() => void handleRunNow(task)} disabled={isBusy} > - {t('sessions.scheduledTasks.dialog.actions.runNow')} + {t('sessions.scheduledTasks.dialog.actions.runNow')}
@@ -612,7 +600,7 @@ export function ScheduledTasksDialog() { onClick={openNewTaskEditor} disabled={!selectedProjectID} > - {t('sessions.scheduledTasks.dialog.actions.newTask')} + {t('sessions.scheduledTasks.dialog.actions.newTask')} )} > diff --git a/packages/ui/src/components/session/SessionDialogs.tsx b/packages/ui/src/components/session/SessionDialogs.tsx index f68b2f9a..2f594bf2 100644 --- a/packages/ui/src/components/session/SessionDialogs.tsx +++ b/packages/ui/src/components/session/SessionDialogs.tsx @@ -10,8 +10,8 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { RiCheckboxBlankLine, RiCheckboxLine, RiDeleteBinLine, RiGitBranchLine } from '@remixicon/react'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; +import { Icon } from "@/components/icon/Icon"; import { DirectoryExplorerDialog } from './DirectoryExplorerDialog'; import { cn, formatPathForDisplay } from '@/lib/utils'; import type { Session } from '@opencode-ai/sdk/v2'; @@ -606,7 +606,7 @@ export const SessionDialogs: React.FC = () => { {isWorktreeDelete ? (
- + {t('sessions.sidebar.sessionDialogs.worktree.label')} {targetWorktree?.label ? ( {targetWorktree.label} @@ -651,9 +651,9 @@ export const SessionDialogs: React.FC = () => { )} > {deleteDialogShouldRemoveRemote ? ( - + ) : ( - + )} {t('sessions.sidebar.sessionDialogs.actions.deleteRemoteBranch')} @@ -680,9 +680,9 @@ export const SessionDialogs: React.FC = () => { )} > {deleteDialogShouldDeleteLocalBranch ? ( - + ) : ( - + )} {t('sessions.sidebar.sessionDialogs.actions.deleteLocalBranch')} @@ -713,7 +713,7 @@ export const SessionDialogs: React.FC = () => { className="inline-flex items-center gap-1.5 typography-meta text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50" aria-pressed={!showDeletionDialog} > - {!showDeletionDialog ? : } + {!showDeletionDialog ? : } {t('sessions.sidebar.dialogs.neverAsk')}
@@ -779,7 +779,7 @@ export const SessionDialogs: React.FC = () => { > - {isWorktreeDelete && } + {isWorktreeDelete && } {deleteDialogTitle} {deleteDialogDescription && {deleteDialogDescription}} diff --git a/packages/ui/src/components/session/SessionFolderItem.tsx b/packages/ui/src/components/session/SessionFolderItem.tsx index d8bf963a..27c569d0 100644 --- a/packages/ui/src/components/session/SessionFolderItem.tsx +++ b/packages/ui/src/components/session/SessionFolderItem.tsx @@ -1,19 +1,8 @@ import React from 'react'; -import { - RiFolderLine, - RiFolderOpenLine, - RiArrowRightSLine, - RiArrowDownSLine, - RiPencilAiLine, - RiDeleteBinLine, - RiCheckLine, - RiCloseLine, - RiAddLine, - RiFolderAddLine, -} from '@remixicon/react'; import { cn } from '@/lib/utils'; import type { SessionFolder } from '@/stores/useSessionFoldersStore'; import { useI18n } from '@/lib/i18n'; +import { Icon } from "@/components/icon/Icon"; interface SessionFolderItemProps { folder: SessionFolder; @@ -143,7 +132,7 @@ const SessionFolderItemBase = ({ }; }, [isRenaming]); - const FolderIcon = isCollapsed ? RiFolderLine : RiFolderOpenLine; + const folderIconName = isCollapsed ? 'folder' : 'folder-open'; const isSubFolder = depth > 0; return ( @@ -179,7 +168,7 @@ const SessionFolderItemBase = ({ ? (alwaysShowActions ? 'pr-7' : 'group-hover/folder:pr-7 group-focus-within/folder:pr-7') : '', )}> - + {renaming ? (
({ onPointerDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} > - + ) : ( @@ -244,9 +233,9 @@ const SessionFolderItemBase = ({ • {sessions.length} {isCollapsed ? ( - + ) : ( - + )}
)} @@ -274,7 +263,7 @@ const SessionFolderItemBase = ({ aria-label={t('sessions.sidebar.folderItem.newSessionAria', { folderName: folder.name })} title={t('sessions.sidebar.project.actions.newSession')} > - + ) : null} {/* Only allow sub-folders at depth 0 (one level deep max) */} @@ -289,7 +278,7 @@ const SessionFolderItemBase = ({ aria-label={t('sessions.sidebar.folderItem.newSubfolderAria', { folderName: folder.name })} title={t('sessions.sidebar.folderItem.newSubfolder')} > - + ) : null} {!archivedBucket ? ( @@ -302,7 +291,7 @@ const SessionFolderItemBase = ({ className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50" aria-label={t('sessions.sidebar.folderItem.renameAria', { folderName: folder.name })} > - + ) : null}
diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index a5c22701..6f6f8420 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -1,6 +1,5 @@ import React from 'react'; import type { Session } from '@opencode-ai/sdk/v2'; -import { RiLayoutLeftLine } from '@remixicon/react'; import { toast } from '@/components/ui'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useI18n } from '@/lib/i18n'; @@ -38,6 +37,7 @@ import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders import { getGitHubPrStatusKey, usePrVisualSummaryByKeys, useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore'; import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog'; import { UpdateDialog } from '@/components/ui/UpdateDialog'; +import { Icon } from "@/components/icon/Icon"; import { SessionGroupSection } from './sidebar/SessionGroupSection'; import { SidebarHeader } from './sidebar/SidebarHeader'; import { SidebarActivitySections } from './sidebar/SidebarActivitySections'; @@ -1648,7 +1648,7 @@ export const SessionSidebar: React.FC = ({ className={desktopSidebarToggleButtonClass} aria-label={t('sessions.sidebar.header.actions.closeSessions')} > - + diff --git a/packages/ui/src/components/session/TodoSendDialog.tsx b/packages/ui/src/components/session/TodoSendDialog.tsx index 21f87c64..483ec084 100644 --- a/packages/ui/src/components/session/TodoSendDialog.tsx +++ b/packages/ui/src/components/session/TodoSendDialog.tsx @@ -17,8 +17,8 @@ import { AgentSelector } from '@/components/sections/commands/AgentSelector'; import { useConfigStore } from '@/stores/useConfigStore'; import { useAgentsStore } from '@/stores/useAgentsStore'; import { isPrimaryMode } from '@/components/chat/mobileControlsUtils'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; -import { RiArrowDownSLine } from '@remixicon/react'; import { useI18n } from '@/lib/i18n'; type TodoSendTarget = 'session' | 'worktree'; @@ -70,7 +70,7 @@ const ThinkingPill = ({ value, options, disabled, onChange }: ThinkingPillProps) )} > {label} - +
); diff --git a/packages/ui/src/components/session/sidebar/BulkActionBar.tsx b/packages/ui/src/components/session/sidebar/BulkActionBar.tsx index 92450bce..f42e3b82 100644 --- a/packages/ui/src/components/session/sidebar/BulkActionBar.tsx +++ b/packages/ui/src/components/session/sidebar/BulkActionBar.tsx @@ -7,12 +7,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { - RiAddLine, - RiCloseLine, - RiDeleteBinLine, - RiFolderLine, -} from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import type { SessionFolder } from '@/stores/useSessionFoldersStore'; import { useI18n } from '@/lib/i18n'; @@ -67,7 +62,7 @@ export const BulkActionBar: React.FC = ({ className={iconButtonClass} aria-label={t('sessions.sidebar.bulkActions.moveToFolder')} > - + @@ -87,7 +82,7 @@ export const BulkActionBar: React.FC = ({ )} - + {t('sessions.sidebar.folders.newFolderEllipsis')} {canRemoveFromFolder ? ( @@ -95,7 +90,7 @@ export const BulkActionBar: React.FC = ({ onClick={onRemoveFromFolder} className="text-destructive focus:text-destructive" > - + {t('sessions.sidebar.folders.removeFromFolder')} ) : null} @@ -111,7 +106,7 @@ export const BulkActionBar: React.FC = ({ className={cn(destructiveIconButtonClass)} aria-label={destructiveLabel} > - +

{destructiveLabel}

@@ -125,7 +120,7 @@ export const BulkActionBar: React.FC = ({ className={iconButtonClass} aria-label={t('sessions.sidebar.header.actions.exitSelection')} > - +

{t('sessions.sidebar.header.actions.exitSelection')}

diff --git a/packages/ui/src/components/session/sidebar/ConfirmDialogs.tsx b/packages/ui/src/components/session/sidebar/ConfirmDialogs.tsx index 6006d343..fb6ee1e8 100644 --- a/packages/ui/src/components/session/sidebar/ConfirmDialogs.tsx +++ b/packages/ui/src/components/session/sidebar/ConfirmDialogs.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { RiCheckboxBlankLine, RiCheckboxLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import type { Session } from '@opencode-ai/sdk/v2'; import { useI18n } from '@/lib/i18n'; @@ -65,7 +65,7 @@ export function SessionDeleteConfirmDialog(props: { className="inline-flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50" aria-pressed={!showDeletionDialog} > - {!showDeletionDialog ? : } + {!showDeletionDialog ? : } {t('sessions.sidebar.dialogs.neverAsk')}
@@ -135,7 +135,7 @@ export function BulkSessionDeleteConfirmDialog(props: { className="inline-flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50" aria-pressed={!showDeletionDialog} > - {!showDeletionDialog ? : } + {!showDeletionDialog ? : } {t('sessions.sidebar.dialogs.neverAsk')}
diff --git a/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx b/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx index 501bf950..22293ec6 100644 --- a/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx +++ b/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx @@ -1,15 +1,7 @@ import React from 'react'; import type { Session } from '@opencode-ai/sdk/v2'; -import { - RiAddLine, - RiArchiveLine, - RiArrowDownSLine, - RiArrowLeftLongLine, - RiArrowRightSLine, - RiDeleteBinLine, - RiGitBranchLine, -} from '@remixicon/react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { sessionEvents } from '@/lib/sessionEvents'; import type { MainTab } from '@/stores/useUIStore'; @@ -536,7 +528,7 @@ export function SessionGroupSection(props: Props): React.ReactNode { - @@ -544,7 +536,7 @@ export function SessionGroupSection(props: Props): React.ReactNode { 'text-muted-foreground h-3.5 w-3.5 items-center justify-center', alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex', )}> - {isCollapsed ? : } + {isCollapsed ? : } {prIndicator.url ? ( @@ -568,7 +560,7 @@ export function SessionGroupSection(props: Props): React.ReactNode { {baseBranchLabel && headBranchLabel ? ( <> {baseBranchLabel} - + {headBranchLabel} ) : ( @@ -593,12 +585,12 @@ export function SessionGroupSection(props: Props): React.ReactNode { ) : group.isArchivedBucket ? ( - + - {isCollapsed ? : } + {isCollapsed ? : } {renderHighlightedText(group.label, normalizedSessionSearchQuery)} @@ -606,7 +598,7 @@ export function SessionGroupSection(props: Props): React.ReactNode { ) : (!group.isMain || group.worktree) ? ( - @@ -614,7 +606,7 @@ export function SessionGroupSection(props: Props): React.ReactNode { 'text-muted-foreground h-3.5 w-3.5 items-center justify-center', alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex', )}> - {isCollapsed ? : } + {isCollapsed ? : } {renderHighlightedText(group.label, normalizedSessionSearchQuery)} @@ -626,16 +618,15 @@ export function SessionGroupSection(props: Props): React.ReactNode { {showBranchSubtitle && statusLine ? ( {group.isArchivedBucket ? ( - + ) : (!group.isMain || isGitProject) ? ( showInlinePrTitle && prIndicator ? ( - + @@ -645,7 +636,7 @@ export function SessionGroupSection(props: Props): React.ReactNode { {baseBranchLabel && headBranchLabel ? ( <> {baseBranchLabel} - + {headBranchLabel} ) : ( @@ -666,10 +657,9 @@ export function SessionGroupSection(props: Props): React.ReactNode { ) : ( - + ) ) : null} - +

{t('sessions.sidebar.group.actions.deleteArchivedSessions')}

@@ -722,7 +712,7 @@ export function SessionGroupSection(props: Props): React.ReactNode { className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50" aria-label={t('sessions.sidebar.group.actions.deleteGroupAria', { label: group.label })} > - +

{t('sessions.sidebar.group.actions.deleteWorktree')}

@@ -745,7 +735,7 @@ export function SessionGroupSection(props: Props): React.ReactNode { className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50" aria-label={t('sessions.sidebar.group.actions.newDraftInGroupAria', { label: group.label })} > - +

{t('sessions.sidebar.project.actions.newDraftSession')}

diff --git a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx index 977da03e..292786ad 100644 --- a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx +++ b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx @@ -11,34 +11,12 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { - RiAddLine, - RiArchiveLine, - RiArrowDownSLine, - RiArrowRightSLine, - RiChat4Line, - RiCheckLine, - RiCloseLine, - RiDeleteBinLine, - RiDownloadLine, - RiErrorWarningLine, - RiFileCopyLine, - RiFolderLine, - RiLinkUnlinkM, - RiMore2Line, - RiPencilAiLine, - RiPushpinLine, - RiShare2Line, - RiShieldLine, - RiUnpinLine, - RiGitBranchLine, - RiWindowLine, -} from '@remixicon/react'; import { cn } from '@/lib/utils'; import { canUseElectronDesktopIPC, invokeDesktop, isVSCodeRuntime } from '@/lib/desktop'; import { toast } from '@/components/ui'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Icon } from "@/components/icon/Icon"; import { buildExportFilename, downloadAsMarkdown, formatSessionAsMarkdown, getExportRevealLabelKey, revealExportedMarkdown, saveAsMarkdownDesktop } from '@/lib/exportSession'; import type { ChildSessionExport } from '@/lib/exportSession'; import { buildSessionMessageRecordsSnapshot, useDirectoryStore, useGlobalSessionStatus, useSession, useSessionPermissions } from '@/sync/sync-context'; @@ -479,7 +457,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { title={t('sessions.sidebar.session.rename.save')} className="shrink-0 text-muted-foreground hover:text-foreground" > - + {!isMinimalMode ? (
- {hasChildren ? {isExpanded ? : } : null} + {hasChildren ? {isExpanded ? : } : null} {sessionUpdatedLabel} {sessionDiffStats ? +{sessionDiffStats.additions}/-{sessionDiffStats.deletions} : null} {hasSecondaryProjectLabel ? {secondaryMeta?.projectLabel} : null} - {hasSecondaryBranchLabel ? {secondaryMeta?.branchLabel} : null} + {hasSecondaryBranchLabel ? {secondaryMeta?.branchLabel} : null}
) : null} @@ -537,7 +515,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { )} > {showStatusMarker ? statusMarkerContent : null} - {isPinnedSession ? : null} + {isPinnedSession ? : null} ) : null; const subsessionChevron = hasChildren ? ( @@ -566,12 +544,12 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { ? t('sessions.sidebar.session.subsessions.collapse') : t('sessions.sidebar.session.subsessions.expand')} > - {isExpanded ? : } + {isExpanded ? : } ) : null; const streamingIndicator = isZombie - ? + ? : null; const handleMenuOpenChange = (open: boolean) => { @@ -663,33 +641,33 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { }} className="[&>svg]:mr-1" > - + {t('sessions.sidebar.session.menu.rename')} togglePinnedSession(session.id)} className="[&>svg]:mr-1"> - {isPinnedSession ? : } + {isPinnedSession ? : } {isPinnedSession ? t('sessions.sidebar.session.menu.unpin') : t('sessions.sidebar.session.menu.pin')} {!resolvedSession.share ? ( handleShareSession(resolvedSession)} className="[&>svg]:mr-1"> - + {t('sessions.sidebar.session.menu.share')} ) : ( <> { if (resolvedSession.share?.url) handleCopyShareUrl(resolvedSession.share.url, session.id); }} className="[&>svg]:mr-1"> {copiedSessionId === session.id - ? <>{t('sessions.sidebar.session.menu.copied')} - : <>{t('sessions.sidebar.session.menu.copyLink')}} + ? <>{t('sessions.sidebar.session.menu.copied')} + : <>{t('sessions.sidebar.session.menu.copyLink')}} handleUnshareSession(session.id)} className="[&>svg]:mr-1"> - + {t('sessions.sidebar.session.menu.unshare')} )} { void handleExportSession(); }} className="[&>svg]:mr-1"> - + {t('sessions.sidebar.session.menu.exportMarkdown')} @@ -700,7 +678,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { <> - {t('sessions.sidebar.folders.moveToFolder')} + {t('sessions.sidebar.folders.moveToFolder')} {scopeFolders.length === 0 ? ( {t('sessions.sidebar.folders.none')} @@ -708,18 +686,18 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { scopeFolders.map((folder) => ( { if (currentFolderId === folder.id) removeSessionFromFolder(sessionDirectory, session.id); else addSessionToFolder(sessionDirectory, folder.id, session.id); }}> {folder.name} - {currentFolderId === folder.id ? : null} + {currentFolderId === folder.id ? : null} )) )} { const newFolder = createFolderAndStartRename(sessionDirectory); if (!newFolder) return; addSessionToFolder(sessionDirectory, newFolder.id, session.id); }}> - + {t('sessions.sidebar.folders.newFolderEllipsis')} {currentFolderId ? ( { removeSessionFromFolder(sessionDirectory, session.id); }} className="text-destructive focus:text-destructive"> - + {t('sessions.sidebar.folders.removeFromFolder')} ) : null} @@ -742,7 +720,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { }} className="[&>svg]:mr-1" > - + {t('sessions.sidebar.session.menu.openInSidePanel')} {t('sessions.sidebar.session.menu.betaBadge')} @@ -754,14 +732,14 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { onClick={handleOpenMiniChatWindow} className="[&>svg]:mr-1" > - + {t('sessions.sidebar.session.menu.openMiniChatWindow')} ) : null} handleDeleteSession(session, { archivedBucket })}> - + {archivedBucket ? t('sessions.sidebar.bulkActions.delete') : t('sessions.sidebar.bulkActions.archive')} @@ -824,7 +802,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { ) : null} {pendingPermissionCount > 0 ? ( - + {pendingPermissionCount} ) : null} @@ -841,7 +819,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
{secondaryMeta?.branchLabel ? (
- {secondaryMeta.branchLabel} + {secondaryMeta.branchLabel}
) : null} {sessionDiffStats ? +{sessionDiffStats.additions}-{sessionDiffStats.deletions} : null} @@ -875,7 +853,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
{renderHighlightedText(sessionTitle, normalizedSessionSearchQuery)}
{pendingPermissionCount > 0 ? ( - + {pendingPermissionCount} ) : null} @@ -887,7 +865,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { {sessionUpdatedLabel} {sessionDiffStats ? +{sessionDiffStats.additions}/-{sessionDiffStats.deletions} : null} {hasSecondaryProjectLabel ? {secondaryMeta?.projectLabel} : null} - {hasSecondaryBranchLabel ? {secondaryMeta?.branchLabel} : null} + {hasSecondaryBranchLabel ? {secondaryMeta?.branchLabel} : null}
) : null} @@ -924,7 +902,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { onClick={handleQuickArchiveClick} onKeyDown={(event) => event.stopPropagation()} > - + @@ -950,7 +928,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { onClick={handleMenuTriggerClick} onKeyDown={(event) => event.stopPropagation()} > - + {sessionMenuContent} diff --git a/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx b/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx index 89f6728d..a6a8f343 100644 --- a/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import type { SessionNode } from './types'; import { useI18n } from '@/lib/i18n'; +import { Icon } from "@/components/icon/Icon"; type ActivityItem = { node: SessionNode; @@ -77,7 +77,7 @@ export function SidebarActivitySections({ sections, renderSessionNode }: Props): aria-expanded={!isCollapsed} > - {isCollapsed ? : } + {isCollapsed ? : } {section.title} diff --git a/packages/ui/src/components/session/sidebar/SidebarFooter.tsx b/packages/ui/src/components/session/sidebar/SidebarFooter.tsx index a312cbe8..d49fdbac 100644 --- a/packages/ui/src/components/session/sidebar/SidebarFooter.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarFooter.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { RiInformationLine, RiQuestionLine, RiSettings3Line } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; type Props = { @@ -32,7 +32,7 @@ export function SidebarFooter({

{t('sessions.sidebar.footer.actions.settings')}

@@ -40,7 +40,7 @@ export function SidebarFooter({

{t('sessions.sidebar.footer.actions.shortcuts')}

@@ -48,7 +48,7 @@ export function SidebarFooter({

{t('sessions.sidebar.footer.actions.aboutOpenChamber')}

diff --git a/packages/ui/src/components/session/sidebar/SidebarHeader.tsx b/packages/ui/src/components/session/sidebar/SidebarHeader.tsx index 5f0081c0..db747d05 100644 --- a/packages/ui/src/components/session/sidebar/SidebarHeader.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarHeader.tsx @@ -7,21 +7,9 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { - RiCheckLine, - RiCheckboxMultipleLine, - RiChatNewLine, - RiEqualizer2Line, - RiFolderAddLine, - RiLayoutLeftLine, - RiSearchLine, - RiCloseLine, - RiContractUpDownLine, - RiExpandUpDownLine, - RiCalendarScheduleLine, -} from '@remixicon/react'; import { cn } from '@/lib/utils'; import { ArrowsMerge } from '@/components/icons/ArrowsMerge'; +import { Icon } from "@/components/icon/Icon"; import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore'; import { useI18n } from '@/lib/i18n'; @@ -118,7 +106,7 @@ export function SidebarHeader(props: Props): React.ReactNode { className="inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50" aria-label={t('sessions.sidebar.header.actions.closeSessions')} > - +

{t('sessions.sidebar.header.actions.closeSessions')}

@@ -132,7 +120,7 @@ export function SidebarHeader(props: Props): React.ReactNode { className={headerActionButtonClass} aria-label={t('sessions.sidebar.header.actions.addProject')} > - +

{t('sessions.sidebar.header.actions.addProject')}

@@ -145,7 +133,7 @@ export function SidebarHeader(props: Props): React.ReactNode { className={headerActionButtonClass} aria-label={t('sessions.sidebar.header.actions.newSession')} > - +

{t('sessions.sidebar.header.actions.newSession')}

@@ -176,7 +164,7 @@ export function SidebarHeader(props: Props): React.ReactNode { className={headerActionButtonClass} aria-label={t('sessions.sidebar.header.actions.scheduledTasks')} > - +

{t('sessions.sidebar.header.actions.scheduledTasks')}

@@ -191,7 +179,7 @@ export function SidebarHeader(props: Props): React.ReactNode { aria-label={t('sessions.sidebar.header.actions.searchSessions')} aria-expanded={isSessionSearchOpen} > - +

{t('sessions.sidebar.header.actions.searchSessions')}

@@ -208,7 +196,7 @@ export function SidebarHeader(props: Props): React.ReactNode { : t('sessions.sidebar.header.actions.selectSessions')} aria-pressed={selectionModeEnabled} > - + @@ -227,7 +215,7 @@ export function SidebarHeader(props: Props): React.ReactNode { className={headerActionButtonClass} aria-label={t('sessions.sidebar.header.actions.sessionDisplayMode')} > - + @@ -239,14 +227,14 @@ export function SidebarHeader(props: Props): React.ReactNode { className="flex items-center justify-between" > {t('sessions.sidebar.header.displayMode.default')} - {displayMode === 'default' ? : null} + {displayMode === 'default' ? : null} setDisplayMode('minimal')} className="flex items-center justify-between" > {t('sessions.sidebar.header.displayMode.minimal')} - {displayMode === 'minimal' ? : null} + {displayMode === 'minimal' ? : null} {t('sessions.sidebar.header.displayMode.showRecent')} - {showRecentSection ? : null} + {showRecentSection ? : null} - + {t('sessions.sidebar.header.displayMode.collapseAll')} - + {t('sessions.sidebar.header.displayMode.expandAll')} @@ -281,7 +269,7 @@ export function SidebarHeader(props: Props): React.ReactNode { {t('sessions.sidebar.header.search.escapeHint')}
- + - + ) : null}
diff --git a/packages/ui/src/components/session/sidebar/sessionFolderDnd.tsx b/packages/ui/src/components/session/sidebar/sessionFolderDnd.tsx index 5aacba24..bff8d5d8 100644 --- a/packages/ui/src/components/session/sidebar/sessionFolderDnd.tsx +++ b/packages/ui/src/components/session/sidebar/sessionFolderDnd.tsx @@ -10,7 +10,7 @@ import { useDroppable, type DragEndEvent, } from '@dnd-kit/core'; -import { RiStickyNoteLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; export const DraggableSessionRow: React.FC<{ sessionId: string; @@ -122,7 +122,7 @@ export const SessionFolderDndScope: React.FC<{ }} className="flex items-center rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-2.5 py-1 shadow-none pointer-events-none" > - +
{activeDragTitle}
diff --git a/packages/ui/src/components/session/sidebar/sortableItems.tsx b/packages/ui/src/components/session/sidebar/sortableItems.tsx index 45f0f5bd..0a970c34 100644 --- a/packages/ui/src/components/session/sidebar/sortableItems.tsx +++ b/packages/ui/src/components/session/sidebar/sortableItems.tsx @@ -8,16 +8,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; -import { - RiAddLine, - RiArrowDownSLine, - RiArrowRightSLine, - RiCloseLine, - RiFolderLine, - RiMore2Line, - RiNodeTree, - RiPencilAiLine, -} from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, getProjectIconImageUrl } from '@/lib/projectMeta'; import { useThemeSystem } from '@/contexts/useThemeSystem'; @@ -104,7 +95,7 @@ export const SortableProjectItem: React.FC = ({ setImageFailed(false); }, [id, projectIconImage?.updatedAt]); - const ProjectIcon = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null; + const projectIconName = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null; const iconColor = projectColor ? (PROJECT_COLOR_MAP[projectColor] ?? null) : null; const imageUrl = !imageFailed ? getProjectIconImageUrl({ id, iconImage: projectIconImage }, { @@ -186,7 +177,7 @@ export const SortableProjectItem: React.FC = ({ 'h-3.5 w-3.5 items-center justify-center text-muted-foreground', alwaysShowActions ? 'inline-flex' : 'hidden group-hover/project:inline-flex group-focus-within/project:inline-flex', )}> - {isCollapsed ? : } + {isCollapsed ? : } {imageUrl ? ( = ({ onError={() => setImageFailed(true)} /> - ) : ProjectIcon ? ( - + ) : projectIconName ? ( + ) : ( - + )} = ({ )} aria-label={t('sessions.sidebar.project.actions.newWorktree')} > - + @@ -271,25 +262,25 @@ export const SortableProjectItem: React.FC = ({ onMouseDown={handleMenuTriggerMouseDown} onClick={handleMenuTriggerClick} > - + {showCreateButtons && !isRepo && !hideDirectoryControls && onNewSession && ( - + {t('sessions.sidebar.project.actions.newSession')} )} - + {t('sessions.sidebar.session.menu.rename')} - + {t('sessions.sidebar.project.actions.closeProject')} @@ -314,7 +305,7 @@ export const SortableProjectItem: React.FC = ({ ? t('sessions.sidebar.project.actions.newDraftSession') : t('sessions.sidebar.project.actions.newSession')} > - + diff --git a/packages/ui/src/components/ui/AboutDialog.tsx b/packages/ui/src/components/ui/AboutDialog.tsx index dc41cd87..4b4a3781 100644 --- a/packages/ui/src/components/ui/AboutDialog.tsx +++ b/packages/ui/src/components/ui/AboutDialog.tsx @@ -4,10 +4,10 @@ import { DialogContent, } from '@/components/ui/dialog'; import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; -import { RiDiscordFill, RiGithubFill, RiTwitterXFill } from '@remixicon/react'; import { debugUtils } from '@/lib/debug'; import { cn } from '@/lib/utils'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; import { getDesktopAppVersion } from '@/lib/desktopNative'; @@ -153,7 +153,7 @@ export const AboutDialog: React.FC = ({ rel="noopener noreferrer" className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors" > - + GitHub = ({ rel="noopener noreferrer" className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors" > - + Discord = ({ rel="noopener noreferrer" className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors" > - + @btriapitsyn
diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx index 47beb26d..bb6551dc 100644 --- a/packages/ui/src/components/ui/CommandPalette.tsx +++ b/packages/ui/src/components/ui/CommandPalette.tsx @@ -28,24 +28,13 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard'; import { toast } from '@/components/ui'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; -import { - RiAddLine, - RiChatAi3Line, - RiFolderAddLine, - RiGitBranchLine, - RiLayoutLeftLine, - RiLayoutRightLine, - RiPieChartLine, - RiWindowLine, - RiSettings3Line, - RiTerminalBoxLine, -} from '@remixicon/react'; import type { Session } from '@opencode-ai/sdk/v2'; import { createWorktreeSession } from '@/lib/worktreeSessionCreator'; import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { canUseElectronDesktopIPC, invokeDesktop, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; import { SETTINGS_PAGE_METADATA, type SettingsRuntimeContext } from '@/lib/settings/metadata'; import { getSettingsNavIcon } from '@/components/views/SettingsView'; +import { Icon } from "@/components/icon/Icon"; import { scoreByFuzzyQuery } from '@/lib/search/fuzzySearch'; import { truncatePathMiddle } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; @@ -154,7 +143,7 @@ export const CommandPalette: React.FC = () => { { id: 'new-session', title: t('commandPalette.item.newSession'), - icon: , + icon: , shortcutId: 'new_chat', searchText: t('commandPalette.item.newSession'), onSelect: run(() => { @@ -166,7 +155,7 @@ export const CommandPalette: React.FC = () => { { id: 'new-worktree', title: t('commandPalette.item.newWorktreeDraft'), - icon: , + icon: , shortcutId: 'new_chat_worktree', searchText: t('commandPalette.item.newWorktreeDraft'), onSelect: run(() => { @@ -176,7 +165,7 @@ export const CommandPalette: React.FC = () => { { id: 'add-project', title: t('commandPalette.item.addProject'), - icon: , + icon: , searchText: t('commandPalette.item.addProject'), onSelect: run(() => { sessionEvents.requestDirectoryDialog(); @@ -187,7 +176,7 @@ export const CommandPalette: React.FC = () => { title: isMobile ? t('commandPalette.item.showSessionSwitcher') : t('commandPalette.item.toggleSidebar'), - icon: , + icon: , shortcutId: 'toggle_sidebar', searchText: isMobile ? t('commandPalette.item.showSessionSwitcher') @@ -204,7 +193,7 @@ export const CommandPalette: React.FC = () => { { id: 'toggle-right-sidebar', title: t('commandPalette.item.toggleRightSidebar'), - icon: , + icon: , shortcutId: 'toggle_right_sidebar', searchText: t('commandPalette.item.toggleRightSidebar'), onSelect: run(() => toggleRightSidebar()), @@ -212,7 +201,7 @@ export const CommandPalette: React.FC = () => { { id: 'toggle-terminal', title: t('commandPalette.item.toggleTerminal'), - icon: , + icon: , shortcutId: 'toggle_terminal', searchText: t('commandPalette.item.toggleTerminal'), onSelect: run(() => toggleBottomTerminal()), @@ -220,7 +209,7 @@ export const CommandPalette: React.FC = () => { { id: 'context-usage', title: t('commandPalette.item.showContextUsage'), - icon: , + icon: , searchText: t('commandPalette.item.showContextUsage'), onSelect: run(() => { if (currentDirectory) openContextOverview(currentDirectory); @@ -229,7 +218,7 @@ export const CommandPalette: React.FC = () => { { id: 'open-settings', title: t('commandPalette.item.openSettings'), - icon: , + icon: , shortcutId: 'open_settings', searchText: t('commandPalette.item.openSettings'), onSelect: run(() => setSettingsDialogOpen(true)), @@ -239,7 +228,7 @@ export const CommandPalette: React.FC = () => { list.splice(1, 0, { id: 'new-mini-chat', title: t('commandPalette.item.newMiniChat'), - icon: , + icon: , shortcutId: 'new_mini_chat', searchText: t('commandPalette.item.newMiniChat'), onSelect: run(() => { @@ -283,12 +272,12 @@ export const CommandPalette: React.FC = () => { .filter((p) => p.slug !== 'home') .filter((p) => (p.isAvailable ? p.isAvailable(settingsRuntimeCtx) : true)) .map((page) => { - const Icon = getSettingsNavIcon(page.slug) ?? RiSettings3Line; + const iconName = getSettingsNavIcon(page.slug) ?? 'settings-3'; const keywords = (page.keywords ?? []).join(' '); return { id: `settings:${page.slug}`, title: page.title, - icon: , + icon: , searchText: `${page.title} ${page.group} ${keywords}`, onSelect: run(() => { setSettingsPage(page.slug); @@ -507,11 +496,11 @@ export const CommandPalette: React.FC = () => { value={`session:${session.id}`} onSelect={() => handleOpenSession(session)} > - + {title} {branch ? ( - + {branch} ) : null} diff --git a/packages/ui/src/components/ui/ContextUsageDisplay.tsx b/packages/ui/src/components/ui/ContextUsageDisplay.tsx index ac4117be..badfcdf2 100644 --- a/packages/ui/src/components/ui/ContextUsageDisplay.tsx +++ b/packages/ui/src/components/ui/ContextUsageDisplay.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { RiDonutChartFill, RiDonutChartLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface ContextUsageDisplayProps { @@ -69,11 +69,11 @@ export const ContextUsageDisplay: React.FC = ({ const contextContent = ( <> - {!isMobile && !hideIcon && } + {!isMobile && !hideIcon && } {showPercentIcon ? ( <> -
diff --git a/packages/ui/src/components/ui/JsonTreeViewer.tsx b/packages/ui/src/components/ui/JsonTreeViewer.tsx index e67f2c20..6e094a9d 100644 --- a/packages/ui/src/components/ui/JsonTreeViewer.tsx +++ b/packages/ui/src/components/ui/JsonTreeViewer.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { @@ -10,6 +9,7 @@ import { type JsonTreeNode, type FlatJsonNode, } from '@/lib/jsonTreeUtils'; +import { Icon } from "@/components/icon/Icon"; interface JsonTreeViewerProps { data: unknown; @@ -129,9 +129,9 @@ const JsonRow = React.memo( className="mr-1 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm hover:bg-[var(--interactive-hover)] text-muted-foreground" > {isExpanded ? ( - + ) : ( - + )} ) : ( diff --git a/packages/ui/src/components/ui/MemoryDebugPanel.tsx b/packages/ui/src/components/ui/MemoryDebugPanel.tsx index dbfe5422..89e2c449 100644 --- a/packages/ui/src/components/ui/MemoryDebugPanel.tsx +++ b/packages/ui/src/components/ui/MemoryDebugPanel.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RiBarChartBoxLine, RiCloseLine, RiDatabase2Line, RiFileCopyLine, RiPulseLine, RiRefreshLine } from '@remixicon/react'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useViewportStore } from '@/sync/viewport-store'; @@ -12,6 +11,7 @@ import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface DebugPanelProps { @@ -198,9 +198,9 @@ export const DebugPanel: React.FC = ({ onClose }) => {
{activeTab === 'memory' ? ( - + ) : ( - + )}

{t('memoryDebugPanel.title')}

@@ -208,7 +208,7 @@ export const DebugPanel: React.FC = ({ onClose }) => { {activeTab === 'streaming' ? ( <> ) : null} {onClose ? ( ) : null}
@@ -290,7 +290,7 @@ export const DebugPanel: React.FC = ({ onClose }) => { >
{stat.title} - {stat.isStreaming ? : null} + {stat.isStreaming ? : null} {stat.isZombie ? ! : null}
diff --git a/packages/ui/src/components/ui/MobileOverlayPanel.tsx b/packages/ui/src/components/ui/MobileOverlayPanel.tsx index bb75ea10..66cdc649 100644 --- a/packages/ui/src/components/ui/MobileOverlayPanel.tsx +++ b/packages/ui/src/components/ui/MobileOverlayPanel.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { createPortal } from 'react-dom'; -import { RiCloseLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import { ScrollableOverlay } from './ScrollableOverlay'; +import { Icon } from "@/components/icon/Icon"; interface MobileOverlayPanelProps { open: boolean; @@ -90,7 +90,7 @@ export const MobileOverlayPanel: React.FC = ({ onClick={onClose} className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-interactive-hover" > - + ); diff --git a/packages/ui/src/components/ui/UpdateDialog.tsx b/packages/ui/src/components/ui/UpdateDialog.tsx index c2a19400..44fe6ba6 100644 --- a/packages/ui/src/components/ui/UpdateDialog.tsx +++ b/packages/ui/src/components/ui/UpdateDialog.tsx @@ -6,7 +6,7 @@ import { } from '@/components/ui/dialog'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer'; -import { RiCheckLine, RiClipboardLine, RiDownloadCloudLine, RiDownloadLine, RiExternalLinkLine, RiLoaderLine, RiRestartLine, RiTerminalLine } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import type { UpdateInfo, UpdateProgress } from '@/lib/desktop'; import { copyTextToClipboard } from '@/lib/clipboard'; @@ -109,7 +109,6 @@ function parseChangelogSections(body: string): ChangelogSection[] { }); } - type InstallWebUpdateResult = { success: boolean; error?: string; @@ -305,7 +304,7 @@ export const UpdateDialog: React.FC = ({ {/* Header Section */}
- + {webUpdateState === 'restarting' || webUpdateState === 'reconnecting' ? t('updateDialog.header.updating') @@ -336,7 +335,7 @@ export const UpdateDialog: React.FC = ({ {isWebRuntime && isWebUpdating && (
- +
{webUpdateState === 'updating' && t('updateDialog.status.installingUpdate')} {webUpdateState === 'restarting' && t('updateDialog.status.serverRestarting')} @@ -409,7 +408,7 @@ export const UpdateDialog: React.FC = ({ {isWebRuntime && webUpdateState === 'error' && (
- + {t('updateDialog.fallback.updateViaTerminal')}
@@ -427,9 +426,9 @@ export const UpdateDialog: React.FC = ({ title={copied ? t('updateDialog.actions.copied') : t('updateDialog.actions.copyCommand')} > {copied ? ( - + ) : ( - + )}
@@ -468,7 +467,7 @@ export const UpdateDialog: React.FC = ({ rel="noopener noreferrer" className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors shrink-0" > - + GitHub @@ -479,7 +478,7 @@ export const UpdateDialog: React.FC = ({ onClick={onDownload} className="flex items-center justify-center gap-2 px-5 py-2 rounded-md text-sm font-medium bg-[var(--primary-base)] text-[var(--primary-foreground)] hover:opacity-90 transition-opacity" > - + {t('updateDialog.actions.downloadUpdate')} )} @@ -489,7 +488,7 @@ export const UpdateDialog: React.FC = ({ disabled className="flex items-center justify-center gap-2 px-5 py-2 rounded-md text-sm font-medium bg-[var(--primary-base)]/50 text-[var(--primary-foreground)] cursor-not-allowed" > - + {t('updateDialog.status.downloading')} )} @@ -499,7 +498,7 @@ export const UpdateDialog: React.FC = ({ onClick={onRestart} className="flex items-center justify-center gap-2 px-5 py-2 rounded-md text-sm font-medium bg-[var(--status-success)] text-white hover:opacity-90 transition-opacity" > - + {t('updateDialog.actions.restartToUpdate')} )} @@ -510,7 +509,7 @@ export const UpdateDialog: React.FC = ({ onClick={handleWebUpdate} className="flex items-center justify-center gap-2 px-5 py-2 rounded-md text-sm font-medium bg-[var(--primary-base)] text-[var(--primary-foreground)] hover:opacity-90 transition-opacity" > - + {t('updateDialog.actions.updateNow')} )} @@ -520,7 +519,7 @@ export const UpdateDialog: React.FC = ({ disabled className="flex items-center justify-center gap-2 px-5 py-2 rounded-md text-sm font-medium bg-[var(--primary-base)]/50 text-[var(--primary-foreground)] cursor-not-allowed" > - + {t('updateDialog.status.updating')} )} diff --git a/packages/ui/src/components/ui/checkbox.tsx b/packages/ui/src/components/ui/checkbox.tsx index 4e207bab..8b849868 100644 --- a/packages/ui/src/components/ui/checkbox.tsx +++ b/packages/ui/src/components/ui/checkbox.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox'; -import { RiCheckLine, RiSubtractLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; +import { Icon } from "@/components/icon/Icon"; interface CheckboxProps { checked: boolean; @@ -15,7 +15,6 @@ interface CheckboxProps { size?: 'sm' | 'default'; } - export const Checkbox = React.memo(function Checkbox({ checked, onChange, @@ -62,9 +61,9 @@ export const Checkbox = React.memo(function Checkbox({ )} > {indeterminate ? ( - + ) : ( - + )} diff --git a/packages/ui/src/components/ui/command.tsx b/packages/ui/src/components/ui/command.tsx index 1ede73e0..ba3c63ff 100644 --- a/packages/ui/src/components/ui/command.tsx +++ b/packages/ui/src/components/ui/command.tsx @@ -2,10 +2,10 @@ import * as React from "react" import { Command as CommandPrimitive } from "cmdk" -import { RiCommandLine, RiArrowUpLine, RiSearchLine } from "@remixicon/react"; import { cn } from "@/lib/utils" import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay"; +import { Icon } from "@/components/icon/Icon"; import { Dialog, DialogContent, @@ -79,7 +79,7 @@ const CommandInput = React.forwardRef< data-slot="command-input-wrapper" className="flex h-8 items-center gap-2 border-b px-3" > - + ; + return ; } if (normalized === 'shift' || normalized === '⇧') { - return ; + return ; } return ( diff --git a/packages/ui/src/components/ui/dialog.tsx b/packages/ui/src/components/ui/dialog.tsx index 906783c9..06bb166a 100644 --- a/packages/ui/src/components/ui/dialog.tsx +++ b/packages/ui/src/components/ui/dialog.tsx @@ -1,9 +1,9 @@ import * as React from "react" import { Dialog as BaseDialog } from "@base-ui/react/dialog" -import { RiCloseLine } from '@remixicon/react'; import { cn } from "@/lib/utils" import { useI18n } from '@/lib/i18n' +import { Icon } from "@/components/icon/Icon"; let openDialogCount = 0; @@ -122,7 +122,7 @@ function DialogContent({ data-slot="dialog-close" className="ring-offset-background focus:ring-ring data-[open]:bg-interactive-active data-[open]:text-foreground absolute top-2 right-2 rounded-lg opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none text-muted-foreground hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" > - + {t('dialog.common.actions.close')} )} diff --git a/packages/ui/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx index 25d7e15a..0125e0cc 100644 --- a/packages/ui/src/components/ui/dropdown-menu.tsx +++ b/packages/ui/src/components/ui/dropdown-menu.tsx @@ -1,8 +1,8 @@ import * as React from "react" import { Menu as BaseMenu } from "@base-ui/react/menu" -import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react'; import { cn } from "@/lib/utils" +import { Icon } from "@/components/icon/Icon"; type AsChildProps = { asChild?: boolean }; type AsChildRenderProps = { @@ -203,7 +203,7 @@ function DropdownMenuCheckboxItem({ > - + {children} @@ -233,7 +233,7 @@ function DropdownMenuRadioItem({ > - + {children} @@ -315,7 +315,7 @@ function DropdownMenuSubTrigger({ {...props} > {children} - + ) } diff --git a/packages/ui/src/components/ui/number-input.tsx b/packages/ui/src/components/ui/number-input.tsx index c10955bd..07b2a8d3 100644 --- a/packages/ui/src/components/ui/number-input.tsx +++ b/packages/ui/src/components/ui/number-input.tsx @@ -1,9 +1,9 @@ import * as React from "react" -import { RiAddLine, RiSubtractLine } from "@remixicon/react" import { useDeviceInfo } from "@/lib/device" import { useI18n } from "@/lib/i18n" import { cn } from "@/lib/utils" +import { Icon } from "@/components/icon/Icon"; export interface NumberInputProps extends Omit, "value" | "onChange" | "type"> { @@ -224,7 +224,7 @@ const NumberInput = React.forwardRef( !decrementDisabled && "active:bg-interactive-hover" )} > - +
( !incrementDisabled && "active:bg-interactive-hover" )} > - +
) @@ -276,7 +276,7 @@ const NumberInput = React.forwardRef( "disabled:pointer-events-none disabled:opacity-50" )} > - + ( "disabled:pointer-events-none disabled:opacity-50" )} > - +
) diff --git a/packages/ui/src/components/ui/select.tsx b/packages/ui/src/components/ui/select.tsx index cfcfaa40..fb8eec4e 100644 --- a/packages/ui/src/components/ui/select.tsx +++ b/packages/ui/src/components/ui/select.tsx @@ -3,10 +3,10 @@ import * as React from "react" import { Select as BaseSelect } from "@base-ui/react/select" import type { SelectRootChangeEventDetails } from "@base-ui/react/select"; -import { RiArrowDownSLine, RiArrowUpSLine, RiCheckLine } from '@remixicon/react'; import { cn } from "@/lib/utils" import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay"; +import { Icon } from "@/components/icon/Icon"; type AsChildProps = { asChild?: boolean }; type AsChildRenderProps = { @@ -145,7 +145,7 @@ function SelectTrigger({ {asChildRender ? undefined : (<> {children} - + )} @@ -249,7 +249,7 @@ function SelectItem({ > - + {children} @@ -283,7 +283,7 @@ function SelectScrollUpButton({ )} {...props} > - + ) } @@ -301,7 +301,7 @@ function SelectScrollDownButton({ )} {...props} > - + ) } diff --git a/packages/ui/src/components/ui/sortable-tabs-strip.tsx b/packages/ui/src/components/ui/sortable-tabs-strip.tsx index 091da0cc..1ee67178 100644 --- a/packages/ui/src/components/ui/sortable-tabs-strip.tsx +++ b/packages/ui/src/components/ui/sortable-tabs-strip.tsx @@ -15,12 +15,12 @@ import { useSortable, } from '@dnd-kit/sortable'; import { CSS as DndCSS } from '@dnd-kit/utilities'; -import { RiCloseLine } from '@remixicon/react'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/useUIStore'; import { useDeviceInfo } from '@/lib/device'; +import { Icon } from "@/components/icon/Icon"; export type SortableTabsStripItem = { id: string; @@ -122,7 +122,6 @@ export const SortableTabsStrip: React.FC = ({ const tabRefs = React.useRef>(new Map()); const [pillRect, setPillRect] = React.useState<{ left: number; top: number; width: number; height: number } | null>(null); - const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), ); @@ -256,8 +255,6 @@ export const SortableTabsStrip: React.FC = ({ updateActivePillRect(); }); - - React.useEffect(() => { if (!isScrollable || !activeId) { return; @@ -457,7 +454,7 @@ export const SortableTabsStrip: React.FC = ({ aria-label={item.closeLabel ?? `Close ${item.label} tab`} title={item.closeLabel ?? `Close ${item.label} tab`} > - + ) : null} @@ -489,7 +486,7 @@ export const SortableTabsStrip: React.FC = ({ aria-label={item.closeLabel ?? `Close ${item.label} tab`} title={item.closeLabel ?? `Close ${item.label} tab`} > - + ) : null} @@ -524,7 +521,7 @@ export const SortableTabsStrip: React.FC = ({ aria-label={item.closeLabel ?? `Close ${item.label} tab`} title={item.closeLabel ?? `Close ${item.label} tab`} > - + ) : null}
diff --git a/packages/ui/src/components/views/DiffView.tsx b/packages/ui/src/components/views/DiffView.tsx index a606f571..96e05bc6 100644 --- a/packages/ui/src/components/views/DiffView.tsx +++ b/packages/ui/src/components/views/DiffView.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RiArrowDownSLine, RiArrowRightSLine, RiEditLine, RiGitCommitLine, RiLoader4Line, RiTextWrap } from '@remixicon/react'; import { useUIStore } from '@/stores/useUIStore'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; @@ -26,6 +25,7 @@ import type { DiffViewMode } from '@/components/chat/message/types'; import { PierreDiffViewer } from './PierreDiffViewer'; import { useDeviceInfo } from '@/lib/device'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; +import { Icon } from "@/components/icon/Icon"; import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard'; import { sessionEvents } from '@/lib/sessionEvents'; import { useI18n } from '@/lib/i18n'; @@ -232,7 +232,7 @@ const FileSelector = React.memo(({ ) : ( {t('diffView.selector.selectFile')} )} - + @@ -297,7 +297,7 @@ const DiffViewModeSelector = React.memo(({ mode, onMo {t(currentOption.labelKey)} - + @@ -788,9 +788,9 @@ const MultiFileDiffEntry = React.memo(({
{isExpanded ? ( - + ) : ( - + )} (({ disabled={isOpeningInEditor} > {isOpeningInEditor ? ( - + ) : ( - + )} ) : null} @@ -895,7 +895,7 @@ const MultiFileDiffEntry = React.memo(({ ) : null} {isLoading && !diffData && !diffLoadError ? (
- + Loading diff…
) : null} @@ -987,7 +987,6 @@ export const DiffView: React.FC = ({ const pendingScrollFrameRef = React.useRef(null); const shouldPinAfterAlignRef = React.useRef(false); - React.useEffect(() => { if (!pinSelectedFileHeaderToTopOnNavigate || !isStackedView || !pinnedStackedTarget) { return; @@ -1630,7 +1629,7 @@ export const DiffView: React.FC = ({ if (isLoadingStatus && !status) { return (
- + {t('diffView.state.loadingRepositoryStatus')}
); @@ -1682,7 +1681,7 @@ export const DiffView: React.FC = ({
) : ( <> - + {t('diffView.state.loadingDiff')} )} @@ -1697,7 +1696,7 @@ export const DiffView: React.FC = ({
{!isMobile && (
- + {isLoadingStatus && !status ? t('diffView.state.loadingChanges') @@ -1734,7 +1733,7 @@ export const DiffView: React.FC = ({ )} title={diffWrapLines ? t('diffView.actions.disableLineWrap') : t('diffView.actions.enableLineWrap')} > - + )} {showOpenInEditorAction && selectedFileEntry && !isStackedView && ( @@ -1749,9 +1748,9 @@ export const DiffView: React.FC = ({ title={t('diffView.actions.openFileAtFirstChangedLine')} > {isOpeningSelectedInEditor ? ( - + ) : ( - + )} )} diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 2ce512e9..8d153bc1 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -1,36 +1,5 @@ import React from 'react'; -import { - RiArrowLeftSLine, - RiArrowDownSLine, - RiClipboardLine, - RiCloseLine, - RiFileCopy2Line, - RiCheckLine, - RiFolder3Fill, - RiFolderOpenFill, - RiFolderReceivedLine, - RiFileCheckFill, - RiFileCheckLine, - RiFullscreenExitLine, - RiFullscreenLine, - RiLoader4Line, - RiRefreshLine, - RiSearchLine, - RiSave3Line, - RiTextWrap, - RiMore2Fill, - RiFileAddLine, - RiFolderAddLine, - RiDeleteBinLine, - RiEditLine, - RiFileCopyLine, - RiFileTransferLine, - RiCodeSSlashLine, - RiNodeTree, - RiDownloadLine, - RiMenuFold2Line, -} from '@remixicon/react'; import { toast } from '@/components/ui'; import { copyTextToClipboard } from '@/lib/clipboard'; @@ -81,6 +50,7 @@ import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; +import { Icon } from "@/components/icon/Icon"; import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry'; import { getDefaultTheme } from '@/lib/theme/themes'; import { openDesktopFileInApp, openDesktopPath } from '@/lib/desktop'; @@ -388,9 +358,9 @@ const FileRow: React.FC = ({ > {isDir ? ( isExpanded ? ( - + ) : ( - + ) ) : ( getFileIcon(node.path, node.extension) @@ -425,13 +395,13 @@ const FileRow: React.FC = ({ className="h-6 w-6" onClick={handleMenuButtonClick} > - + setContextMenuPath(null)}> {canRename && ( { e.stopPropagation(); onOpenDialog('rename', node); }}> - {t('sidebarFilesTree.menu.rename')} + {t('sidebarFilesTree.menu.rename')} )} { @@ -444,7 +414,7 @@ const FileRow: React.FC = ({ toast.error(t('sidebarFilesTree.toast.copyFailed')); }); }}> - {t('sidebarFilesTree.menu.copyPath')} + {t('sidebarFilesTree.menu.copyPath')} { e.stopPropagation(); @@ -457,19 +427,19 @@ const FileRow: React.FC = ({ toast.error(t('sidebarFilesTree.toast.copyFailed')); }); }}> - {t('filesView.tree.menu.copyRelativePath')} + {t('filesView.tree.menu.copyRelativePath')} {!isDir && downloadFile && ( { e.stopPropagation(); void downloadFile(node.path); }}> - {t('sidebarFilesTree.menu.save')} + {t('sidebarFilesTree.menu.save')} )} {canReveal && ( { e.stopPropagation(); onRevealPath(node.path); }}> - {t(getRevealLabelKey())} + {t(getRevealLabelKey())} )} {isDir && (canCreateFile || canCreateFolder) && ( @@ -477,12 +447,12 @@ const FileRow: React.FC = ({ {canCreateFile && ( { e.stopPropagation(); onOpenDialog('createFile', node); }}> - {t('sidebarFilesTree.menu.newFile')} + {t('sidebarFilesTree.menu.newFile')} )} {canCreateFolder && ( { e.stopPropagation(); onOpenDialog('createFolder', node); }}> - {t('sidebarFilesTree.menu.newFolder')} + {t('sidebarFilesTree.menu.newFolder')} )} @@ -494,7 +464,7 @@ const FileRow: React.FC = ({ onClick={(e) => { e.stopPropagation(); onOpenDialog('delete', node); }} className="text-destructive focus:text-destructive" > - {t('sidebarFilesTree.menu.delete')} + {t('sidebarFilesTree.menu.delete')} )} @@ -2418,9 +2388,6 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }).toString()}`)) : ''; - - - React.useEffect(() => { let cancelled = false; @@ -2507,7 +2474,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { onClick={() => void handleDialogSubmit()} disabled={isDialogSubmitting || (activeDialog !== 'delete' && !dialogInputValue.trim())} > - {isDialogSubmitting ? : ( + {isDialogSubmitting ? : ( activeDialog === 'delete' ? t('filesView.dialog.delete.confirm') : t('filesView.dialog.confirm') )} @@ -2572,12 +2539,12 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { <> {isSaving ? ( - + {t('filesView.editor.saving')} ) : autoSaveEnabled && autoSaveStatus === 'saved' && !isDirty ? ( - + {t('filesView.editor.saved')} ) : isDirty ? ( @@ -2589,7 +2556,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` })} aria-label={t('filesView.editor.saveAria', { shortcut: `${getModifierLabel()}+S` })} > - + ) : null} )} @@ -2617,7 +2584,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { title={t('filesView.editor.openInDesktopApp')} aria-label={t('filesView.editor.openInDesktopApp')} > - + @@ -2636,7 +2603,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { className="flex items-center gap-2" onClick={() => void loadOpenInApps(true)} > - + {t('filesView.editor.refreshApps')} ) : null} @@ -2655,7 +2622,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { )} title={wrapLines ? t('filesView.editor.disableLineWrap') : t('filesView.editor.enableLineWrap')} > - + {textViewMode === 'edit' && ( <> @@ -2669,7 +2636,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { className="h-6 w-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" title={t('filesView.editor.findInFile')} > - + = ({ mode = 'full' }) => { title={jsonViewMode === 'tree' ? t('filesView.editor.switchToTextView') : t('filesView.editor.switchToTreeView')} > {jsonViewMode === 'tree' ? ( - + ) : ( - + )} )} @@ -2755,9 +2722,9 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { aria-label={t('filesView.editor.copyFileContents')} > {copiedContent ? ( - + ) : ( - + )} )} @@ -2785,9 +2752,9 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { aria-label={t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath })} > {copiedPath ? ( - + ) : ( - + )} )} @@ -2804,7 +2771,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { title={t('filesView.editor.saveFile')} aria-label={t('filesView.editor.saveFile')} > - + )} @@ -2817,7 +2784,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { title={t('filesView.editor.exitFullscreen')} aria-label={t('filesView.editor.exitFullscreen')} > - + ) : (!isMobile && mode === 'full' && ( ))} @@ -2880,7 +2847,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { aria-label={t('filesView.editor.back')} className="inline-flex h-7 w-7 flex-shrink-0 items-center justify-center mr-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" > - + )} @@ -2895,7 +2862,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { > {selectedFile.name} - + @@ -2938,7 +2905,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { className="inline-flex h-6 w-6 items-center justify-center rounded-md text-[var(--surface-muted-foreground)] hover:text-[var(--surface-foreground)]" aria-label={t('filesView.editor.closeFileAria', { name: file.name })} > - + ); @@ -2999,7 +2966,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { )} aria-label={t('filesView.editor.closeFileAria', { name: file.name })} > - +
); @@ -3037,7 +3004,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { aria-label={t('filesView.editor.showControlsAria')} title={t('filesView.editor.controlsTitle')} > - + )}
@@ -3050,7 +3017,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { ?
: (
- + {t('filesView.state.loading')}
) @@ -3228,7 +3195,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { {shouldMaskEditorForPendingNavigation && (
- + {t('filesView.state.openingFileAtChange')}
@@ -3250,7 +3217,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => {
- + = ({ mode = 'full' }) => { searchInputRef.current?.focus(); }} > - + )}
@@ -3279,7 +3246,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { className="h-8 w-8 p-0 flex-shrink-0" title={t('filesView.tree.actions.newFileTitle')} > - +
@@ -3300,7 +3267,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => {
    {searching ? (
  • - + {t('filesView.tree.search.searching')}
  • ) : searchResults.length > 0 ? ( @@ -3352,7 +3319,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { ?
    : (
    - + Loading…
    ) @@ -3417,7 +3384,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { {shouldMaskEditorForPendingNavigation && (
    - + {t('filesView.state.openingFileAtChange')}
    diff --git a/packages/ui/src/components/views/GitView.tsx b/packages/ui/src/components/views/GitView.tsx index 71458b4d..1a368b6a 100644 --- a/packages/ui/src/components/views/GitView.tsx +++ b/packages/ui/src/components/views/GitView.tsx @@ -19,13 +19,6 @@ import { } from '@/stores/useGitStore'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { ScrollShadow } from '@/components/ui/ScrollShadow'; -import { - RiGitBranchLine, - RiGitMergeLine, - RiGitCommitLine, - RiGitPullRequestLine, - RiLoader4Line, -} from '@remixicon/react'; import { toast } from '@/components/ui'; import { Dialog, @@ -43,6 +36,7 @@ import { CommandItem, CommandList, } from '@/components/ui/command'; +import { Icon } from "@/components/icon/Icon"; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useUIStore } from '@/stores/useUIStore'; @@ -84,7 +78,6 @@ const GIT_ACTION_TAB_STORAGE_KEY = 'oc.git.actionTab'; const isActionTab = (value: unknown): value is ActionTab => value === 'commit' || value === 'branch' || value === 'pr'; - type GitViewSnapshot = { directory?: string; selectedPaths: string[]; @@ -521,9 +514,9 @@ export const GitView: React.FC = () => { const [isHistoryDialogOpen, setIsHistoryDialogOpen] = React.useState(false); const actionTabItems = React.useMemo(() => [ - { id: 'commit', label: t('gitView.tabs.commit'), icon: }, - { id: 'branch', label: t('gitView.tabs.update'), icon: }, - { id: 'pr', label: t('gitView.tabs.pr'), icon: }, + { id: 'commit', label: t('gitView.tabs.commit'), icon: }, + { id: 'branch', label: t('gitView.tabs.update'), icon: }, + { id: 'pr', label: t('gitView.tabs.pr'), icon: }, ], [t]); const [actionTab, setActionTab] = React.useState(() => { if (typeof window === 'undefined') { @@ -884,7 +877,6 @@ export const GitView: React.FC = () => { }; }, [changeEntries, currentDirectory, git, prefetchDiffs, selectedPaths, visibleChangePaths]); - React.useEffect(() => { if (!status || changeEntries.length === 0) { setSelectedPaths(new Set()); @@ -2101,7 +2093,7 @@ export const GitView: React.FC = () => { return (
    - + {t('gitView.loading.checkingRepository')}
    @@ -2112,7 +2104,7 @@ export const GitView: React.FC = () => { if (shouldHideNotGitState) { return (
    - +

    {t('gitView.empty.worktreeSetupInProgress')}

    @@ -2125,7 +2117,7 @@ export const GitView: React.FC = () => { return (
    - +

    {t('gitView.empty.notGitRepository')}

    diff --git a/packages/ui/src/components/views/MultiRunWindow.tsx b/packages/ui/src/components/views/MultiRunWindow.tsx index 898c6894..47042cd7 100644 --- a/packages/ui/src/components/views/MultiRunWindow.tsx +++ b/packages/ui/src/components/views/MultiRunWindow.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Dialog } from '@base-ui/react/dialog'; -import { RiCloseLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import { MultiRunLauncher } from '@/components/multirun'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface MultiRunWindowProps { @@ -65,7 +65,7 @@ export const MultiRunWindow: React.FC = ({ aria-label={t('multiRun.window.actions.closeAria')} className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" > - +
    diff --git a/packages/ui/src/components/views/PlanView.tsx b/packages/ui/src/components/views/PlanView.tsx index 0dd0a3ce..0b3071e8 100644 --- a/packages/ui/src/components/views/PlanView.tsx +++ b/packages/ui/src/components/views/PlanView.tsx @@ -20,7 +20,6 @@ import { useThemeSystem } from '@/contexts/useThemeSystem'; import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator'; import { createFlexokiCodeMirrorTheme } from '@/lib/codemirror/flexokiTheme'; import { languageByExtension } from '@/lib/codemirror/languageByExtension'; -import { RiCheckLine, RiClipboardLine, RiCodeAiLine, RiLoopRightAiLine } from '@remixicon/react'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessions } from '@/sync/sync-context'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; @@ -39,6 +38,7 @@ import { generateBranchName } from '@/lib/git/branchNameGenerator'; import { parseProjectPlanMarkdown } from '@/lib/openchamberConfig'; import { createWorktreeSessionForNewBranch } from '@/lib/worktreeSessionCreator'; import { TodoSendDialog, type TodoSendExecution } from '@/components/session/TodoSendDialog'; +import { Icon } from "@/components/icon/Icon"; import { renderMagicPrompt } from '@/lib/magicPrompts'; import { useI18n } from '@/lib/i18n'; @@ -338,7 +338,6 @@ export const PlanView: React.FC = ({ targetPath = null }) => { }; }, [cancel, editingDraftId, isMobile, lineSelection]); - const editorExtensions = React.useMemo(() => { const extensions = [createFlexokiCodeMirrorTheme(currentTheme)]; const language = languageByExtension(resolvedPath || 'plan.md'); @@ -638,7 +637,7 @@ export const PlanView: React.FC = ({ targetPath = null }) => { aria-label={t('planView.actions.improvePlanAria')} disabled={!content.trim()} > - + @@ -667,7 +666,7 @@ export const PlanView: React.FC = ({ targetPath = null }) => { aria-label={t('planView.actions.implementPlanAria')} disabled={!content.trim()} > - + @@ -711,9 +710,9 @@ export const PlanView: React.FC = ({ targetPath = null }) => { aria-label={t('planView.actions.copyPlanContents')} > {copiedContent ? ( - + ) : ( - + )}
    diff --git a/packages/ui/src/components/views/PreviewToggleButton.tsx b/packages/ui/src/components/views/PreviewToggleButton.tsx index e42451da..77404afc 100644 --- a/packages/ui/src/components/views/PreviewToggleButton.tsx +++ b/packages/ui/src/components/views/PreviewToggleButton.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { RiEyeLine, RiEyeOffLine } from '@remixicon/react'; - import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; +import { Icon } from "@/components/icon/Icon"; export type PreviewToggleButtonProps = { /** Current mode - determines which icon is displayed */ @@ -37,9 +36,9 @@ export const PreviewToggleButton: React.FC = ({ className="h-5 w-5 p-0" > {isPreview ? ( -
    )} diff --git a/packages/ui/src/components/views/TerminalView.tsx b/packages/ui/src/components/views/TerminalView.tsx index 8ddb99c0..a9490f97 100644 --- a/packages/ui/src/components/views/TerminalView.tsx +++ b/packages/ui/src/components/views/TerminalView.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RiAddLine, RiArrowDownLine, RiArrowGoBackLine, RiArrowLeftLine, RiArrowRightLine, RiArrowUpLine, RiCloseLine, RiCommandLine, RiFullscreenExitLine, RiFullscreenLine, RiGlobalLine, RiTerminalLine } from '@remixicon/react'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useTerminalStore } from '@/stores/useTerminalStore'; @@ -14,6 +13,7 @@ import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/useUIStore'; import { Button } from '@/components/ui/button'; import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip'; +import { Icon } from "@/components/icon/Icon"; import { useDeviceInfo } from '@/lib/device'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { primeTerminalInputTransport } from '@/lib/terminalApi'; @@ -45,7 +45,6 @@ const MODIFIER_ARROW_SUFFIX: Record = { cmd: '3', }; - const STREAM_OPTIONS = { retry: { maxRetries: 3, @@ -142,8 +141,8 @@ export const TerminalView: React.FC = () => { const terminalTabItems = React.useMemo(() => { return (directoryTerminalState?.tabs ?? []).map((tab) => ({ icon: (() => { - const Icon = tab.iconKey ? PROJECT_ACTION_ICON_MAP[tab.iconKey as ProjectActionIconKey] ?? RiTerminalLine : RiTerminalLine; - return ; + const tabIconName = tab.iconKey ? PROJECT_ACTION_ICON_MAP[tab.iconKey as ProjectActionIconKey] ?? 'terminal' : 'terminal'; + return ; })(), id: tab.id, label: tab.label, @@ -649,7 +648,6 @@ export const TerminalView: React.FC = () => { [activeTabId, closeTab, disconnectStream, effectiveDirectory] ); - const handleViewportInput = React.useCallback( (data: string) => { if (!data || isReconnectPending) { @@ -958,7 +956,7 @@ export const TerminalView: React.FC = () => { onClick={() => handleMobileKeyPress('tab')} disabled={quickKeysDisabled} > - + {t('terminalView.quickKeys.tabAria')} @@ -1068,7 +1066,7 @@ export const TerminalView: React.FC = () => { onClick={handleCreateTab} title={t('terminalView.tabs.newTabTitle')} > - +
    @@ -1084,7 +1082,7 @@ export const TerminalView: React.FC = () => { }} title={t('terminalView.preview.openTitle')} > - + {t('terminalView.preview.open')} ) : null} @@ -1099,7 +1097,7 @@ export const TerminalView: React.FC = () => { title={isBottomTerminalExpanded ? t('terminalView.bottomDock.restoreTitle') : t('terminalView.bottomDock.expandTitle')} aria-label={isBottomTerminalExpanded ? t('terminalView.bottomDock.restoreAria') : t('terminalView.bottomDock.expandAria')} > - {isBottomTerminalExpanded ? : } + {isBottomTerminalExpanded ? : } ) : null} diff --git a/packages/ui/src/components/views/agent-manager/AgentGroupDetail.tsx b/packages/ui/src/components/views/agent-manager/AgentGroupDetail.tsx index d9e03e30..807da526 100644 --- a/packages/ui/src/components/views/agent-manager/AgentGroupDetail.tsx +++ b/packages/ui/src/components/views/agent-manager/AgentGroupDetail.tsx @@ -1,12 +1,4 @@ import React from 'react'; -import { - RiGitBranchLine, - RiArrowDownSLine, - RiCheckLine, - RiMore2Line, - RiFileCopyLine, - RiLoader4Line, -} from '@remixicon/react'; import { toast } from '@/components/ui'; import { Button } from '@/components/ui/button'; import { copyTextToClipboard } from '@/lib/clipboard'; @@ -31,6 +23,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface AgentGroupDetailProps { @@ -163,7 +156,7 @@ export const AgentGroupDetail: React.FC = ({

    {group.name}

    - {groupBusy && } + {groupBusy && }
    @@ -173,7 +166,7 @@ export const AgentGroupDetail: React.FC = ({ · - + {selectedSession?.worktreeMetadata?.label || selectedSession?.branch || t('agentManager.detail.header.noBranch')}
    @@ -209,7 +202,7 @@ export const AgentGroupDetail: React.FC = ({ )}
    - + @@ -237,13 +230,13 @@ export const AgentGroupDetail: React.FC = ({
    {session.branch && (
    - + {session.worktreeMetadata?.label || session.branch}
    )}
{selectedSession?.id === session.id && ( - + )} ))} @@ -254,7 +247,7 @@ export const AgentGroupDetail: React.FC = ({ @@ -278,7 +271,7 @@ export const AgentGroupDetail: React.FC = ({ }} disabled={!selectedSession?.path} > - + {t('agentManager.detail.actions.copyWorktreePath')} diff --git a/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx b/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx index e08758ba..31b766b8 100644 --- a/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx +++ b/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx @@ -1,15 +1,4 @@ import React from 'react'; -import { - RiAddCircleLine, - RiAddLine, - RiArrowDownSLine, - RiCloseLine, - RiFileImageLine, - RiFileLine, - RiGitBranchLine, - RiHourglassFill, - RiSendPlane2Line, -} from '@remixicon/react'; import { toast } from '@/components/ui'; import { cn } from '@/lib/utils'; import { Input } from '@/components/ui/input'; @@ -22,6 +11,7 @@ import { BranchSelector, useBranchOptions } from '@/components/multirun/BranchSe import { AgentSelector } from '@/components/multirun/AgentSelector'; import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete'; import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete'; +import { Icon } from "@/components/icon/Icon"; import { isIMECompositionEvent } from '@/lib/ime'; import { getWorktreeSetupCommands } from '@/lib/openchamberConfig'; import { useThemeSystem } from '@/contexts/useThemeSystem'; @@ -438,7 +428,7 @@ export const AgentManagerEmptyState: React.FC = ({ {/* Branch Selection */}
= ({ ) : null; })()}

- @@ -500,7 +490,7 @@ export const AgentManagerEmptyState: React.FC = ({ className="flex-shrink-0 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50" aria-label={t('agentManager.empty.setupCommands.removeCommandAria')} > - +
))} @@ -509,7 +499,7 @@ export const AgentManagerEmptyState: React.FC = ({ onClick={() => setSetupCommands([...setupCommands, ''])} className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors" > - + {t('agentManager.empty.setupCommands.addCommand')}
@@ -583,9 +573,9 @@ export const AgentManagerEmptyState: React.FC = ({ className="inline-flex items-center gap-1.5 px-2 py-1 bg-muted/30 border border-border/30 rounded-md typography-meta" > {file.mimeType.startsWith('image/') ? ( - + ) : ( - + )} {file.filename} @@ -595,7 +585,7 @@ export const AgentManagerEmptyState: React.FC = ({ onClick={() => handleRemoveFile(file.id)} className="text-muted-foreground hover:text-destructive ml-0.5" > - +
))} @@ -620,7 +610,7 @@ export const AgentManagerEmptyState: React.FC = ({ className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground transition-colors" aria-label={t('agentManager.empty.prompt.addAttachmentAria')} > - +
@@ -645,9 +635,9 @@ export const AgentManagerEmptyState: React.FC = ({ aria-label={t('agentManager.empty.actions.startAgentGroupAria')} > {isSubmittingOrCreating ? ( - + ) : ( - + )}
diff --git a/packages/ui/src/components/views/agent-manager/AgentManagerSidebar.tsx b/packages/ui/src/components/views/agent-manager/AgentManagerSidebar.tsx index d5c12d29..5ed17d8b 100644 --- a/packages/ui/src/components/views/agent-manager/AgentManagerSidebar.tsx +++ b/packages/ui/src/components/views/agent-manager/AgentManagerSidebar.tsx @@ -1,12 +1,4 @@ import React from 'react'; -import { - RiAddLine, - RiArrowDownSLine, - RiMore2Line, - RiSearchLine, - RiGitBranchLine, - RiLoader4Line, -} from '@remixicon/react'; import { toast } from '@/components/ui'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -25,6 +17,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { useAgentGroupsStore, type AgentGroup } from '@/stores/useAgentGroupsStore'; import { useAllSessionStatuses } from '@/sync/sync-context'; @@ -92,11 +85,11 @@ const AgentGroupItem: React.FC = ({ group, isSelected, isBu {group.name} - {isBusy && } + {isBusy && }
- + {group.sessionCount === 1 ? t('agentManager.sidebar.item.modelCountSingle', { count: group.sessionCount }) : t('agentManager.sidebar.item.modelCountPlural', { count: group.sessionCount })} @@ -126,7 +119,7 @@ const AgentGroupItem: React.FC = ({ group, isSelected, isBu aria-label={t('agentManager.sidebar.item.groupMenuAria')} onClick={(e) => e.stopPropagation()} > - + @@ -218,7 +211,7 @@ export const AgentManagerSidebar: React.FC = ({ {/* Search Input */}
- + setSearchQuery(e.target.value)} @@ -235,14 +228,14 @@ export const AgentManagerSidebar: React.FC = ({ className="w-full justify-start gap-2 h-8" onClick={onNewAgent} > - + {t('agentManager.sidebar.actions.newAgentGroup')}
{/* Agent Groups Section Header */}
- + {t('agentManager.sidebar.section.agentGroups')} diff --git a/packages/ui/src/components/views/git/AIHighlightsBox.tsx b/packages/ui/src/components/views/git/AIHighlightsBox.tsx index d7c7da99..4e23646a 100644 --- a/packages/ui/src/components/views/git/AIHighlightsBox.tsx +++ b/packages/ui/src/components/views/git/AIHighlightsBox.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { RiArrowDownLine } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface AIHighlightsBoxProps { @@ -35,7 +35,7 @@ export const AIHighlightsBox: React.FC = ({ onClick={handleInsert} aria-label={t('gitView.commit.aiHighlights.insertAria')} > - + diff --git a/packages/ui/src/components/views/git/BranchIntegrationSection.tsx b/packages/ui/src/components/views/git/BranchIntegrationSection.tsx index 7f9943b1..dfe753be 100644 --- a/packages/ui/src/components/views/git/BranchIntegrationSection.tsx +++ b/packages/ui/src/components/views/git/BranchIntegrationSection.tsx @@ -1,12 +1,4 @@ import React from 'react'; -import { - RiGitMergeLine, - RiGitBranchLine, - RiLoader4Line, - RiArrowDownSLine, - RiCheckLine, - RiCloseLine, -} from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -31,6 +23,7 @@ import { CommandSeparator, } from '@/components/ui/command'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; @@ -188,13 +181,13 @@ export const BranchIntegrationSection: React.FC =
{log.status === 'running' && ( - + )} {log.status === 'done' && ( - + )} {log.status === 'error' && ( - + )} {log.status === 'pending' && (
@@ -251,7 +244,7 @@ export const BranchIntegrationSection: React.FC = )} >
- = )} >
- = {selectedBranch || t('gitView.branch.selectBranch')} - + = > {operation === 'merge' ? ( <> - + {t('gitView.operation.merge')} ) : ( <> - + {t('gitView.operation.rebase')} )} @@ -444,9 +437,9 @@ export const BranchIntegrationSection: React.FC = disabled={isDisabled} > {isOperating ? ( - + ) : ( - + )} {t('gitView.branch.mergeRebase')} diff --git a/packages/ui/src/components/views/git/BranchSelector.tsx b/packages/ui/src/components/views/git/BranchSelector.tsx index 60bd4b12..fa5e3b64 100644 --- a/packages/ui/src/components/views/git/BranchSelector.tsx +++ b/packages/ui/src/components/views/git/BranchSelector.tsx @@ -1,12 +1,4 @@ import React from 'react'; -import { - RiGitBranchLine, - RiArrowDownSLine, - RiAddLine, - RiCloseLine, - RiLoader4Line, - RiArrowLeftLine, -} from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -23,6 +15,7 @@ import { CommandSeparator, } from '@/components/ui/command'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import type { GitRemote } from '@/lib/api/types'; import { useI18n } from '@/lib/i18n'; @@ -177,11 +170,11 @@ export const BranchSelector: React.FC = ({ className="h-8 min-w-0 max-w-full justify-start gap-1.5 px-2 py-1" disabled={disabled} > - + {currentBranch || t('gitView.branch.detachedHead')} - + @@ -214,7 +207,7 @@ export const BranchSelector: React.FC = ({ disabled={isCreating} className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50" > - + {t('gitView.branch.pushToPrefix')} {sanitizedNewBranch} {t('gitView.branch.pushToSuffix')} @@ -231,7 +224,7 @@ export const BranchSelector: React.FC = ({ > {isCreating ? ( - + ) : null} {remote.name} @@ -244,7 +237,7 @@ export const BranchSelector: React.FC = ({
) : !showCreate ? ( - + {t('gitView.branch.create')} ) : ( @@ -274,9 +267,9 @@ export const BranchSelector: React.FC = ({ className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50" > {isCreating ? ( - + ) : ( - + )}
)} diff --git a/packages/ui/src/components/views/git/ChangeRow.tsx b/packages/ui/src/components/views/git/ChangeRow.tsx index 0b41f16b..f10fb9d5 100644 --- a/packages/ui/src/components/views/git/ChangeRow.tsx +++ b/packages/ui/src/components/views/git/ChangeRow.tsx @@ -1,11 +1,8 @@ import React, { useCallback, useMemo } from 'react'; -import { - RiArrowGoBackLine, - RiLoader4Line, -} from '@remixicon/react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Checkbox } from '@/components/ui/checkbox'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; +import { Icon } from "@/components/icon/Icon"; import type { GitStatus } from '@/lib/api/types'; import { useI18n } from '@/lib/i18n'; @@ -160,9 +157,9 @@ export const ChangeRow = React.memo(function ChangeRow({ aria-label={t('gitView.changes.revertFileAria', { path: file.path })} > {isReverting ? ( - + ) : ( - + )} diff --git a/packages/ui/src/components/views/git/ChangesSection.tsx b/packages/ui/src/components/views/git/ChangesSection.tsx index afc0484b..dc8f67f2 100644 --- a/packages/ui/src/components/views/git/ChangesSection.tsx +++ b/packages/ui/src/components/views/git/ChangesSection.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; -import { RiArchiveStackLine, RiFolder3Fill, RiFolderOpenFill } from '@remixicon/react'; import { Checkbox } from '@/components/ui/checkbox'; import { Button } from '@/components/ui/button'; import { @@ -13,6 +12,7 @@ import { } from '@/components/ui/dialog'; import { ScrollShadow } from '@/components/ui/ScrollShadow'; import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar'; +import { Icon } from "@/components/icon/Icon"; import { ChangeRow } from './ChangeRow'; import type { GitStatus } from '@/lib/api/types'; import { cn } from '@/lib/utils'; @@ -405,9 +405,9 @@ export const ChangesSection: React.FC = ({ : t('gitView.changes.expandDirectoryAria', { path: directory.path })} > {isExpanded ? ( - + ) : ( - + )} {directory.name} @@ -475,7 +475,7 @@ export const ChangesSection: React.FC = ({ aria-label={t('gitView.stashes.title')} title={t('gitView.stashes.title')} > - + ) : null}
diff --git a/packages/ui/src/components/views/git/CommitSection.tsx b/packages/ui/src/components/views/git/CommitSection.tsx index ca298838..b7a58f29 100644 --- a/packages/ui/src/components/views/git/CommitSection.tsx +++ b/packages/ui/src/components/views/git/CommitSection.tsx @@ -1,15 +1,9 @@ -import { - RiGitCommitLine, - RiArrowUpLine, - RiAiGenerate2, - RiLoader4Line, - RiEmotionHappyLine, -} from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { CommitInput } from './CommitInput'; import { AIHighlightsBox } from './AIHighlightsBox'; import { useDeviceInfo } from '@/lib/device'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; type CommitAction = 'commit' | 'commitAndPush' | null; @@ -87,7 +81,7 @@ export const CommitSection: React.FC = ({ className="w-fit" type="button" > - + {t('gitView.commit.addGitmoji')} )} @@ -107,9 +101,9 @@ export const CommitSection: React.FC = ({ className="commit-actions__btn" > {isGeneratingMessage ? ( - + ) : ( - + )} {t('gitView.commit.generate')} @@ -126,12 +120,12 @@ export const CommitSection: React.FC = ({ > {commitAction === 'commit' ? ( <> - + {t('gitView.commit.committing')} ) : ( <> - + {t('gitView.commit.commit')} )} @@ -149,9 +143,9 @@ export const CommitSection: React.FC = ({ aria-label={t('gitView.commit.pushAria')} > {commitAction === 'commitAndPush' ? ( - + ) : ( - + )} @@ -170,12 +164,12 @@ export const CommitSection: React.FC = ({ > {commitAction === 'commitAndPush' ? ( <> - + {t('gitView.commit.pushing')} ) : ( <> - + {t('gitView.commit.push')} )} diff --git a/packages/ui/src/components/views/git/ConflictDialog.tsx b/packages/ui/src/components/views/git/ConflictDialog.tsx index 28d1168d..bd75bc4e 100644 --- a/packages/ui/src/components/views/git/ConflictDialog.tsx +++ b/packages/ui/src/components/views/git/ConflictDialog.tsx @@ -8,11 +8,11 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { RiAlertLine, RiLoader4Line, RiChat1Line, RiAddLine } from '@remixicon/react'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useInputStore } from '@/sync/input-store'; import { useUIStore } from '@/stores/useUIStore'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { getConflictDetails, type MergeConflictDetails } from '@/lib/gitApi'; import { renderMagicPrompt } from '@/lib/magicPrompts'; import { useI18n } from '@/lib/i18n'; @@ -173,7 +173,7 @@ export const ConflictDialog: React.FC = ({
- + {t('gitView.conflict.detectedTitle', { operation: operationLabel })}
@@ -183,7 +183,7 @@ export const ConflictDialog: React.FC = ({ {isLoading && (
- + {t('gitView.conflict.loading')}
)} @@ -237,9 +237,9 @@ export const ConflictDialog: React.FC = ({ className="w-full gap-2" > {isLoading ? ( - + ) : ( - + )} {t('gitView.conflict.resolveNewSession')} @@ -250,9 +250,9 @@ export const ConflictDialog: React.FC = ({ className="w-full gap-2" > {isLoading ? ( - + ) : ( - + )} {t('gitView.conflict.resolveCurrentSession')} diff --git a/packages/ui/src/components/views/git/GitEmptyState.tsx b/packages/ui/src/components/views/git/GitEmptyState.tsx index 40ce2d19..daab1914 100644 --- a/packages/ui/src/components/views/git/GitEmptyState.tsx +++ b/packages/ui/src/components/views/git/GitEmptyState.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { RiArchiveStackLine, RiGitCommitLine } from '@remixicon/react'; import { Button } from '@/components/ui/button'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface GitEmptyStateProps { @@ -11,7 +11,7 @@ export const GitEmptyState: React.FC = ({ onOpenStashes }) = const { t } = useI18n(); return (
- +

{t('gitView.empty.cleanTitle')}

@@ -27,7 +27,7 @@ export const GitEmptyState: React.FC = ({ onOpenStashes }) = onClick={onOpenStashes} className="gap-1.5" > - + {t('gitView.stashes.title')} ) : null} diff --git a/packages/ui/src/components/views/git/GitHeader.tsx b/packages/ui/src/components/views/git/GitHeader.tsx index 6d1485e3..40eefa35 100644 --- a/packages/ui/src/components/views/git/GitHeader.tsx +++ b/packages/ui/src/components/views/git/GitHeader.tsx @@ -1,17 +1,4 @@ import React from 'react'; -import { - RiArrowDownSLine, - RiCheckLine, - RiLoader4Line, - RiGitBranchLine, - RiBriefcaseLine, - RiHomeLine, - RiGraduationCapLine, - RiCodeLine, - RiHeartLine, - RiHistoryLine, - RiUser3Line, -} from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { SortableTabsStrip, type SortableTabsStripItem } from '@/components/ui/sortable-tabs-strip'; import { @@ -21,6 +8,8 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; +import type { IconName } from "@/components/icon/icons"; import { BranchSelector } from './BranchSelector'; import { WorktreeBranchDisplay } from './WorktreeBranchDisplay'; import { SyncActions } from './SyncActions'; @@ -54,17 +43,14 @@ interface GitHeaderProps { onSelectActionTab?: (tabID: string) => void; } -const IDENTITY_ICON_MAP: Record< - string, - React.ComponentType> -> = { - branch: RiGitBranchLine, - briefcase: RiBriefcaseLine, - house: RiHomeLine, - graduation: RiGraduationCapLine, - code: RiCodeLine, - heart: RiHeartLine, - user: RiUser3Line, +const IDENTITY_ICON_MAP: Record = { + branch: 'git-branch', + briefcase: 'briefcase', + house: 'home', + graduation: 'graduation-cap', + code: 'code', + heart: 'heart', + user: 'user-3', }; const IDENTITY_COLOR_MAP: Record = { @@ -92,9 +78,10 @@ interface IdentityIconProps { } const IdentityIcon: React.FC = ({ icon, className, colorToken }) => { - const IconComponent = IDENTITY_ICON_MAP[icon ?? 'branch'] ?? RiUser3Line; + const iconName = IDENTITY_ICON_MAP[icon ?? 'branch'] ?? 'user-3'; return ( - @@ -132,7 +119,7 @@ export const IdentityDropdown: React.FC = ({ disabled={isDisabled} > {isApplying ? ( - + ) : ( = ({ {activeProfile?.name || t('gitView.header.noIdentity')} )} - + @@ -178,7 +165,7 @@ export const IdentityDropdown: React.FC = ({ {isSelected ? ( - + ) : null} @@ -230,7 +217,7 @@ export const GitHeader: React.FC = ({ className="h-8 w-8 px-0" onClick={onOpenHistory} > - + {t('gitView.history.title')} diff --git a/packages/ui/src/components/views/git/HistoryCommitRow.tsx b/packages/ui/src/components/views/git/HistoryCommitRow.tsx index 24fe26de..dac47328 100644 --- a/packages/ui/src/components/views/git/HistoryCommitRow.tsx +++ b/packages/ui/src/components/views/git/HistoryCommitRow.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { RiLoader4Line, RiFileCopyLine } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import type { GitLogEntry, CommitFileEntry } from '@/lib/api/types'; import { useI18n } from '@/lib/i18n'; @@ -99,7 +99,7 @@ export const HistoryCommitRow = React.memo(({ onCopyHash(entry.hash); }} > - + {t('gitView.history.copySha')} @@ -112,7 +112,7 @@ export const HistoryCommitRow = React.memo(({
{isLoadingFiles ? (
- + {t('gitView.history.loadingFiles')}
) : files.length === 0 ? ( diff --git a/packages/ui/src/components/views/git/HistorySection.tsx b/packages/ui/src/components/views/git/HistorySection.tsx index fa61a9ac..c73d7281 100644 --- a/packages/ui/src/components/views/git/HistorySection.tsx +++ b/packages/ui/src/components/views/git/HistorySection.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { RiArrowDownSLine, RiArrowUpLine, RiArrowUpSLine } from '@remixicon/react'; import { Collapsible, CollapsibleContent, @@ -13,6 +12,7 @@ import { SelectValue, } from '@/components/ui/select'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { Icon } from "@/components/icon/Icon"; import { HistoryCommitRow } from './HistoryCommitRow'; import type { GitLogEntry, CommitFileEntry } from '@/lib/api/types'; import { useI18n } from '@/lib/i18n'; @@ -78,8 +78,8 @@ export const HistorySection: React.FC = ({ const bottomEntries = hasDivider ? log.all.slice(branchDivider.insertBeforeIndex) : []; const dividerIcon = branchDivider?.direction === 'down' - ? - : ; + ? + : ; const renderCommitList = (entries: GitLogEntry[]) => (
    @@ -182,9 +182,9 @@ export const HistorySection: React.FC = ({
)} {isOpen ? ( - + ) : ( - + )}
diff --git a/packages/ui/src/components/views/git/InProgressOperationBanner.tsx b/packages/ui/src/components/views/git/InProgressOperationBanner.tsx index 09de85a4..64d461ab 100644 --- a/packages/ui/src/components/views/git/InProgressOperationBanner.tsx +++ b/packages/ui/src/components/views/git/InProgressOperationBanner.tsx @@ -1,13 +1,6 @@ import React from 'react'; -import { - RiGitMergeLine, - RiGitBranchLine, - RiLoader4Line, - RiCheckLine, - RiCloseLine, - RiSparklingLine, -} from '@remixicon/react'; import { Button } from '@/components/ui/button'; +import { Icon } from "@/components/icon/Icon"; import type { GitMergeInProgress, GitRebaseInProgress } from '@/lib/api/types'; import { useI18n } from '@/lib/i18n'; @@ -63,7 +56,7 @@ export const InProgressOperationBanner: React.FC const isProcessing = processingAction !== null; const operationLabel = operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase'); - const OperationIcon = operation === 'merge' ? RiGitMergeLine : RiGitBranchLine; + const operationIconName = operation === 'merge' ? 'git-merge' : 'git-branch'; // Build description let description = ''; @@ -81,7 +74,7 @@ export const InProgressOperationBanner: React.FC
- +

{t('gitView.operation.inProgressTitle', { operation: operationLabel })} @@ -103,7 +96,7 @@ export const InProgressOperationBanner: React.FC disabled={isProcessing || isLoading} className="gap-1.5" > - + {t('gitView.operation.resolveWithAi')} )} @@ -117,9 +110,9 @@ export const InProgressOperationBanner: React.FC className="gap-1.5" > {processingAction === 'abort' ? ( - + ) : ( - + )} {t('gitView.operation.abort')} @@ -134,9 +127,9 @@ export const InProgressOperationBanner: React.FC className="gap-1.5" > {processingAction === 'continue' ? ( - + ) : ( - + )} {t('gitView.operation.continue')} diff --git a/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx b/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx index e8be153c..5b8704df 100644 --- a/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx +++ b/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { RiArrowDownSLine, RiLoader4Line, RiSplitCellsHorizontal, RiSparklingLine } from '@remixicon/react'; import { DropdownMenu, DropdownMenuContent, @@ -15,6 +14,7 @@ import { CommandList, } from '@/components/ui/command'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useInputStore } from '@/sync/input-store'; import { useUIStore } from '@/stores/useUIStore'; @@ -344,7 +344,7 @@ export const IntegrateCommitsSection: React.FC<{

- +

{t('gitView.integrate.title')}

{ui.kind === 'ready' && ui.plan.commits.length > 0 ? ( @@ -354,7 +354,7 @@ export const IntegrateCommitsSection: React.FC<{
{ui.kind === 'loading' || ui.kind === 'running' ? ( - + ) : null}
@@ -375,7 +375,7 @@ export const IntegrateCommitsSection: React.FC<{ void handleResolveWithAi({ state: ui.state, details: ui.details }, false)} > - + {t('gitView.integrate.currentSession')} @@ -859,7 +841,7 @@ export const PullRequestSection: React.FC<{ (isFail ? 'bg-destructive/10 text-destructive' : 'text-muted-foreground') } > - {stepExpanded ? : } + {stepExpanded ? : } {step.name} {step.conclusion ? {step.conclusion} : null} @@ -1385,13 +1367,13 @@ export const PullRequestSection: React.FC<{ const shouldShowConnectionNotice = githubAuthChecked && status?.connected === false; const prVisualState = getPrVisualState(status); const prColorVar = prVisualState ? `var(--pr-${prVisualState})` : 'var(--status-info)'; - const PrStateIcon = prVisualState === 'draft' - ? RiGitPrDraftLine + const prStateIconName = prVisualState === 'draft' + ? 'git-pr-draft' : prVisualState === 'merged' - ? RiGitMergeLine + ? 'git-merge' : prVisualState === 'closed' - ? RiGitClosePullRequestLine - : RiGitPullRequestLine; + ? 'git-close-pull-request' + : 'git-pull-request'; const prStatusText = pr ? [ `${pr.state}${pr.draft ? ' (draft)' : ''}`, @@ -1424,13 +1406,13 @@ export const PullRequestSection: React.FC<{ onClick={() => void openExternal(pr.url)} aria-label={t('gitView.pr.actions.openOnGitHubAria')} > - +

{t('gitView.pr.actions.openOnGitHub')}

) : ( - + )}

{t('gitView.pullRequest.title')}

{pr ? ( @@ -1438,7 +1420,7 @@ export const PullRequestSection: React.FC<{ ) : null}
- {isLoading ? : null} + {isLoading ? : null}

{t('gitView.pr.actions.refresh')}

@@ -1493,7 +1475,7 @@ export const PullRequestSection: React.FC<{ {repoUrl ? ( @@ -1503,7 +1485,7 @@ export const PullRequestSection: React.FC<{ {!pr && !isInitialStatusResolved && !error && !shouldShowConnectionNotice ? (
- + {t('gitView.pr.checkingStatus')}
) : pr ? ( @@ -1574,7 +1556,7 @@ export const PullRequestSection: React.FC<{ disabled={isUpdating} aria-label={t('gitView.pr.actions.cancelEditingAria')} > - +

{t('gitView.pr.actions.cancelEditing')}

@@ -1588,7 +1570,7 @@ export const PullRequestSection: React.FC<{ disabled={isUpdating || !editTitle.trim()} aria-label={t('gitView.pr.actions.savePrAria')} > - {isUpdating ? : } + {isUpdating ? : }

{t('gitView.pr.actions.savePr')}

@@ -1604,7 +1586,7 @@ export const PullRequestSection: React.FC<{ onClick={() => setIsEditingPr(true)} aria-label={t('gitView.pr.actions.editPrAria')} > - +

{t('gitView.pr.actions.editPr')}

@@ -1623,7 +1605,7 @@ export const PullRequestSection: React.FC<{ disabled={isLoadingCheckDetails} aria-label={t('gitView.pr.actions.openChecksAria')} > - {isLoadingCheckDetails ? : } + {isLoadingCheckDetails ? : }

{t('gitView.pr.actions.openChecks')}

@@ -1640,7 +1622,7 @@ export const PullRequestSection: React.FC<{ onClick={sendFailedChecksToChat} aria-label={t('gitView.pr.actions.resolveFailedChecksAria')} > - +

{t('gitView.pr.actions.resolveFailedChecks')}

@@ -1656,7 +1638,7 @@ export const PullRequestSection: React.FC<{ onClick={openCommentsDialog} aria-label={t('gitView.pr.actions.openCommentsAria')} > - +

{t('gitView.pr.actions.openComments')}

@@ -1671,7 +1653,7 @@ export const PullRequestSection: React.FC<{ onClick={sendCommentsToChat} aria-label={t('gitView.pr.actions.shareCommentsAria')} > - +

{t('gitView.pr.actions.shareComments')}

@@ -1688,7 +1670,7 @@ export const PullRequestSection: React.FC<{ disabled={isMarkingReady || isMerging || isUpdating || isEditingPr} aria-label={t('gitView.pr.actions.markReadyAria')} > - {isMarkingReady ? : } + {isMarkingReady ? : }

{t('gitView.pr.actions.markReady')}

@@ -1722,7 +1704,7 @@ export const PullRequestSection: React.FC<{ disabled={isMerging || isMarkingReady || pr.state !== 'open' || pr.draft || isUpdating || isEditingPr} aria-label={t('gitView.pr.actions.mergePrAria')} > - {isMerging ? : } + {isMerging ? : }

{t('gitView.pr.actions.mergePr')}

@@ -1745,7 +1727,7 @@ export const PullRequestSection: React.FC<{ {repoUrl ? ( @@ -1906,7 +1888,7 @@ export const PullRequestSection: React.FC<{ onClick={generateDescription} disabled={isGenerating || isCreating} > - {isGenerating ? : } + {isGenerating ? : } {t('gitView.commit.generate')}
@@ -1917,7 +1899,7 @@ export const PullRequestSection: React.FC<{ disabled={isCreating || !isConnected || !targetBaseBranch.trim() || (!useDetectedUpstream && targetBaseBranch.trim() === branch)} > - {isCreating ? : } + {isCreating ? : } {t('gitView.pr.actions.createPr')} @@ -1930,7 +1912,7 @@ export const PullRequestSection: React.FC<{ - + {t('gitView.pr.checkDetails.title')} @@ -1941,7 +1923,7 @@ export const PullRequestSection: React.FC<{
{isLoadingCheckDetails ? (
- + {t('gitView.loading.loading')}
) : null} @@ -1971,7 +1953,7 @@ export const PullRequestSection: React.FC<{ - + {t('gitView.pr.comments.title')} {pr ? ( {t('gitView.pr.numberLabel', { number: pr.number })} @@ -1982,7 +1964,7 @@ export const PullRequestSection: React.FC<{ {isLoadingCommentsDetails ? (
- + {t('gitView.loading.loading')}
) : null} @@ -2023,7 +2005,7 @@ export const PullRequestSection: React.FC<{ }} aria-label={t('gitView.pr.actions.sendCommentToAgentAria')} > - + {t('gitView.pr.actions.sendToAgent')} diff --git a/packages/ui/src/components/views/git/StashDialog.tsx b/packages/ui/src/components/views/git/StashDialog.tsx index e4cddccb..a55e8fb5 100644 --- a/packages/ui/src/components/views/git/StashDialog.tsx +++ b/packages/ui/src/components/views/git/StashDialog.tsx @@ -10,7 +10,7 @@ import { import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { toast } from '@/components/ui'; -import { RiAlertLine, RiLoader4Line } from '@remixicon/react'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface StashDialogProps { @@ -59,7 +59,7 @@ export const StashDialog: React.FC = ({
- + {t('gitView.stash.title')}
@@ -115,7 +115,7 @@ export const StashDialog: React.FC = ({ > {isProcessing ? ( <> - + {t('gitView.common.processing')} ) : ( diff --git a/packages/ui/src/components/views/git/StashesDialog.tsx b/packages/ui/src/components/views/git/StashesDialog.tsx index 4b7f5ab0..d4fef84e 100644 --- a/packages/ui/src/components/views/git/StashesDialog.tsx +++ b/packages/ui/src/components/views/git/StashesDialog.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { RiArchiveStackLine, RiDeleteBinLine, RiInboxArchiveLine, RiInboxUnarchiveFill, RiInboxUnarchiveLine, RiLoader4Line, RiSearchLine } from '@remixicon/react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import type { GitStashEntry } from '@/lib/api/types'; @@ -124,7 +124,7 @@ export const StashesDialog: React.FC = ({ - + {t('gitView.stashes.title')} {t('gitView.stashes.description')} @@ -140,7 +140,7 @@ export const StashesDialog: React.FC = ({ className="flex-1" />
@@ -148,13 +148,13 @@ export const StashesDialog: React.FC = ({
- + setQuery(event.target.value)} placeholder={t('gitView.stashes.searchPlaceholder')} className="pl-9" />
{isLoading ? ( -
+
) : filtered.length === 0 ? (
{query ? t('gitView.stashes.empty.search') : t('gitView.stashes.empty.list')}
) : filtered.map((stash, index) => ( @@ -172,9 +172,9 @@ export const StashesDialog: React.FC = ({

- runStashAction(stash, 'apply')} disabled={isOperating}> - runStashAction(stash, 'pop')} disabled={isOperating}> - runStashAction(stash, 'drop')} disabled={isOperating} destructive> + runStashAction(stash, 'apply')} disabled={isOperating}> + runStashAction(stash, 'pop')} disabled={isOperating}> + runStashAction(stash, 'drop')} disabled={isOperating} destructive>
))} @@ -188,7 +188,7 @@ const StashIconButton: React.FC<{ label: string; loading: boolean; disabled: boo {label} diff --git a/packages/ui/src/components/views/git/SyncActions.tsx b/packages/ui/src/components/views/git/SyncActions.tsx index 5e287988..710b2ee2 100644 --- a/packages/ui/src/components/views/git/SyncActions.tsx +++ b/packages/ui/src/components/views/git/SyncActions.tsx @@ -1,10 +1,4 @@ import React from 'react'; -import { - RiArrowDownSLine, - RiCloseLine, - RiLoader4Line, - RiRefreshLine, -} from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { @@ -13,6 +7,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { Icon } from "@/components/icon/Icon"; import type { GitRemote } from '@/lib/gitApi'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; @@ -90,9 +85,9 @@ export const SyncActions: React.FC = ({ aria-label={t('gitView.sync.syncChanges')} > {syncAction === 'sync' ? ( - + ) : ( - + )} {primaryLabel} @@ -111,7 +106,7 @@ export const SyncActions: React.FC = ({ disabled={isDropdownDisabled} aria-label={t('gitView.sync.moreActionsAria')} > - + @@ -128,7 +123,7 @@ export const SyncActions: React.FC = ({ }} >
- +
@@ -161,9 +156,9 @@ export const SyncActions: React.FC = ({ title={t('gitView.header.removeRemoteTitle', { name: remote.name })} > {removingRemoteName === remote.name ? ( - + ) : ( - + )} ) : null} diff --git a/packages/ui/src/components/views/git/WorktreeBranchDisplay.tsx b/packages/ui/src/components/views/git/WorktreeBranchDisplay.tsx index ca4205a9..4676eba7 100644 --- a/packages/ui/src/components/views/git/WorktreeBranchDisplay.tsx +++ b/packages/ui/src/components/views/git/WorktreeBranchDisplay.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { RiGitBranchLine, RiEditLine, RiCheckLine, RiCloseLine, RiLoader4Line } from '@remixicon/react'; import { Button } from '@/components/ui/button'; +import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface WorktreeBranchDisplayProps { @@ -86,7 +86,7 @@ export const WorktreeBranchDisplay: React.FC = ({ handleSaveEdit(); }} > - + = ({ className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50" > {isRenaming ? ( - + ) : ( - + )}
@@ -126,7 +126,7 @@ export const WorktreeBranchDisplay: React.FC = ({ return (
- +
{currentBranch || t('gitView.branch.detachedHead')} @@ -139,7 +139,7 @@ export const WorktreeBranchDisplay: React.FC = ({ onClick={handleStartEdit} title={t('gitView.branch.renameTitle')} > - + )}
diff --git a/packages/ui/src/components/voice/BrowserVoiceButton.tsx b/packages/ui/src/components/voice/BrowserVoiceButton.tsx index f385c0a2..70b17431 100644 --- a/packages/ui/src/components/voice/BrowserVoiceButton.tsx +++ b/packages/ui/src/components/voice/BrowserVoiceButton.tsx @@ -22,14 +22,9 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; -import { - RiMicOffLine, - RiStopCircleLine, - RiVoiceRecognitionLine, - RiVolumeUpLine, -} from '@remixicon/react'; import { VoiceStatusIndicator } from './VoiceStatusIndicator'; import { toast } from '@/components/ui/toast'; +import { Icon } from "@/components/icon/Icon"; // Status text for accessibility and labels const statusLabels: Record = { @@ -260,8 +255,6 @@ export function BrowserVoiceButton() { toggleConversationMode(); }, [toggleConversationMode]); - - // If voice mode is disabled, don't render anything if (!voiceModeEnabled) { return null; @@ -289,7 +282,7 @@ export function BrowserVoiceButton() { aria-label={tooltipMessage} className={`${buttonSizeClass} p-0 ${clearHoverBackgroundClass}`} > - + @@ -341,10 +334,10 @@ export function BrowserVoiceButton() { {isActive ? ( isSpeaking ? ( // Green speaker icon when AI is speaking - + ) : ( // Red stop icon for listening/processing (both mobile and desktop) - + ) ) : ( - + )}
diff --git a/packages/ui/src/components/voice/VoiceStatusIndicator.tsx b/packages/ui/src/components/voice/VoiceStatusIndicator.tsx index 0c0718aa..9be499b0 100644 --- a/packages/ui/src/components/voice/VoiceStatusIndicator.tsx +++ b/packages/ui/src/components/voice/VoiceStatusIndicator.tsx @@ -18,15 +18,9 @@ */ import React from 'react'; -import { - RiMicLine, - RiMicOffLine, - RiLoader4Line, - RiVolumeUpLine, - RiAlertLine, -} from '@remixicon/react'; import type { BrowserVoiceStatus } from '@/hooks/useBrowserVoice'; import { useI18n } from '@/lib/i18n'; +import { Icon } from "@/components/icon/Icon"; export interface VoiceStatusIndicatorProps { /** Current voice status */ @@ -59,7 +53,7 @@ const sizeClasses = { const statusConfig: Record< BrowserVoiceStatus, { - icon: typeof RiMicLine; + icon: string; color: string; labelKey: | 'voice.status.idle' @@ -71,29 +65,29 @@ const statusConfig: Record< } > = { idle: { - icon: RiMicOffLine, + icon: "mic-off", color: 'text-muted-foreground', labelKey: 'voice.status.idle', }, listening: { - icon: RiMicLine, + icon: "mic", color: 'text-primary', labelKey: 'voice.status.listening', animation: 'animate-pulse', }, processing: { - icon: RiLoader4Line, + icon: "loader-4", color: 'text-primary', labelKey: 'voice.status.processing', animation: 'animate-spin', }, speaking: { - icon: RiVolumeUpLine, + icon: "volume-up", color: 'text-green-500', labelKey: 'voice.status.speaking', }, error: { - icon: RiAlertLine, + icon: "alert", color: 'text-destructive', labelKey: 'voice.status.error', }, @@ -111,14 +105,14 @@ export function VoiceStatusIndicator({ }: VoiceStatusIndicatorProps) { const { t } = useI18n(); const config = statusConfig[status]; - const Icon = config.icon; + const statusIconName = config.icon; const sizeClass = sizeClasses[size]; const containerClass = showLabel ? sizeClass.container : ''; return (
- ; +export type IconComponent = IconName; export interface SidebarSectionConfig { id: SidebarSection; @@ -18,55 +16,55 @@ export const SIDEBAR_SECTIONS: SidebarSectionConfig[] = [ id: 'sessions', label: 'Sessions', description: 'Browse and manage chat sessions scoped to the current directory.', - icon: RiChatAi3Line, + icon: "chat-ai-3", }, { id: 'agents', label: 'Agents', description: 'Configure OpenCode agents, prompts, and permissions.', - icon: RiBrainAi3Line, + icon: "brain-ai-3", }, { id: 'commands', label: 'Commands', description: 'Create and maintain custom slash commands for OpenCode.', - icon: RiCommandLine, + icon: "command", }, { id: 'skills', label: 'Skills', description: 'Create reusable instruction files for agents to load on-demand.', - icon: RiBookLine, + icon: "book", }, { id: 'mcp', label: 'MCP', description: 'Manage Model Context Protocol servers and their configurations.', - icon: RiPlugLine, + icon: "plug-line", }, { id: 'providers', label: 'Providers', description: 'Configure AI model providers and API credentials.', - icon: RiStackLine, + icon: "stack", }, { id: 'usage', label: 'Usage', description: 'Monitor API quota and usage across providers.', - icon: RiBarChart2Line, + icon: "bar-chart-2", }, { id: 'git-identities', label: 'Git Identities', description: 'Manage Git profiles with different credentials and SSH keys.', - icon: RiGitBranchLine, + icon: "git-branch", }, { id: 'settings', label: 'OpenChamber', description: 'OpenChamber app settings: themes, fonts, and preferences.', - icon: RiSettings3Line, + icon: "settings-3", }, ]; diff --git a/packages/ui/src/lib/projectActions.ts b/packages/ui/src/lib/projectActions.ts index abdeb96f..a6ee9726 100644 --- a/packages/ui/src/lib/projectActions.ts +++ b/packages/ui/src/lib/projectActions.ts @@ -1,24 +1,3 @@ -import { - RiBrainAi3Line, - RiCheckboxCircleLine, - RiBugLine, - RiCodeLine, - RiCommandLine, - RiFileTextLine, - RiFlaskLine, - RiGitBranchLine, - RiHammerLine, - RiPlayLine, - RiRocketLine, - RiRobot2Line, - RiSearchLine, - RiServerLine, - RiSettings3Line, - RiStackLine, - RiTerminalBoxLine, - RiToolsLine, -} from '@remixicon/react'; -import type { ComponentType } from 'react'; import type { OpenChamberProjectAction, OpenChamberProjectActionPlatform, @@ -27,6 +6,7 @@ import type { DesktopSshInstance, DesktopSshPortForward, } from '@/lib/desktopSsh'; +import type { IconName } from "@/components/icon/icons"; export type ProjectActionIconKey = | 'play' @@ -51,31 +31,31 @@ export type ProjectActionIconKey = export const PROJECT_ACTION_ICONS: Array<{ key: ProjectActionIconKey; label: string; - Icon: ComponentType<{ className?: string }>; + Icon: IconName; }> = [ - { key: 'play', label: 'Play', Icon: RiPlayLine }, - { key: 'build', label: 'Build', Icon: RiHammerLine }, - { key: 'lint', label: 'Lint', Icon: RiCheckboxCircleLine }, - { key: 'terminal', label: 'Terminal', Icon: RiTerminalBoxLine }, - { key: 'tools', label: 'Tools', Icon: RiToolsLine }, - { key: 'bug', label: 'Bug', Icon: RiBugLine }, - { key: 'flask', label: 'Flask', Icon: RiFlaskLine }, - { key: 'rocket', label: 'Rocket', Icon: RiRocketLine }, - { key: 'code', label: 'Code', Icon: RiCodeLine }, - { key: 'server', label: 'Server', Icon: RiServerLine }, - { key: 'branch', label: 'Branch', Icon: RiGitBranchLine }, - { key: 'search', label: 'Search', Icon: RiSearchLine }, - { key: 'settings', label: 'Settings', Icon: RiSettings3Line }, - { key: 'brain', label: 'Brain', Icon: RiBrainAi3Line }, - { key: 'stack', label: 'Stack', Icon: RiStackLine }, - { key: 'robot', label: 'Robot', Icon: RiRobot2Line }, - { key: 'command', label: 'Command', Icon: RiCommandLine }, - { key: 'file', label: 'File', Icon: RiFileTextLine }, + { key: 'play', label: 'Play', Icon: 'play' }, + { key: 'build', label: 'Build', Icon: 'hammer' }, + { key: 'lint', label: 'Lint', Icon: 'checkbox-circle' }, + { key: 'terminal', label: 'Terminal', Icon: 'terminal-box' }, + { key: 'tools', label: 'Tools', Icon: 'tools' }, + { key: 'bug', label: 'Bug', Icon: 'bug' }, + { key: 'flask', label: 'Flask', Icon: 'flask' }, + { key: 'rocket', label: 'Rocket', Icon: 'rocket' }, + { key: 'code', label: 'Code', Icon: 'code' }, + { key: 'server', label: 'Server', Icon: 'server' }, + { key: 'branch', label: 'Branch', Icon: 'git-branch' }, + { key: 'search', label: 'Search', Icon: 'search' }, + { key: 'settings', label: 'Settings', Icon: 'settings-3' }, + { key: 'brain', label: 'Brain', Icon: 'brain-ai-3' }, + { key: 'stack', label: 'Stack', Icon: 'stack' }, + { key: 'robot', label: 'Robot', Icon: 'robot-2' }, + { key: 'command', label: 'Command', Icon: 'command' }, + { key: 'file', label: 'File', Icon: 'file-text' }, ]; export const PROJECT_ACTION_ICON_MAP = Object.fromEntries( PROJECT_ACTION_ICONS.map((entry) => [entry.key, entry.Icon]) -) as Record>; +) as Record; export const PROJECT_ACTIONS_UPDATED_EVENT = 'openchamber:project-actions-updated'; diff --git a/packages/ui/src/lib/projectMeta.ts b/packages/ui/src/lib/projectMeta.ts index 1116f3ae..770e568b 100644 --- a/packages/ui/src/lib/projectMeta.ts +++ b/packages/ui/src/lib/projectMeta.ts @@ -1,52 +1,31 @@ -import { - RiCodeBoxLine, - RiTerminalBoxLine, - RiRocketLine, - RiFlaskLine, - RiGamepadLine, - RiBriefcaseLine, - RiHomeLine, - RiGlobalLine, - RiLeafLine, - RiShieldLine, - RiPaletteLine, - RiServerLine, - RiSmartphoneLine, - RiDatabase2Line, - RiLightbulbLine, - RiMusicLine, - RiCameraLine, - RiBookOpenLine, - RiHeartLine, - type RemixiconComponentType, -} from '@remixicon/react'; import type { ProjectEntry } from '@/lib/api/types'; +import type { IconName } from "@/components/icon/icons"; type ThemeVariant = 'light' | 'dark'; -export const PROJECT_ICONS: Array<{ key: string; Icon: RemixiconComponentType; label: string }> = [ - { key: 'code', Icon: RiCodeBoxLine, label: 'Code' }, - { key: 'terminal', Icon: RiTerminalBoxLine, label: 'Terminal' }, - { key: 'rocket', Icon: RiRocketLine, label: 'Rocket' }, - { key: 'flask', Icon: RiFlaskLine, label: 'Lab' }, - { key: 'gamepad', Icon: RiGamepadLine, label: 'Game' }, - { key: 'briefcase', Icon: RiBriefcaseLine, label: 'Work' }, - { key: 'home', Icon: RiHomeLine, label: 'Home' }, - { key: 'globe', Icon: RiGlobalLine, label: 'Web' }, - { key: 'leaf', Icon: RiLeafLine, label: 'Nature' }, - { key: 'shield', Icon: RiShieldLine, label: 'Security' }, - { key: 'palette', Icon: RiPaletteLine, label: 'Design' }, - { key: 'server', Icon: RiServerLine, label: 'Server' }, - { key: 'phone', Icon: RiSmartphoneLine, label: 'Mobile' }, - { key: 'database', Icon: RiDatabase2Line, label: 'Data' }, - { key: 'lightbulb', Icon: RiLightbulbLine, label: 'Idea' }, - { key: 'music', Icon: RiMusicLine, label: 'Music' }, - { key: 'camera', Icon: RiCameraLine, label: 'Media' }, - { key: 'book', Icon: RiBookOpenLine, label: 'Docs' }, - { key: 'heart', Icon: RiHeartLine, label: 'Favorite' }, +export const PROJECT_ICONS: Array<{ key: string; Icon: IconName; label: string }> = [ + { key: 'code', Icon: 'code-box', label: 'Code' }, + { key: 'terminal', Icon: 'terminal-box', label: 'Terminal' }, + { key: 'rocket', Icon: 'rocket', label: 'Rocket' }, + { key: 'flask', Icon: 'flask', label: 'Lab' }, + { key: 'gamepad', Icon: 'gamepad', label: 'Game' }, + { key: 'briefcase', Icon: 'briefcase', label: 'Work' }, + { key: 'home', Icon: 'home', label: 'Home' }, + { key: 'globe', Icon: 'global', label: 'Web' }, + { key: 'leaf', Icon: 'leaf', label: 'Nature' }, + { key: 'shield', Icon: 'shield', label: 'Security' }, + { key: 'palette', Icon: 'palette', label: 'Design' }, + { key: 'server', Icon: 'server', label: 'Server' }, + { key: 'phone', Icon: 'smartphone', label: 'Mobile' }, + { key: 'database', Icon: 'database-2', label: 'Data' }, + { key: 'lightbulb', Icon: 'lightbulb', label: 'Idea' }, + { key: 'music', Icon: 'music', label: 'Music' }, + { key: 'camera', Icon: 'camera', label: 'Media' }, + { key: 'book', Icon: 'book-open', label: 'Docs' }, + { key: 'heart', Icon: 'heart', label: 'Favorite' }, ]; -export const PROJECT_ICON_MAP: Record = Object.fromEntries( +export const PROJECT_ICON_MAP: Record = Object.fromEntries( PROJECT_ICONS.map((i) => [i.key, i.Icon]) ); diff --git a/scripts/generate-icon-sprite.mjs b/scripts/generate-icon-sprite.mjs new file mode 100644 index 00000000..1839c3d1 --- /dev/null +++ b/scripts/generate-icon-sprite.mjs @@ -0,0 +1,250 @@ +/** + * Generates the SVG icon sprite file from @remixicon/react bundle. + * + * Usage: bun run scripts/generate-icon-sprite.mjs + * + * Reads the minified @remixicon/react bundle, extracts SVG path data + * for all Ri* icons used in packages/ui/src, and writes + * packages/ui/src/components/icon/sprite.ts. + */ + +import { readFileSync, writeFileSync } from "node:fs" +import { resolve, dirname } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const repoRoot = resolve(__dirname, "..") +const remixPath = resolve(repoRoot, "node_modules/@remixicon/react/index.mjs") + +const source = readFileSync(remixPath, "utf-8") + +// --- Step 1: extract variable → path mapping --- +// Pattern: const VARNAME=({color:...})=>...createElement("path",{d:"PATH_DATA"})..., +// Each icon is defined as `const X=...` where X is 1-4 chars. +const varPathMap = new Map() +const varRegex = /(?:[,;]const |\),)([A-Za-z0-9_$]{1,4})=\([{]color:/g +// Find all variable definitions and their boundaries +const varPositions = [] +let m +while ((m = varRegex.exec(source)) !== null) { + varPositions.push({ + varName: m[1], + start: m.index + m[0].length - 1, // first `{` after `=({color:` + }) +} + +for (let i = 0; i < varPositions.length; i++) { + const current = varPositions[i] + const next = varPositions[i + 1] + // End at the )), just before the next variable definition + const end = next + ? source.indexOf("))," + next.varName + "=(", current.start) + : source.length + if (end < 0 || end < current.start) continue + const segment = source.slice(current.start, end) + const pathRegex = /\w+\.createElement\("path",[{]d:"([^"]*)"/g + let pm + const paths = [] + while ((pm = pathRegex.exec(segment)) !== null) { + paths.push(pm[1]) + } + if (paths.length > 0) { + varPathMap.set(current.varName, paths) + } +} + +// --- Step 2: extract export mapping --- +// The export map is near the end of the file: +// export{V1 as Ri...Z2 as RiLast}; +const exportRegex = /export[{]([^}]+)[}]/ +const exportMatch = exportRegex.exec(source) +if (!exportMatch) { + console.error("Could not find export mapping in remixicon bundle") + process.exit(1) +} + +const nameToVar = new Map() +const entries = exportMatch[1].split(",") +for (const entry of entries) { + // Pattern: VAR as RiIconName + const parts = entry.trim().split(" as ") + if (parts.length === 2) { + nameToVar.set(parts[1].trim(), parts[0].trim()) + } +} + +// --- Step 3: find which icons we actually use --- +const srcDir = resolve(repoRoot, "packages/ui/src") + +// Helper: convert kebab-case name back to RiName +function nameToRi(kebab) { + // "arrow-down-sline" → RiArrowDownSline + const parts = kebab.split("-") + let result = "Ri" + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + if (i > 0 && /^\d/.test(part)) { + result += part[0].toUpperCase() + part.slice(1) + } else { + result += part.charAt(0).toUpperCase() + part.slice(1) + } + } + return result +} + +const srcFiles = [] +function walk(dir) { + const { readdirSync, statSync } = require("node:fs") + for (const entry of readdirSync(dir)) { + const full = resolve(dir, entry) + if (statSync(full).isDirectory()) { + if (entry === "node_modules") continue + walk(full) + } else if (/\.(tsx?|jsx?)$/.test(entry)) { + srcFiles.push(full) + } + } +} +import("node:fs").then(({ readdirSync, statSync: st }) => { + // Already imported above, use recursive function + function localWalk(dir) { + const { readdirSync: rd, statSync: s } = require("node:fs") + for (const entry of rd(dir)) { + const full = resolve(dir, entry) + try { + if (s(full).isDirectory()) { + if (entry === "node_modules") continue + localWalk(full) + } else if (/\.(tsx?)$/.test(entry)) { + srcFiles.push(full) + } + } catch {} + } + } + localWalk(srcDir) +}) + +// Finish step 3 synchronously with simpler approach +import { readdirSync, statSync } from "node:fs" +function findAllSourceFiles(dir) { + const results = [] + for (const entry of readdirSync(dir)) { + const full = resolve(dir, entry) + try { + const st = statSync(full) + if (st.isDirectory()) { + if (entry === "node_modules") continue + results.push(...findAllSourceFiles(full)) + } else if (/\.(tsx?)$/.test(entry)) { + results.push(full) + } + } catch { /* skip */ } + } + return results +} + +// Helper: convert kebab-case name back to RiX name +function nameToRi(kebab) { + const parts = kebab.split("-") + let result = "Ri" + for (let i = 0; i < parts.length; i++) { + result += parts[i].charAt(0).toUpperCase() + parts[i].slice(1) + } + return result +} + +const allSrcFiles = findAllSourceFiles(srcDir) +const usedIcons = new Set() +for (const file of allSrcFiles) { + const content = readFileSync(file, "utf-8") + // Match RiIcons from @remixicon/react imports + const iconRegex = /Ri[A-Z][A-Za-z0-9]+/g + let im + while ((im = iconRegex.exec(content)) !== null) { + if (nameToVar.has(im[0])) { + usedIcons.add(im[0]) + } + } + + // Also scan for patterns (already-migrated icons) + const iconNameRegex = /Icon\s+name="([^"]+)"/g + let nm + while ((nm = iconNameRegex.exec(content)) !== null) { + const kebab = nm[1] + for (const suffix of ["Line", "Fill", ""]) { + const riName = nameToRi(kebab) + suffix + if (nameToVar.has(riName)) { + usedIcons.add(riName) + break + } + } + } + + // Also scan for icon: 'kebab-name' in object literals (e.g. MODALITY_ICON_MAP) + const iconPropRegex = /icon:\s*'([a-z][a-z0-9-]*)'/g + let ip + while ((ip = iconPropRegex.exec(content)) !== null) { + const kebab = ip[1] + for (const suffix of ["Line", "Fill", ""]) { + const riName = nameToRi(kebab) + suffix + if (nameToVar.has(riName)) { + usedIcons.add(riName) + break + } + } + } +} + +console.log(`Found ${usedIcons.size} unique remixicon names used in source`) + +// --- Step 4: build sprite data --- +const iconEntries = [] +for (const iconName of [...usedIcons].sort()) { + const varName = nameToVar.get(iconName) + if (!varName) { + console.warn(` ⚠ Unknown icon: ${iconName}`) + continue + } + const paths = varPathMap.get(varName) + if (!paths || paths.length === 0) { + console.warn(` ⚠ No path data for: ${iconName} (var: ${varName})`) + continue + } + + // Build SVG content from paths + const svgContent = paths + .map((d) => ``) + .join("") + + iconEntries.push({ name: iconName, content: svgContent }) +} + +// --- Step 5: write sprite.ts --- +const remixToSpriteName = (name) => { + // RiArrowDownSLine → arrow-down-s + // RiGithubFill → github-fill (keep Fill for fill variants) + return name + .replace(/^Ri/, "") + .replace(/Line$/, "") + .replace(/([a-z])([A-Z0-9])/g, "$1-$2") + .replace(/([0-9])([A-Z])/g, "$1-$2") + .toLowerCase() +} + +const spriteLines = iconEntries.map(({ name, content }) => { + const spriteName = remixToSpriteName(name) + return ` "${spriteName}": \`${content}\`,` +}) + +const spriteContent = `// This file is auto-generated by scripts/generate-icon-sprite.mjs +// Do not edit manually. Run the script to update. + +export const iconSpriteData: Record = { +${spriteLines.join("\n")} +}; +` + +const outPath = resolve(repoRoot, "packages/ui/src/components/icon/sprite.ts") +writeFileSync(outPath, spriteContent, "utf-8") +console.log(`\n✅ Generated sprite data for ${iconEntries.length} icons → ${outPath}`) +console.log(` Total sprite size: ${Buffer.byteLength(spriteContent).toLocaleString()} bytes`)