feat: introduced a token-based theming system across the UI

* feat: added themes system

* feat: smart sidebar auto-hide for files/diff tabs + lower files sidebar threshold

* feat: added Checkbox component and update theming

- Add reusable Checkbox component for toggles across UI
- Replace several inputs with Checkbox in settings and commands panels
- Add DiffIcon and apply surface/border theming to key UI areas

* feat: Add convert-vscode-theme.cjs to convert VS Code themes to OpenChamber format

* refactor: remove unused permission logic from ChatInput

- Remove unused permission rules parsing logic from ChatInput
- Memoize renderTheme in DiffWorkerProvider to avoid unnecessary recalculations
- Remove forceOpaque helper in vscode theme adapter

* fix: guard VSCode theme loading in MarkdownRenderer

* feat: add custom user themes loading and reload

- Load user themes from ~/.config/openchamber/themes at runtime
- Expose /api/config/themes to fetch custom themes
- Allow theme reloading from Settings → Theme → Reload themes in the UI
This commit is contained in:
Bohdan Triapitsyn
2026-02-01 18:29:34 +02:00
committed by GitHub
parent 5bb2c56bc0
commit ddedc02687
147 changed files with 9853 additions and 2435 deletions
@@ -137,10 +137,10 @@ export const AgentMentionAutocomplete = React.forwardRef<AgentMentionAutocomplet
ref={(el) => {
itemRefs.current[index] = el;
}}
className={cn(
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
index === selectedIndex && 'bg-muted'
)}
className={cn(
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
index === selectedIndex && 'bg-interactive-selection'
)}
onClick={() => onAgentSelect(agent.name)}
onMouseEnter={() => setSelectedIndex(index)}
>
@@ -175,7 +175,7 @@ export const AgentMentionAutocomplete = React.forwardRef<AgentMentionAutocomplet
return (
<div
ref={containerRef}
className="absolute z-[100] min-w-0 w-full max-w-[360px] max-h-60 bg-background border border-border rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
className="absolute z-[100] min-w-0 w-full max-w-[360px] max-h-60 bg-background border-2 border-border/60 rounded-xl shadow-md bottom-full mb-2 left-0 flex flex-col"
>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
{agents.length ? (
@@ -298,13 +298,13 @@ export const ChatContainer: React.FC = () => {
<div className="relative bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 z-10">
{showScrollButton && sessionMessages.length > 0 && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2">
<Button
variant="outline"
size="sm"
onClick={() => scrollToBottom({ force: true })}
className="rounded-full h-8 w-8 p-0 shadow-none bg-background/95 hover:bg-accent"
aria-label="Scroll to bottom"
>
<Button
variant="outline"
size="sm"
onClick={() => scrollToBottom({ force: true })}
className="rounded-full h-8 w-8 p-0 shadow-none bg-background/95 hover:bg-interactive-hover"
aria-label="Scroll to bottom"
>
<RiArrowDownLine className="h-4 w-4" />
</Button>
@@ -2,7 +2,7 @@ import React from 'react';
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
import { TextLoop } from '@/components/ui/TextLoop';
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
import { useThemeSystem } from '@/contexts/useThemeSystem';
const phrases = [
"Fix the failing tests",
@@ -24,17 +24,10 @@ const phrases = [
];
const ChatEmptyState: React.FC = () => {
const themeContext = useOptionalThemeSystem();
const { currentTheme } = useThemeSystem();
let isDark = true;
if (themeContext) {
isDark = themeContext.currentTheme.metadata.variant !== 'light';
} else if (typeof window !== 'undefined') {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
// Same colors as face fill in OpenChamberLogo, but higher opacity for text readability
const textColor = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)';
// Use theme's muted foreground for secondary text
const textColor = currentTheme?.colors?.surface?.mutedForeground || 'var(--muted-foreground)';
return (
<div className="flex flex-col items-center justify-center min-h-full w-full gap-6">
@@ -60,7 +60,7 @@ export class ChatErrorBoundary extends React.Component<ChatErrorBoundaryProps, C
{this.state.error && (
<details className="text-xs font-mono bg-muted p-3 rounded">
<summary className="cursor-pointer hover:bg-muted/80">Error details</summary>
<summary className="cursor-pointer hover:bg-interactive-hover/80">Error details</summary>
<pre className="mt-2 overflow-x-auto">
{this.state.error.toString()}
</pre>
@@ -85,4 +85,4 @@ export class ChatErrorBoundary extends React.Component<ChatErrorBoundaryProps, C
return this.props.children;
}
}
}
+14 -131
View File
@@ -11,8 +11,7 @@ import { useSessionStore } from '@/stores/useSessionStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { useUIStore } from '@/stores/useUIStore';
import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
import type { AttachedFile, EditPermissionMode } from '@/stores/types/sessionTypes';
import { getEditModeColors } from '@/lib/permissions/editModeColors';
import type { AttachedFile } from '@/stores/types/sessionTypes';
import { AttachedFilesList } from './FileAttachment';
import { QueuedMessageChips } from './QueuedMessageChips';
import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete';
@@ -40,7 +39,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useContextStore } from '@/stores/contextStore';
import { useThemeSystem } from '@/contexts/useThemeSystem';
const MAX_VISIBLE_TEXTAREA_LINES = 8;
const EMPTY_QUEUE: QueuedMessage[] = [];
@@ -52,53 +51,6 @@ interface ChatInputProps {
const isPrimaryMode = (mode?: string) => mode === 'primary' || mode === 'all' || mode === undefined || mode === null;
type PermissionAction = 'allow' | 'ask' | 'deny';
type PermissionRule = { permission: string; pattern: string; action: PermissionAction };
const asPermissionRuleset = (value: unknown): PermissionRule[] | null => {
if (!Array.isArray(value)) {
return null;
}
const rules: PermissionRule[] = [];
for (const entry of value) {
if (!entry || typeof entry !== 'object') {
continue;
}
const candidate = entry as Partial<PermissionRule>;
if (typeof candidate.permission !== 'string' || typeof candidate.pattern !== 'string' || typeof candidate.action !== 'string') {
continue;
}
if (candidate.action !== 'allow' && candidate.action !== 'ask' && candidate.action !== 'deny') {
continue;
}
rules.push({ permission: candidate.permission, pattern: candidate.pattern, action: candidate.action });
}
return rules;
};
const resolveWildcardPermissionAction = (ruleset: unknown, permission: string): PermissionAction | undefined => {
const rules = asPermissionRuleset(ruleset);
if (!rules || rules.length === 0) {
return undefined;
}
for (let i = rules.length - 1; i >= 0; i -= 1) {
const rule = rules[i];
if (rule.permission === permission && rule.pattern === '*') {
return rule.action;
}
}
for (let i = rules.length - 1; i >= 0; i -= 1) {
const rule = rules[i];
if (rule.permission === '*' && rule.pattern === '*') {
return rule.action;
}
}
return undefined;
};
export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBottom }) => {
const [message, setMessage] = React.useState('');
const [isDragging, setIsDragging] = React.useState(false);
@@ -140,6 +92,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
const agents = getVisibleAgents();
const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius } = useUIStore();
const { working } = useAssistantStatus();
const { currentTheme } = useThemeSystem();
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const prevWasAbortedRef = React.useRef(false);
@@ -270,79 +223,6 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
}
}, [pendingInputText, consumePendingInputText]);
const currentAgent = React.useMemo(() => {
const selectedName = currentSessionId
? (useContextStore.getState().getSessionAgentSelection(currentSessionId) || currentAgentName)
: currentAgentName;
if (!selectedName) {
return undefined;
}
return agents.find((agent) => agent.name === selectedName);
}, [agents, currentAgentName, currentSessionId]);
const agentEditAction = React.useMemo<EditPermissionMode>(() => {
if (!currentAgent) {
return 'deny';
}
return resolveWildcardPermissionAction(currentAgent.permission, 'edit') ?? 'allow';
}, [currentAgent]);
const sessionEditMode = useContextStore(
React.useCallback((state) => {
if (!currentAgentName) {
return undefined;
}
const sessionId = currentSessionId ?? '__global__';
return state.getSessionAgentEditMode(sessionId, currentAgentName, 'ask');
}, [currentAgentName, currentSessionId])
);
const selectionContextReady = Boolean(currentSessionId && currentAgentName);
const effectiveEditPermission = React.useMemo<EditPermissionMode>(() => {
// Only show accent when edits are effectively allowed.
if (agentEditAction === 'allow') {
return 'allow';
}
if (agentEditAction !== 'ask') {
return 'ask';
}
const sessionMode = selectionContextReady ? (sessionEditMode ?? 'ask') : 'ask';
return (sessionMode === 'allow' || sessionMode === 'full') ? 'allow' : 'ask';
}, [agentEditAction, selectionContextReady, sessionEditMode]);
const chatInputAccent = React.useMemo(() => getEditModeColors(effectiveEditPermission), [effectiveEditPermission]);
// VS Code webviews tend to have stronger status border colors; in web/desktop themes the same
// border tokens can already be subtle, so avoid double-softening there.
const softenBorderColor = React.useCallback((color: string) => (
isVSCodeRuntime()
? `color-mix(in srgb, ${color} 55%, transparent)`
: color
), []);
const chatInputWrapperStyle = React.useMemo<React.CSSProperties | undefined>(() => {
// Keep border width stable so toggling modes doesn't shift layout.
const baseBorderWidth = isVSCodeRuntime() ? 1 : 2;
const baseStyle: React.CSSProperties = {
borderRadius: cornerRadius,
};
if (!chatInputAccent) {
return { ...baseStyle, borderWidth: baseBorderWidth };
}
const borderColor = chatInputAccent.border ?? chatInputAccent.text;
return {
...baseStyle,
borderColor: softenBorderColor(borderColor),
borderWidth: baseBorderWidth,
};
}, [chatInputAccent, softenBorderColor, cornerRadius]);
const hasContent = message.trim() || attachedFiles.length > 0;
const hasQueuedMessages = queuedMessages.length > 0;
const canSend = hasContent || hasQueuedMessages;
@@ -1378,7 +1258,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
<form
onSubmit={handleSubmit}
className={cn(
"relative pt-0 pb-2 md:pb-4",
"relative pt-0 pb-4",
isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area"
)}
data-keyboard-avoid="true"
@@ -1438,10 +1318,14 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
/>
<div
className={cn(
"border border-border/80 bg-input/10 dark:bg-input/30",
"flex flex-col relative overflow-visible"
"flex flex-col relative overflow-visible",
"border border-border/80",
"focus-within:ring-1 focus-within:ring-primary/50"
)}
style={chatInputWrapperStyle}
style={{
borderRadius: cornerRadius,
backgroundColor: currentTheme?.colors?.surface?.subtle,
}}
>
{stopButton}
{}
@@ -1493,12 +1377,11 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
? "# for agents; @ for files; / for commands"
: "Select or create a session to start chatting"}
disabled={!currentSessionId && !newSessionDraftOpen}
outerClassName="focus-within:ring-0"
className={cn(
'min-h-[52px] resize-none border-0 px-3 shadow-none rounded-b-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent',
'min-h-[52px] resize-none border-0 px-3 rounded-b-none appearance-none hover:border-transparent bg-transparent',
isMobile ? "py-2.5" : "pt-4 pb-2",
canAbort && 'pr-10',
"focus-visible:outline-none focus-visible:ring-0"
canAbort && 'pr-10'
)}
style={{
flex: 'none',
@@ -898,7 +898,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
displayParts.length === 0 ? null : (
<FadeInOnReveal>
<div className="flex justify-end">
<div className="max-w-[85%] rounded-2xl rounded-br-sm bg-primary/10 dark:bg-primary/10 px-5 py-3 shadow-sm border border-primary/5">
<div style={{ backgroundColor: 'var(--chat-user-message-bg)' }} className="max-w-[85%] rounded-2xl rounded-br-sm px-5 py-3 shadow-sm border border-primary/5">
<MessageBody
messageId={message.info.id}
parts={displayParts}
@@ -932,7 +932,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
</FadeInOnReveal>
)
) : (
<div className="relative pl-4 ml-1">
<div className="relative">
{shouldShowHeader && (
<MessageHeader
isUser={isUser}
@@ -232,7 +232,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
return (
<div
ref={containerRef}
className="absolute z-[100] min-w-0 w-full max-w-[450px] max-h-64 bg-background border border-border rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
className="absolute z-[100] min-w-0 w-full max-w-[450px] max-h-64 bg-background border-2 border-border/60 rounded-xl shadow-md bottom-full mb-2 left-0 flex flex-col"
>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
{loading ? (
@@ -251,7 +251,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
ref={(el) => { itemRefs.current[index] = el; }}
className={cn(
"flex items-start gap-2 px-3 py-2 cursor-pointer rounded-lg",
index === selectedIndex && "bg-muted"
index === selectedIndex && "bg-interactive-selection"
)}
onClick={() => onCommandSelect(command)}
onMouseEnter={() => setSelectedIndex(index)}
@@ -36,7 +36,7 @@ export const DiffPreview: React.FC<DiffPreviewProps> = ({ diff, syntaxTheme, fil
: {}
}
>
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{line.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
@@ -94,7 +94,7 @@ export const WritePreview: React.FC<WritePreviewProps> = ({ content, syntaxTheme
<div className="space-y-0">
{lines.map((line, lineIdx) => (
<div key={lineIdx} className="typography-code font-mono px-2 py-0.5 flex -mx-1">
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{lineIdx + 1}
</span>
<div className="flex-1 min-w-0">
@@ -295,18 +295,18 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
case 'tsx':
case 'js':
case 'jsx':
return <RiCodeLine className="h-3.5 w-3.5 text-blue-500" />;
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-info)]" />;
case 'json':
return <RiCodeLine className="h-3.5 w-3.5 text-yellow-500" />;
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-warning)]" />;
case 'md':
case 'mdx':
return <RiFileLine className="h-3.5 w-3.5 text-gray-500" />;
return <RiFileLine className="h-3.5 w-3.5 text-muted-foreground" />;
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
case 'svg':
return <RiFileImageLine className="h-3.5 w-3.5 text-green-500" />;
return <RiFileImageLine className="h-3.5 w-3.5 text-[var(--status-success)]" />;
default:
return <RiFilePdfLine className="h-3.5 w-3.5 text-muted-foreground" />;
}
@@ -315,7 +315,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
return (
<div
ref={containerRef}
className="absolute z-[100] min-w-0 w-full max-w-[520px] max-h-64 bg-background border border-border rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
className="absolute z-[100] min-w-0 w-full max-w-[520px] max-h-64 bg-background border-2 border-border/60 rounded-xl shadow-md bottom-full mb-2 left-0 flex flex-col"
>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0">
{loading ? (
@@ -334,10 +334,10 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
const item = (
<div
ref={(el) => { itemRefs.current[index] = el; }}
className={cn(
"flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label rounded-lg",
isSelected && "bg-muted"
)}
className={cn(
"flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label rounded-lg",
isSelected && "bg-interactive-selection"
)}
onClick={() => handleFileSelect(file)}
onMouseEnter={() => setSelectedIndex(index)}
>
@@ -5,8 +5,10 @@ import type { Part } from '@opencode-ai/sdk/v2';
import { cn } from '@/lib/utils';
import { RiFileCopyLine, RiCheckLine, RiDownloadLine } from '@remixicon/react';
import { flexokiStreamdownThemes } from '@/lib/shiki/flexokiThemes';
import { isVSCodeRuntime } from '@/lib/desktop';
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
import { getStreamdownThemePair } from '@/lib/shiki/appThemeRegistry';
import { getDefaultTheme } from '@/lib/theme/themes';
const withStableStringId = <T extends object>(value: T, id: string): T => {
const existingPrimitive = (value as Record<symbol, unknown>)[Symbol.toPrimitive];
@@ -43,45 +45,75 @@ const withStableStringId = <T extends object>(value: T, id: string): T => {
return value;
};
const getMarkdownShikiThemes = (): readonly [string | object, string | object] => {
if (!isVSCodeRuntime() || typeof window === 'undefined') {
return flexokiStreamdownThemes;
}
const provided = window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__;
if (provided?.light && provided?.dark) {
const light = withStableStringId(
{ ...(provided.light as Record<string, unknown>) },
`vscode-shiki-light:${String((provided.light as { name?: unknown })?.name ?? 'theme')}`,
);
const dark = withStableStringId(
{ ...(provided.dark as Record<string, unknown>) },
`vscode-shiki-dark:${String((provided.dark as { name?: unknown })?.name ?? 'theme')}`,
);
return [light, dark] as const;
}
return flexokiStreamdownThemes;
};
const useMarkdownShikiThemes = (): readonly [string | object, string | object] => {
const [themes, setThemes] = React.useState(getMarkdownShikiThemes);
const themeSystem = useOptionalThemeSystem();
const isVSCode = isVSCodeRuntime() && typeof window !== 'undefined';
const fallbackLight = getDefaultTheme(false);
const fallbackDark = getDefaultTheme(true);
const lightThemeId = themeSystem?.lightThemeId ?? fallbackLight.metadata.id;
const darkThemeId = themeSystem?.darkThemeId ?? fallbackDark.metadata.id;
const lightTheme =
themeSystem?.availableThemes.find((theme) => theme.metadata.id === lightThemeId) ??
fallbackLight;
const darkTheme =
themeSystem?.availableThemes.find((theme) => theme.metadata.id === darkThemeId) ??
fallbackDark;
const fallbackThemes = React.useMemo(
() => getStreamdownThemePair(lightTheme, darkTheme),
[darkTheme, lightTheme],
);
const getThemes = React.useCallback((): readonly [string | object, string | object] => {
if (!isVSCode) {
return fallbackThemes;
}
const provided = window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__;
if (provided?.light && provided?.dark) {
const light = withStableStringId(
{ ...(provided.light as Record<string, unknown>) },
`vscode-shiki-light:${String((provided.light as { name?: unknown })?.name ?? 'theme')}`,
);
const dark = withStableStringId(
{ ...(provided.dark as Record<string, unknown>) },
`vscode-shiki-dark:${String((provided.dark as { name?: unknown })?.name ?? 'theme')}`,
);
return [light, dark] as const;
}
return fallbackThemes;
}, [fallbackThemes, isVSCode]);
const [themes, setThemes] = React.useState(getThemes);
React.useEffect(() => {
if (!isVSCodeRuntime() || typeof window === 'undefined') return;
if (!isVSCode) {
return;
}
setThemes(getThemes());
}, [getThemes, isVSCode]);
React.useEffect(() => {
if (!isVSCode) return;
const handler = (event: Event) => {
// Rely on the canonical `window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__` that the webview updates
// before dispatching this event, so we always apply stable cache keys and avoid stale token reuse.
void event;
setThemes(getMarkdownShikiThemes());
setThemes(getThemes());
};
window.addEventListener('openchamber:vscode-shiki-themes', handler as EventListener);
return () => window.removeEventListener('openchamber:vscode-shiki-themes', handler as EventListener);
}, []);
}, [getThemes, isVSCode]);
return themes;
return isVSCode ? themes : fallbackThemes;
};
// Table utility functions
@@ -208,7 +240,7 @@ const TableCopyButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement | nul
<div className="relative" ref={menuRef}>
<button
onClick={() => setShowMenu(!showMenu)}
className="p-1 rounded hover:bg-muted/60 text-muted-foreground hover:text-foreground transition-colors"
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title="Copy table"
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
@@ -216,13 +248,13 @@ const TableCopyButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement | nul
{showMenu && (
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-lg">
<button
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/40"
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-interactive-hover/40"
onClick={() => handleCopy('csv')}
>
CSV
</button>
<button
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/40"
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-interactive-hover/40"
onClick={() => handleCopy('tsv')}
>
TSV
@@ -268,7 +300,7 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement |
<div className="relative" ref={menuRef}>
<button
onClick={() => setShowMenu(!showMenu)}
className="p-1 rounded hover:bg-muted/60 text-muted-foreground hover:text-foreground transition-colors"
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title="Download table"
>
<RiDownloadLine className="size-3.5" />
@@ -276,13 +308,13 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement |
{showMenu && (
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-lg">
<button
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/40"
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-interactive-hover/40"
onClick={() => handleDownload('csv')}
>
CSV
</button>
<button
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/40"
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-interactive-hover/40"
onClick={() => handleDownload('markdown')}
>
Markdown
@@ -395,7 +427,7 @@ const CodeBlockWrapper: React.FC<CodeBlockWrapperProps> = ({ children, className
<div className="absolute top-1 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={handleCopy}
className="p-1 rounded hover:bg-muted/60 text-muted-foreground hover:text-foreground transition-colors"
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title="Copy"
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
@@ -1371,7 +1371,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
value={mobileModelQuery}
onChange={(event) => setMobileModelQuery(event.target.value)}
placeholder="Search providers or models"
className="pl-7 h-9 rounded-xl border-border/40 bg-background/95 typography-meta"
className="pl-7 h-9 rounded-xl border-border/40 bg-[var(--surface-elevated)] typography-meta"
/>
{mobileModelQuery && (
<button
@@ -1394,7 +1394,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{/* Favorites Section for Mobile */}
{!mobileModelQuery && favoriteModelsList.length > 0 && (
<div className="rounded-xl border border-border/40 bg-background/95 overflow-hidden">
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
<RiStarFill className="h-3 w-3 inline-block mr-1.5 text-primary" />
Favorites
@@ -1413,7 +1413,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 text-left last:border-b-0',
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
'first:rounded-t-xl last:rounded-b-xl transition-colors',
isSelected ? 'bg-primary/15 text-primary' : 'hover:bg-muted'
isSelected ? 'bg-interactive-selection/15 text-interactive-selection-foreground' : 'hover:bg-interactive-hover'
)}
>
<div className="flex items-center gap-2 min-w-0">
@@ -1440,7 +1440,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{/* Recent Section for Mobile */}
{!mobileModelQuery && recentModelsList.length > 0 && (
<div className="rounded-xl border border-border/40 bg-background/95 overflow-hidden">
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
<RiTimeLine className="h-3 w-3 inline-block mr-1.5" />
Recent
@@ -1459,7 +1459,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 text-left last:border-b-0',
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
'first:rounded-t-xl last:rounded-b-xl transition-colors',
isSelected ? 'bg-primary/15 text-primary' : 'hover:bg-muted'
isSelected ? 'bg-interactive-selection/15 text-interactive-selection-foreground' : 'hover:bg-interactive-hover'
)}
>
<div className="flex items-center gap-2 min-w-0">
@@ -1492,8 +1492,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const isActiveProvider = provider.id === currentProviderId;
const isExpanded = expandedMobileProviders.has(provider.id) || normalizedQuery.length > 0;
return (
<div key={provider.id} className="rounded-xl border border-border/40 bg-background/95 overflow-hidden">
return (
<div key={provider.id} className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
<button
type="button"
onClick={() => toggleMobileProviderExpansion(provider.id)}
@@ -1533,9 +1533,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
className={cn(
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 last:border-b-0',
'rounded-lg transition-colors',
!isSelected && 'hover:bg-muted',
!isSelected && 'hover:bg-interactive-hover',
isSelected
? 'bg-primary/15 text-primary'
? 'bg-interactive-selection/15 text-interactive-selection-foreground'
: ''
)}
>
@@ -1584,9 +1584,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
toggleFavoriteModel(provider.id as string, model.id as string);
}}
className={cn(
"model-favorite-button flex h-5 w-5 items-center justify-center hover:text-yellow-600 flex-shrink-0",
"model-favorite-button flex h-5 w-5 items-center justify-center hover:text-primary/80 flex-shrink-0",
isFavoriteModel(provider.id as string, model.id as string)
? "text-yellow-500"
? "text-primary"
: "text-muted-foreground"
)}
aria-label={isFavoriteModel(provider.id as string, model.id as string) ? "Unfavorite" : "Favorite"}
@@ -1857,7 +1857,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
ref={(el) => { modelItemRefs.current[flatIndex] = el; }}
className={cn(
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
isHighlighted ? "bg-accent" : "hover:bg-accent/50"
isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
)}
onClick={() => handleProviderAndModelChange(providerID, modelID)}
onMouseEnter={() => setModelSelectedIndex(flatIndex)}
@@ -1901,8 +1901,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
toggleFavoriteModel(providerID, modelID);
}}
className={cn(
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-yellow-600",
isFavorite ? "text-yellow-500" : "text-muted-foreground"
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-primary/80",
isFavorite ? "text-primary" : "text-muted-foreground"
)}
aria-label={isFavorite ? "Unfavorite" : "Favorite"}
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
@@ -2021,7 +2021,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuTrigger asChild>
<div
className={cn(
'model-controls__model-trigger flex items-center gap-1.5 cursor-pointer hover:opacity-70 min-w-0',
'model-controls__model-trigger flex items-center gap-1.5 cursor-pointer hover:bg-transparent hover:opacity-70 min-w-0',
buttonHeight
)}
>
@@ -2082,7 +2082,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{/* Favorites Section */}
{filteredFavorites.length > 0 && (
<>
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<DropdownMenuLabel
style={{ backgroundColor: 'var(--surface-elevated)' }}
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
>
<RiStarFill className="h-4 w-4 text-primary" />
Favorites
</DropdownMenuLabel>
@@ -2097,7 +2100,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{filteredRecents.length > 0 && (
<>
{filteredFavorites.length > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<DropdownMenuLabel
style={{ backgroundColor: 'var(--surface-elevated)' }}
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
>
<RiTimeLine className="h-4 w-4" />
Recent
</DropdownMenuLabel>
@@ -2117,7 +2123,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{filteredProviders.map((provider, index) => (
<React.Fragment key={provider.id}>
{index > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<DropdownMenuLabel
style={{ backgroundColor: 'var(--surface-elevated)' }}
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
>
<ProviderLogo
providerId={provider.id}
className="h-4 w-4 flex-shrink-0"
@@ -2148,7 +2157,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onTouchCancel={handleLongPressEnd}
className={cn(
'model-controls__model-trigger flex items-center gap-1.5 min-w-0 focus:outline-none',
'cursor-pointer hover:opacity-70',
'cursor-pointer hover:bg-transparent hover:opacity-70',
buttonHeight
)}
>
@@ -2317,7 +2326,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
className={cn(
'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
buttonHeight,
'cursor-pointer hover:opacity-70',
'cursor-pointer hover:bg-transparent hover:opacity-70',
)}
>
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
@@ -2341,7 +2350,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuTrigger asChild>
<div
className={cn(
'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity cursor-pointer hover:opacity-70 min-w-0',
'model-controls__variant-trigger flex items-center gap-1.5 transition-colors cursor-pointer hover:bg-transparent hover:opacity-70 min-w-0',
buttonHeight,
)}
>
@@ -2403,7 +2412,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<div className={cn(
'flex items-center gap-1.5 transition-opacity cursor-pointer hover:opacity-70 min-w-0',
'flex items-center gap-1.5 transition-colors cursor-pointer hover:bg-transparent hover:opacity-70 min-w-0',
buttonHeight
)}>
<RiAiAgentLine
@@ -2485,9 +2494,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onTouchEnd={handleLongPressEnd}
onTouchCancel={handleLongPressEnd}
className={cn(
'model-controls__agent-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
'model-controls__agent-trigger flex items-center gap-1.5 transition-colors min-w-0 focus:outline-none',
buttonHeight,
'cursor-pointer hover:opacity-70',
'cursor-pointer hover:bg-transparent hover:opacity-70',
)}
>
<RiAiAgentLine
@@ -313,7 +313,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
<div className="px-2 py-1.5 border-b border-border/20 bg-muted/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<RiQuestionLine className="h-3.5 w-3.5 text-yellow-500" />
<RiQuestionLine className="h-3.5 w-3.5 text-[var(--status-warning)]" />
<span className="typography-meta font-medium text-muted-foreground">
Permission Required
</span>
@@ -1,5 +1,6 @@
import React from 'react';
import { RiCheckLine, RiCheckboxCircleFill, RiCircleLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react';
import { RiCheckLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import type { QuestionRequest } from '@/types/question';
@@ -191,8 +192,8 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
className={cn(
'px-2 py-0.5 typography-meta font-medium rounded transition-colors flex items-center gap-1',
isActive
? 'bg-muted/40 text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/20'
? 'bg-interactive-selection/40 text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-interactive-hover/20'
)}
>
{isSummary ? <RiListCheck3 className="h-3 w-3" /> : null}
@@ -214,7 +215,7 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
key={index}
type="button"
onClick={() => setActiveTab(String(index))}
className="w-full text-left rounded px-1.5 py-1 hover:bg-muted/20 transition-colors"
className="w-full text-left rounded px-1.5 py-1 hover:bg-interactive-hover/20 transition-colors"
>
<div className="typography-micro text-muted-foreground">{q.header || `Question ${index + 1}`}</div>
<div className={cn(
@@ -248,18 +249,18 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
disabled={isResponding}
className={cn(
'w-full px-1.5 py-1 text-left rounded transition-colors',
'hover:bg-muted/30',
selected ? 'bg-muted/20' : null,
'hover:bg-interactive-hover/30',
selected ? 'bg-interactive-selection/20' : null,
isResponding ? 'opacity-60 cursor-not-allowed' : null
)}
>
<div className="flex items-start gap-2">
<div className="mt-0.5 shrink-0">
{selected ? (
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-primary" />
) : (
<RiCircleLine className="h-3.5 w-3.5 text-muted-foreground/50" />
)}
<Checkbox
checked={selected}
onChange={() => handleToggleOption(option.label)}
disabled={isResponding}
/>
</div>
<div className="min-w-0 flex-1">
@@ -290,8 +291,8 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
disabled={isResponding}
className={cn(
'w-full px-1.5 py-1 text-left rounded transition-colors',
'hover:bg-muted/30',
isCustomActive ? 'bg-muted/20' : null,
'hover:bg-interactive-hover/30',
isCustomActive ? 'bg-interactive-selection/20' : null,
isResponding ? 'opacity-60 cursor-not-allowed' : null
)}
>
@@ -269,18 +269,18 @@ export const ServerFilePicker: React.FC<ServerFilePickerProps> = ({
case 'css':
case 'scss':
case 'less':
return <RiCodeLine className="h-3.5 w-3.5 text-blue-500" />;
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-info)]" />;
case 'json':
return <RiCodeLine className="h-3.5 w-3.5 text-yellow-500" />;
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-warning)]" />;
case 'md':
case 'mdx':
return <RiFileTextLine className="h-3.5 w-3.5 text-gray-500" />;
return <RiFileTextLine className="h-3.5 w-3.5 text-muted-foreground" />;
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
case 'svg':
return <RiFileImageLine className="h-3.5 w-3.5 text-green-500" />;
return <RiFileImageLine className="h-3.5 w-3.5 text-[var(--status-success)]" />;
default:
return <RiFileTextLine className="h-3.5 w-3.5 text-muted-foreground" />;
}
@@ -381,7 +381,7 @@ export const ServerFilePicker: React.FC<ServerFilePickerProps> = ({
const row = (
<div
className={cn(
"flex w-full items-center justify-start gap-1 px-2 py-1.5 rounded hover:bg-muted cursor-pointer typography-ui-label text-foreground text-left",
"flex w-full items-center justify-start gap-1 px-2 py-1.5 rounded hover:bg-interactive-hover cursor-pointer typography-ui-label text-foreground text-left",
file.type === 'file' && selectedFiles.has(file.path) && "bg-primary/10"
)}
style={{ paddingLeft: `${level * 12}px` }}
@@ -514,7 +514,7 @@ export const ServerFilePicker: React.FC<ServerFilePickerProps> = ({
e.stopPropagation();
setSearchQuery('');
}}
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-muted rounded"
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-interactive-hover rounded"
>
<RiCloseLine className="h-3 w-3"/>
</button>
@@ -114,10 +114,10 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
ref={(el) => {
itemRefs.current[index] = el;
}}
className={cn(
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
index === selectedIndex && 'bg-muted'
)}
className={cn(
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
index === selectedIndex && 'bg-interactive-selection'
)}
onClick={() => onSkillSelect(skill.name)}
onMouseEnter={() => setSelectedIndex(index)}
>
@@ -146,7 +146,7 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
return (
<div
ref={containerRef}
className="absolute z-[100] min-w-0 w-full max-w-[360px] max-h-60 bg-background border border-border rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
className="absolute z-[100] min-w-0 w-full max-w-[360px] max-h-60 bg-background border-2 border-border/60 rounded-xl shadow-md bottom-full mb-2 left-0 flex flex-col"
>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
{filteredSkills.length ? (
@@ -41,7 +41,7 @@ export const StatusChip: React.FC<StatusChipProps> = ({ onClick, className }) =>
'inline-flex min-w-0 items-center justify-center',
'rounded-lg border border-border/50 px-1.5',
'typography-meta font-medium text-foreground/80',
'focus:outline-none',
'focus:outline-none hover:bg-[var(--interactive-hover)]',
className
)}
style={{
@@ -117,7 +117,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({ open, onOpenChan
return (
<div
key={message.info.id}
className="group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer"
className="group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer"
onClick={() => {
onScrollToMessage?.(message.info.id);
onOpenChange(false);
@@ -323,7 +323,7 @@ export const UnifiedControlsDrawer: React.FC<UnifiedControlsDrawerProps> = ({
'inline-flex items-center rounded-full border px-2.5 py-1 typography-meta font-medium',
isSelected
? 'border-primary/30 bg-primary/10 text-foreground'
: 'border-border/40 text-muted-foreground hover:bg-muted/50'
: 'border-border/40 text-muted-foreground hover:bg-interactive-hover/50'
)}
aria-pressed={isSelected}
>
@@ -335,7 +335,7 @@ export const UnifiedControlsDrawer: React.FC<UnifiedControlsDrawerProps> = ({
<button
type="button"
onClick={onOpenEffort}
className="inline-flex items-center rounded-full border border-border/40 px-2.5 py-1 typography-meta font-medium text-muted-foreground hover:bg-muted/50"
className="inline-flex items-center rounded-full border border-border/40 px-2.5 py-1 typography-meta font-medium text-muted-foreground hover:bg-interactive-hover/50"
aria-label="More effort options"
>
...
@@ -209,7 +209,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
: {}),
}}
>
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-3 self-start select-none">
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{line.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
@@ -282,7 +282,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
}}
>
<div className="flex">
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-3 self-start select-none">
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{line.leftLine.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
@@ -336,7 +336,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
}}
>
<div className="flex">
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-3 self-start select-none">
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{line.rightLine.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
@@ -500,12 +500,12 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
const shouldShowLineNumber = !isInfo && (limit === undefined || idx < limit);
return (
<div key={idx} className={`typography-code font-mono flex ${isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-4 self-start select-none">
{shouldShowLineNumber ? lineNumber : ''}
</span>
<div className="flex-1 min-w-0">
return (
<div key={idx} className={`typography-code font-mono flex ${isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{shouldShowLineNumber ? lineNumber : ''}
</span>
<div className="flex-1 min-w-0">
{isInfo ? (
<div className="whitespace-pre-wrap break-words">{line}</div>
) : (
@@ -159,6 +159,7 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
isExpanded && 'opacity-0',
!isExpanded && !isMobile && 'group-hover/tool:opacity-0'
)}
style={{ color: 'var(--tools-icon)' }}
>
<RiStackLine className="h-3.5 w-3.5" />
</div>
@@ -178,10 +179,10 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
</div>
</>
) : (
<RiStackLine className="h-3.5 w-3.5" />
<RiStackLine className="h-3.5 w-3.5" style={{ color: 'var(--tools-icon)' }} />
)}
</div>
<span className="typography-meta font-medium">Activity</span>
<span className="typography-meta font-medium" style={{ color: 'var(--tools-title)' }}>Activity</span>
</div>
{diffStats && (diffStats.additions > 0 || diffStats.deletions > 0) && (
@@ -201,11 +202,13 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
<div
className={cn(
'relative pr-2 pb-1 pt-1 pl-[1.4375rem]',
'before:absolute before:left-[0.4375rem] before:w-px before:bg-border/80 before:content-[""]',
'before:top-[-0.25rem] before:bottom-0'
'relative pr-2 pb-1 pt-1 pl-[1.4375rem]'
)}
>
<div
className="absolute left-[0.4375rem] w-px top-[-0.25rem] bottom-0"
style={{ backgroundColor: 'var(--tools-border)', borderWidth: '0', width: '1px' }}
></div>
{!isExpanded && hiddenCount > 0 && (
<div
className="typography-micro text-muted-foreground/70 mb-1 cursor-pointer hover:text-muted-foreground"
@@ -256,7 +256,8 @@ const ToolScrollableSection: React.FC<ToolScrollableSectionProps> = ({
}) => (
<ScrollableOverlay
outerClassName={cn('w-full min-w-0 flex-none overflow-hidden', maxHeightClass, outerClassName)}
className={cn('tool-output-surface p-2 rounded-xl w-full min-w-0 border border-border/20 bg-transparent', className)}
className={cn('tool-output-surface p-2 rounded-xl w-full min-w-0 bg-transparent', className)}
style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}
disableHorizontal={disableHorizontal}
>
<div className="w-full min-w-0">
@@ -410,8 +411,8 @@ interface DiffPreviewProps {
const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme, input }) => (
<div className="typography-code px-1 pb-1 pt-0 space-y-0">
{parseDiffToUnified(diff).map((hunk, hunkIdx) => (
<div key={hunkIdx} className="-mx-1 px-1 border-b border-border/20 last:border-b-0">
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground border-b border-border/10 break-words -mx-1">
<div key={hunkIdx} className="-mx-1 px-1 last:border-b-0" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground break-words -mx-1" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
{`${hunk.file} (line ${hunk.oldStart})`}
</div>
@@ -433,7 +434,7 @@ const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme,
: {}
}
>
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{line.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
@@ -454,9 +455,15 @@ const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme,
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
overflowWrap: 'anywhere',
color: line.type === 'removed' ? 'var(--tools-edit-removed)' : line.type === 'added' ? 'var(--tools-edit-added)' : 'inherit',
}}
codeTagProps={{
style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' },
style: {
background: 'transparent',
backgroundColor: 'transparent',
fontSize: 'inherit',
color: line.type === 'removed' ? 'var(--tools-edit-removed)' : line.type === 'added' ? 'var(--tools-edit-added)' : 'inherit',
},
}}
>
{line.content}
@@ -491,13 +498,13 @@ const WriteInputPreview: React.FC<WriteInputPreviewProps> = React.memo(({ conten
return (
<div className="w-full min-w-0">
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground border border-border/10 rounded-lg mb-1">
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground rounded-lg mb-1" style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}>
{`${displayPath} (${headerLineLabel})`}
</div>
<div className="space-y-0">
{lines.map((line, lineIdx) => (
<div key={lineIdx} className="typography-code font-mono px-2 py-0.5 flex -mx-1">
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{lineIdx + 1}
</span>
<div className="flex-1 min-w-0">
@@ -560,10 +567,10 @@ const ImagePreview: React.FC<ImagePreviewProps> = React.memo(({ content, filePat
return (
<div className="w-full min-w-0">
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground border border-border/10 rounded-lg mb-2">
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground rounded-lg mb-2" style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}>
{displayPath}
</div>
<div className="flex justify-center p-4 bg-muted/10 rounded-lg border border-border/10">
<div className="flex justify-center p-4 bg-muted/10 rounded-lg" style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}>
<img
src={imageSrc}
alt={displayPath}
@@ -815,7 +822,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
return (
<div key={idx} className={cn('typography-code font-mono flex w-full min-w-0', isInfo && 'text-muted-foreground/70 italic')}>
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-3 self-start select-none">
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{shouldShowLineNumber ? lineNumber : ''}
</span>
<div className="flex-1 min-w-0">
@@ -892,12 +899,18 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
return (
<div
className={cn(
'relative pr-2 pb-2 pt-2 space-y-2 pl-[1.4375rem]',
'before:absolute before:left-[0.4375rem] before:w-px before:bg-border/80 before:content-[""]',
hasPrevTool ? 'before:top-[-0.45rem]' : 'before:top-[-0.25rem]',
hasNextTool ? 'before:bottom-[-0.6rem]' : 'before:bottom-0'
'relative pr-2 pb-2 pt-2 space-y-2 pl-[1.4375rem]'
)}
>
<div
className="absolute left-[0.4375rem] w-px"
style={{
backgroundColor: 'var(--tools-border)',
top: hasPrevTool ? '-0.45rem' : '-0.25rem',
bottom: hasNextTool ? '-0.6rem' : '0',
width: '1px'
}}
></div>
{(part.tool === 'todowrite' || part.tool === 'todoread' || part.tool === 'question') ? (
renderResultContent()
) : (
@@ -1109,7 +1122,7 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
isExpanded && 'opacity-0',
!isExpanded && !isMobile && 'group-hover/tool:opacity-0'
)}
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : {}}
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : { color: 'var(--tools-icon)' }}
>
{getToolIcon(part.tool)}
</div>
@@ -1127,13 +1140,13 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
</div>
<span
className="typography-meta font-medium"
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : {}}
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : { color: 'var(--tools-title)' }}
>
{displayName}
</span>
</div>
<div className="flex items-center gap-1 flex-1 min-w-0 typography-meta text-muted-foreground/70">
<div className="flex items-center gap-1 flex-1 min-w-0 typography-meta" style={{ color: 'var(--tools-description)' }}>
{description && (
<span className={cn("truncate", isMobile && "max-w-[120px]")}>
{description}
@@ -0,0 +1,27 @@
import type { SVGProps } from 'react';
interface DiffIconProps extends Omit<SVGProps<SVGSVGElement>, 'children'> {
size?: number | string;
}
/**
* Git merge/branch icon for the Diff tab.
*/
export function DiffIcon({ size, className, style, ...props }: DiffIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={className}
style={{
width: typeof size === 'number' ? `${size}px` : size,
height: typeof size === 'number' ? `${size}px` : size,
...style,
}}
{...props}
>
<path d="M112,148a12,12,0,0,0-12,12v19L69.17,148.2A4,4,0,0,1,68,145.37V97.94a36,36,0,1,0-24,0v47.43a27.81,27.81,0,0,0,8.2,19.8L83,196H64a12,12,0,0,0,0,24h48a12,12,0,0,0,12-12V160A12,12,0,0,0,112,148ZM56,52A12,12,0,1,1,44,64,12,12,0,0,1,56,52ZM212,158.06V110.63a27.81,27.81,0,0,0-8.2-19.8L173,60h19a12,12,0,0,0,0-24H144a12,12,0,0,0-12,12V96a12,12,0,0,0,24,0V77l30.83,30.83a4,4,0,0,1,1.17,2.83v47.43a36,36,0,1,0,24,0ZM200,204a12,12,0,1,1,12-12A12,12,0,0,1,200,204Z" />
</svg>
);
}
+71 -25
View File
@@ -13,7 +13,8 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { RiArrowLeftSLine, RiChat4Line, RiCheckLine, RiCodeLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
import { RiArrowLeftSLine, RiChat4Line, RiCheckLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
import { DiffIcon } from '@/components/icons/DiffIcon';
import { useUIStore, type MainTab } from '@/stores/useUIStore';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -64,7 +65,7 @@ const resolveTilde = (path: string, homeDir: string | null): string => {
interface TabConfig {
id: MainTab;
label: string;
icon: RemixiconComponentType;
icon: RemixiconComponentType | 'diff';
badge?: number;
showDot?: boolean;
}
@@ -304,7 +305,7 @@ export const Header: React.FC = () => {
setSettingsDialogOpen(true);
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors';
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-interactive-hover transition-colors';
const desktopPaddingClass = React.useMemo(() => {
if (isDesktopApp && isMacPlatform) {
@@ -415,7 +416,7 @@ export const Header: React.FC = () => {
{
id: 'diff',
label: 'Diff',
icon: RiCodeLine,
icon: 'diff',
badge: !isMobile && diffFileCount > 0 ? diffFileCount : undefined,
},
{ id: 'files', label: 'Files', icon: RiFolder6Line },
@@ -447,18 +448,34 @@ export const Header: React.FC = () => {
const renderTab = (tab: TabConfig) => {
const isActive = activeMainTab === tab.id;
const Icon = tab.icon;
const isDiffTab = tab.icon === 'diff';
const Icon = isDiffTab ? null : (tab.icon as RemixiconComponentType);
const isChatTab = tab.id === 'chat';
const showContextTooltip = isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0;
return (
const renderIcon = (iconSize: number) => {
if (isDiffTab) {
return <DiffIcon size={iconSize} />;
}
return Icon ? <Icon size={iconSize} /> : null;
};
const formatTokens = (tokens: number) => {
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`;
return tokens.toFixed(1).replace(/\.0$/, '');
};
const tabButton = (
<button
key={tab.id}
type="button"
onClick={() => setActiveMainTab(tab.id)}
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
className={cn(
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
isActive
? 'app-region-drag bg-interactive-selection text-interactive-selection-foreground shadow-sm'
: 'app-region-no-drag text-muted-foreground hover:bg-interactive-hover/50 hover:text-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
isChatTab && !isMobile && 'min-w-[100px] justify-center'
)}
@@ -467,33 +484,57 @@ export const Header: React.FC = () => {
role="tab"
>
{isMobile ? (
<Icon size={20} />
renderIcon(20)
) : (
<>
<Icon size={16} />
{renderIcon(16)}
<span className="header-tab-label">{tab.label}</span>
</>
)}
{isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0 && (
<span className="ml-1">
<ContextUsageDisplay
totalTokens={contextUsage.totalTokens}
percentage={contextUsage.percentage}
contextLimit={contextUsage.contextLimit}
outputLimit={contextUsage.outputLimit ?? 0}
size="compact"
/>
{showContextTooltip && (
<span className="header-tab-badge">
<div className={cn(
'app-region-no-drag flex items-center gap-1.5 text-muted-foreground/60 select-none typography-micro',
)}>
<span className={cn(
'font-medium',
contextUsage.percentage >= 90 ? 'text-status-error' :
contextUsage.percentage >= 75 ? 'text-status-warning' : 'text-status-success'
)}>
{Math.min(contextUsage.percentage, 999).toFixed(1)}%
</span>
</div>
</span>
)}
{tab.badge !== undefined && tab.badge > 0 && (
<span className="ml-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary/10 px-1 text-[10px] font-bold text-primary">
<span className="header-tab-badge typography-micro text-status-info font-medium">
{tab.badge}
</span>
)}
</button>
);
if (showContextTooltip) {
const safeOutputLimit = typeof contextUsage.outputLimit === 'number' ? Math.max(contextUsage.outputLimit, 0) : 0;
return (
<Tooltip key={tab.id} delayDuration={1000}>
<TooltipTrigger asChild>
{tabButton}
</TooltipTrigger>
<TooltipContent>
<div className="space-y-0.5">
<p className="typography-micro leading-tight">Used tokens: {formatTokens(contextUsage.totalTokens)}</p>
<p className="typography-micro leading-tight">Context limit: {formatTokens(contextUsage.contextLimit)}</p>
<p className="typography-micro leading-tight">Output limit: {formatTokens(safeOutputLimit)}</p>
</div>
</TooltipContent>
</Tooltip>
);
}
return <React.Fragment key={tab.id}>{tabButton}</React.Fragment>;
};
const renderDesktop = () => (
@@ -662,7 +703,7 @@ export const Header: React.FC = () => {
{isSessionSwitcherOpen ? (
<button
onClick={() => setSessionSwitcherOpen(false)}
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-secondary"
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-interactive-active"
aria-label="Back"
>
<RiArrowLeftSLine className="h-5 w-5" />
@@ -670,7 +711,7 @@ export const Header: React.FC = () => {
) : (
<button
onClick={handleOpenSessionSwitcher}
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-secondary"
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-interactive-active"
aria-label="Open sessions"
>
<RiPlayListAddLine className="h-5 w-5" />
@@ -697,7 +738,8 @@ export const Header: React.FC = () => {
<div className="flex items-center gap-0.5" role="tablist" aria-label="Main navigation">
{tabs.map((tab) => {
const isActive = activeMainTab === tab.id;
const Icon = tab.icon;
const isDiffTab = tab.icon === 'diff';
const Icon = isDiffTab ? null : (tab.icon as RemixiconComponentType);
return (
<Tooltip key={tab.id} delayDuration={500}>
<TooltipTrigger asChild>
@@ -715,10 +757,14 @@ export const Header: React.FC = () => {
className={cn(
headerIconButtonClass,
'relative',
isActive && 'text-foreground bg-secondary'
isActive && 'bg-interactive-selection text-interactive-selection-foreground'
)}
>
<Icon className="h-5 w-5" />
{isDiffTab ? (
<DiffIcon className="h-5 w-5" />
) : Icon ? (
<Icon className="h-5 w-5" />
) : null}
{tab.badge !== undefined && tab.badge > 0 && (
<span className="absolute -top-1 -right-1 text-[10px] font-semibold text-primary">
{tab.badge}
@@ -182,14 +182,14 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
<div className="flex-1 overflow-hidden">
<ErrorBoundary>{children}</ErrorBoundary>
</div>
<div className="flex-shrink-0 border-t border-border h-12 px-2 bg-sidebar-accent/10">
<div className="flex-shrink-0 border-t border-border h-12 px-2 bg-sidebar">
<div className="flex h-full items-center justify-between gap-2">
<button
onClick={() => setSettingsDialogOpen(true)}
className={cn(
'flex h-8 items-center gap-2 rounded-md px-2',
'text-sm font-semibold text-sidebar-foreground/90',
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
'hover:text-sidebar-foreground hover:bg-interactive-hover',
'transition-all duration-200'
)}
>
@@ -219,7 +219,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
className={cn(
'flex h-8 w-8 items-center justify-center rounded-md',
'text-sidebar-foreground/70',
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
'hover:text-sidebar-foreground hover:bg-interactive-hover',
'transition-all duration-200'
)}
>
@@ -127,7 +127,7 @@ export const McpDropdown: React.FC<McpDropdownProps> = ({ headerIconButtonClass
return (
<div
key={serverName}
className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-lg hover:bg-muted/50"
className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-lg hover:bg-interactive-hover/50"
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 min-w-0">
@@ -235,7 +235,7 @@ export const McpDropdown: React.FC<McpDropdownProps> = ({ headerIconButtonClass
<div className="flex items-center gap-1">
<button
type="button"
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover transition-colors"
disabled={isSpinning}
onClick={handleRefresh}
aria-label="Refresh"
@@ -281,7 +281,7 @@ export const McpDropdown: React.FC<McpDropdownProps> = ({ headerIconButtonClass
<span className="typography-ui-label font-semibold">MCP Servers</span>
<button
type="button"
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover transition-colors"
disabled={isSpinning}
onClick={handleRefresh}
aria-label="Refresh"
@@ -68,7 +68,7 @@ export const ModelChip: React.FC<{
const label = totalSameModel > 1 ? `${displayName} (${instanceIndex})` : displayName;
return (
<div className={cn('flex items-center gap-1.5 px-2 rounded-md bg-accent/50 border border-border/30', CHIP_HEIGHT_CLASS)}>
<div className={cn('flex items-center gap-1.5 px-2 rounded-md bg-interactive-selection/20 border border-border/30', CHIP_HEIGHT_CLASS)}>
<ProviderLogo providerId={model.providerID} className="h-3.5 w-3.5" />
<span className="typography-meta font-medium truncate max-w-[140px]">
{label}
@@ -273,7 +273,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
onMouseEnter={() => setSelectedIndex(flatIndex)}
className={cn(
'w-full text-left px-2 py-1.5 rounded-md typography-meta transition-colors flex items-center gap-2',
isHighlighted ? 'bg-accent' : 'hover:bg-accent/50'
isHighlighted ? 'bg-interactive-selection' : 'hover:bg-interactive-hover/50'
)}
>
<div className="flex items-center gap-1.5 flex-1 min-w-0">
@@ -379,7 +379,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
let currentFlatIndex = 0;
return (
<div className="absolute bottom-full left-0 mb-1 z-50 border border-border/30 rounded-xl overflow-hidden bg-background shadow-lg w-[min(380px,calc(100vw-2rem))] flex flex-col">
<div style={{ backgroundColor: 'var(--surface-elevated)' }} className="absolute bottom-full left-0 mb-1 z-50 border border-border/30 rounded-xl overflow-hidden shadow-lg w-[min(380px,calc(100vw-2rem))] flex flex-col">
{/* Search input */}
<div className="p-2 border-b border-border/40">
<div className="relative">
@@ -411,7 +411,10 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
{/* Favorites Section */}
{filteredFavorites.length > 0 && (
<>
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<div
style={{ backgroundColor: 'var(--surface-elevated)' }}
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
>
<RiStarFill className="h-4 w-4 text-primary" />
Favorites
</div>
@@ -426,7 +429,10 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
{filteredRecents.length > 0 && (
<>
{filteredFavorites.length > 0 && <div className="h-px bg-border/40 my-1" />}
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<div
style={{ backgroundColor: 'var(--surface-elevated)' }}
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
>
<RiTimeLine className="h-4 w-4" />
Recent
</div>
@@ -446,7 +452,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
{filteredProviders.map((provider, index) => (
<React.Fragment key={provider.id}>
{index > 0 && <div className="h-px bg-border/40 my-1" />}
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<div style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
<ProviderLogo
providerId={provider.id}
className="h-4 w-4 flex-shrink-0"
@@ -507,7 +513,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
onUpdate(index, { ...model, variant: nextVariant });
}}
>
<SelectTrigger size="chip" className="px-2 gap-1.5 rounded-md bg-accent/50 border-border/30 hover:bg-accent/60 typography-meta font-medium text-foreground">
<SelectTrigger size="chip" className="px-2 gap-1.5 rounded-md bg-interactive-selection/20 border-border/30 hover:bg-interactive-hover/30 typography-meta font-medium text-foreground">
<RiBrainAi3Line
className={cn(
'h-3.5 w-3.5 flex-shrink-0',
@@ -334,7 +334,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
type="button"
onClick={onCancel}
aria-label="Close (Esc)"
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag"
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"
>
<RiCloseLine className="h-5 w-5" />
</button>
@@ -399,7 +399,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
{/* Setup commands collapsible */}
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:opacity-80 transition-opacity">
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:bg-[var(--interactive-hover)] rounded-md px-1 -mx-1 transition-colors">
<p className="typography-ui-label font-medium text-foreground">
Setup commands
{setupCommands.filter(cmd => cmd.trim()).length > 0 && (
@@ -6,30 +6,12 @@ interface ThemeProviderProps {
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const { theme, applyTheme, fontSize, applyTypography, padding, applyPadding } = useUIStore();
const { fontSize, applyTypography, padding, applyPadding } = useUIStore();
React.useLayoutEffect(() => {
applyTheme();
applyTypography();
applyPadding();
}, [theme, applyTheme, fontSize, applyTypography, padding, applyPadding]);
React.useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
if (theme === 'system') {
applyTheme();
}
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [theme, applyTheme]);
}, [fontSize, applyTypography, padding, applyPadding]);
return <>{children}</>;
};
};
@@ -756,7 +756,7 @@ export const AgentsPage: React.FC = () => {
const newValue = Math.max(0, current - 0.1);
setTemperature(parseFloat(newValue.toFixed(1)));
}}
className="absolute left-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
className="absolute left-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-interactive-hover text-muted-foreground hover:text-foreground"
>
<RiSubtractLine className="h-3.5 w-3.5" />
</button>
@@ -795,7 +795,7 @@ export const AgentsPage: React.FC = () => {
const newValue = Math.min(2, current + 0.1);
setTemperature(parseFloat(newValue.toFixed(1)));
}}
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-interactive-hover text-muted-foreground hover:text-foreground"
>
<RiAddLine className="h-3.5 w-3.5" />
</button>
@@ -824,7 +824,7 @@ export const AgentsPage: React.FC = () => {
const newValue = Math.max(0, current - 0.1);
setTopP(parseFloat(newValue.toFixed(1)));
}}
className="absolute left-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
className="absolute left-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-interactive-hover text-muted-foreground hover:text-foreground"
>
<RiSubtractLine className="h-3.5 w-3.5" />
</button>
@@ -863,7 +863,7 @@ export const AgentsPage: React.FC = () => {
const newValue = Math.min(1, current + 0.1);
setTopP(parseFloat(newValue.toFixed(1)));
}}
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-interactive-hover text-muted-foreground hover:text-foreground"
>
<RiAddLine className="h-3.5 w-3.5" />
</button>
@@ -405,7 +405,7 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
<Button
variant="ghost"
onClick={() => setRenameDialogAgent(null)}
className="text-foreground hover:bg-muted hover:text-foreground"
className="text-foreground hover:bg-interactive-hover hover:text-foreground"
>
Cancel
</Button>
@@ -446,7 +446,7 @@ const AgentListItem: React.FC<AgentListItemProps> = ({
<div
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
)}
>
<div className="flex min-w-0 flex-1 items-center">
@@ -161,7 +161,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
ref={(el) => { itemRefs.current[flatIndex] = el; }}
className={cn(
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
isHighlighted ? "bg-accent" : "hover:bg-accent/50"
isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
)}
onClick={() => handleProviderAndModelChange(provID, modID)}
onMouseEnter={() => setSelectedIndex(flatIndex)}
@@ -190,8 +190,8 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
toggleFavoriteModel(provID, modID);
}}
className={cn(
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-yellow-600",
isFavorite ? "text-yellow-500" : "text-muted-foreground"
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-primary/80",
isFavorite ? "text-primary" : "text-muted-foreground"
)}
aria-label={isFavorite ? "Unfavorite" : "Favorite"}
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
@@ -253,7 +253,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<div className="space-y-1">
{/* Favorites Section for Mobile */}
{favoriteModelsList.length > 0 && (
<div className="rounded-xl border border-border/40 bg-background/95 mb-2">
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] mb-2">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Favorites
</div>
@@ -293,7 +293,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
e.stopPropagation();
toggleFavoriteModel(providerID, modelID);
}}
className="model-favorite-button flex h-8 w-8 items-center justify-center text-yellow-500 hover:text-yellow-600 active:scale-95 touch-manipulation"
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="Unfavorite"
>
<RiStarFill className="h-4 w-4" />
@@ -307,7 +307,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
{/* Recents Section for Mobile */}
{recentModelsList.length > 0 && (
<div className="rounded-xl border border-border/40 bg-background/95 mb-2">
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] mb-2">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Recents
</div>
@@ -347,7 +347,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
e.stopPropagation();
toggleFavoriteModel(providerID, modelID);
}}
className="model-favorite-button flex h-8 w-8 items-center justify-center text-muted-foreground/50 hover:text-yellow-600 active:scale-95 touch-manipulation"
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="Favorite"
>
<RiStarLine className="h-4 w-4" />
@@ -367,7 +367,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
const isExpanded = expandedMobileProviders.has(provider.id);
return (
<div key={provider.id} className="rounded-xl border border-border/40 bg-background/95">
<div key={provider.id} className="rounded-xl border border-border/40 bg-[var(--surface-elevated)]">
<button
type="button"
className="flex w-full items-center justify-between gap-1.5 px-2 py-1.5 text-left"
@@ -425,9 +425,9 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
toggleFavoriteModel(provider.id as string, modelItem.id as string);
}}
className={cn(
"flex h-8 w-8 items-center justify-center active:scale-95 touch-manipulation",
"flex h-8 w-8 items-center justify-center active:scale-95 touch-manipulation hover:text-primary/80",
isFavoriteModel(provider.id as string, modelItem.id as string)
? "text-yellow-500"
? "text-primary"
: "text-muted-foreground/50"
)}
aria-label={isFavoriteModel(provider.id as string, modelItem.id as string) ? "Unfavorite" : "Favorite"}
@@ -454,7 +454,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left"
className="flex w-full items-center justify-between rounded-lg border border-border/40 bg-[var(--surface-elevated)] px-2 py-1.5 text-left"
onClick={() => {
handleProviderAndModelChange('', '');
closeMobilePanel();
@@ -474,7 +474,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
type="button"
onClick={() => setIsMobilePanelOpen(true)}
className={cn(
'flex w-full items-center justify-between gap-2 rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left',
'flex w-full items-center justify-between gap-2 rounded-lg border border-border/40 bg-[var(--surface-elevated)] px-2 py-1.5 text-left',
className
)}
>
@@ -497,7 +497,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
<DropdownMenuTrigger asChild>
<div className={cn(
'flex items-center gap-2 px-2 rounded-lg bg-accent/20 border border-border/20 cursor-pointer hover:bg-accent/30 h-6 w-fit',
'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
)}>
{providerId ? (
@@ -596,7 +596,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<div
className={cn(
"typography-meta flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
"hover:bg-accent/50"
"hover:bg-interactive-hover/50"
)}
onClick={() => handleProviderAndModelChange('', '')}
>
@@ -618,7 +618,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
{/* Favorites Section */}
{filteredFavorites.length > 0 && (
<>
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<DropdownMenuLabel style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
<RiStarFill className="h-4 w-4 text-primary" />
Favorites
</DropdownMenuLabel>
@@ -633,7 +633,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
{filteredRecents.length > 0 && (
<>
{filteredFavorites.length > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<DropdownMenuLabel style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
<RiTimeLine className="h-4 w-4" />
Recent
</DropdownMenuLabel>
@@ -653,7 +653,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
{filteredProviders.map((provider, index) => (
<React.Fragment key={provider.id}>
{index > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
<DropdownMenuLabel style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
<ProviderLogo
providerId={provider.id}
className="h-4 w-4 flex-shrink-0"
@@ -120,7 +120,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className={cn(
'flex items-center gap-2 px-2 rounded-lg bg-accent/20 border border-border/20 cursor-pointer hover:bg-accent/30 h-6 w-fit',
'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
)}>
<RiRobot2Line className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
@@ -2,10 +2,10 @@ import React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from '@/components/ui';
import { useCommandsStore, type CommandConfig, type CommandScope } from '@/stores/useCommandsStore';
import { RiCheckLine, RiInformationLine, RiSaveLine, RiTerminalBoxLine, RiUser3Line, RiFolderLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { RiInformationLine, RiSaveLine, RiTerminalBoxLine, RiUser3Line, RiFolderLine } from '@remixicon/react';
import { ModelSelector } from '../agents/ModelSelector';
import { AgentSelector } from './AgentSelector';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
@@ -309,22 +309,10 @@ export const CommandsPage: React.FC = () => {
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground flex items-center gap-2 cursor-pointer">
<div className="relative">
<input
type="checkbox"
checked={subtask}
onChange={(e) => setSubtask(e.target.checked)}
className="sr-only"
/>
<div className={cn(
"w-5 h-5 rounded border-2 flex items-center justify-center",
subtask
? "bg-primary border-primary"
: "bg-background border-border hover:border-primary/50"
)}>
{subtask && <RiCheckLine className="w-3 h-3 text-primary-foreground" />}
</div>
</div>
<Checkbox
checked={subtask}
onChange={(checked) => setSubtask(checked)}
/>
Force Subagent Invocation
</label>
<div className="flex items-center gap-2">
@@ -302,7 +302,7 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
<Button
variant="ghost"
onClick={() => setRenameDialogCommand(null)}
className="text-foreground hover:bg-muted hover:text-foreground"
className="text-foreground hover:bg-interactive-hover hover:text-foreground"
>
Cancel
</Button>
@@ -339,7 +339,7 @@ const CommandListItem: React.FC<CommandListItemProps> = ({
<div
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
)}
>
<div className="flex min-w-0 flex-1 items-center">
@@ -261,7 +261,7 @@ const ProfileListItem: React.FC<ProfileListItemProps> = ({
<div
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
)}
>
<div className="flex min-w-0 flex-1 items-center">
@@ -357,7 +357,7 @@ const DiscoveredCredentialItem: React.FC<DiscoveredCredentialItemProps> = ({
const isRepoSpecific = credential.host.includes('/');
return (
<div className="group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200 hover:dark:bg-accent/40 hover:bg-primary/6">
<div className="group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200 hover:bg-interactive-hover">
<div className="flex min-w-0 flex-1 items-center">
<div className="flex min-w-0 flex-1 flex-col gap-0">
<div className="flex items-center gap-1.5">
@@ -4,6 +4,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
import { AgentSelector } from '@/components/sections/commands/AgentSelector';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { updateDesktopSettings } from '@/lib/persistence';
import { getDesktopSettings, isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -317,17 +318,15 @@ export const DefaultsSettings: React.FC = () => {
{!isVSCode && (
<div className="pt-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
<Checkbox
checked={settingsAutoCreateWorktree}
onChange={handleAutoWorktreeChange}
onChange={(checked) => handleAutoWorktreeChange({ target: { checked } } as React.ChangeEvent<HTMLInputElement>)}
/>
<span className="typography-ui-label text-foreground">
Always create worktree for new sessions
</span>
</label>
<p className="typography-meta text-muted-foreground pl-5.5 mt-1">
<p className="typography-meta text-muted-foreground pl-5 mt-1">
{settingsAutoCreateWorktree
? `New session (Worktree): ${getModifierLabel()} + N • New session (Standard): Shift + ${getModifierLabel()} + N`
: `New session (Standard): ${getModifierLabel()} + N • New session (Worktree): Shift + ${getModifierLabel()} + N`}
@@ -1,6 +1,7 @@
import React from 'react';
import { RiInformationLine } from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Checkbox } from '@/components/ui/checkbox';
import { updateDesktopSettings } from '@/lib/persistence';
import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -105,11 +106,9 @@ export const GitSettings: React.FC = () => {
<div className="space-y-3">
<div className="flex flex-col gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
<Checkbox
checked={settingsGitmojiEnabled}
onChange={handleGitmojiChange}
onChange={(checked) => handleGitmojiChange({ target: { checked } } as React.ChangeEvent<HTMLInputElement>)}
/>
<span className="typography-ui-label text-foreground">Enable gitmoji picker</span>
</label>
@@ -128,11 +127,9 @@ export const GitSettings: React.FC = () => {
<div className="space-y-3">
<div className="flex flex-col gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
<Checkbox
checked={showGitignored}
onChange={(event) => setFilesViewShowGitignored(event.target.checked)}
onChange={setFilesViewShowGitignored}
/>
<span className="typography-ui-label text-foreground">Display gitignored files</span>
</label>
@@ -102,7 +102,7 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
key={group.id}
className={cn(
'group relative rounded-md px-1.5 py-1 transition-all duration-200',
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
)}
>
<button
@@ -9,6 +9,14 @@ import { cn, getModifierLabel } from '@/lib/utils';
import { ButtonSmall } from '@/components/ui/button-small';
import { NumberInput } from '@/components/ui/number-input';
import { Switch } from '@/components/ui/switch';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { isVSCodeRuntime } from '@/lib/desktop';
import { useDeviceInfo } from '@/lib/device';
import {
@@ -109,8 +117,46 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const {
themeMode,
setThemeMode,
availableThemes,
customThemesLoading,
reloadCustomThemes,
lightThemeId,
darkThemeId,
setLightThemePreference,
setDarkThemePreference,
} = useThemeSystem();
const [themesReloading, setThemesReloading] = React.useState(false);
const lightThemes = React.useMemo(
() => availableThemes
.filter((theme) => theme.metadata.variant === 'light')
.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)),
[availableThemes],
);
const darkThemes = React.useMemo(
() => availableThemes
.filter((theme) => theme.metadata.variant === 'dark')
.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)),
[availableThemes],
);
const selectedLightTheme = React.useMemo(
() => lightThemes.find((theme) => theme.metadata.id === lightThemeId) ?? lightThemes[0],
[lightThemes, lightThemeId],
);
const selectedDarkTheme = React.useMemo(
() => darkThemes.find((theme) => theme.metadata.id === darkThemeId) ?? darkThemes[0],
[darkThemes, darkThemeId],
);
const formatThemeLabel = React.useCallback((themeName: string, variant: 'light' | 'dark') => {
const suffix = variant === 'dark' ? ' Dark' : ' Light';
return themeName.endsWith(suffix) ? themeName.slice(0, -suffix.length) : themeName;
}, []);
const shouldShow = (setting: VisibleSetting): boolean => {
if (!visibleSettings) return true;
return visibleSettings.includes(setting);
@@ -138,6 +184,62 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
</ButtonSmall>
))}
</div>
<div className="flex gap-10">
<div className="flex flex-col gap-1.5">
<h4 className="typography-ui-label font-medium text-foreground">Light Theme</h4>
<Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}>
<SelectTrigger aria-label="Select light theme" className="min-w-32">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent className="max-h-64 min-w-40">
{lightThemes.map((theme) => (
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
{formatThemeLabel(theme.metadata.name, 'light')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5">
<h4 className="typography-ui-label font-medium text-foreground">Dark Theme</h4>
<Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}>
<SelectTrigger aria-label="Select dark theme" className="min-w-32">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent className="max-h-64 min-w-40">
{darkThemes.map((theme) => (
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
{formatThemeLabel(theme.metadata.name, 'dark')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1.5">
<button
type="button"
disabled={customThemesLoading || themesReloading}
onClick={async () => {
setThemesReloading(true);
try {
await reloadCustomThemes();
} finally {
setThemesReloading(false);
}
}}
className="typography-ui-label text-muted-foreground hover:text-foreground hover:underline underline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
>
<RiRestartLine className={cn('h-3.5 w-3.5', themesReloading && 'animate-spin')} />
Reload custom themes
</button>
<p className="typography-meta text-muted-foreground/70">
Import themes from ~/.config/openchamber/themes/
</p>
</div>
</div>
)}
@@ -172,7 +274,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
variant="ghost"
onClick={() => setFontSize(100)}
disabled={fontSize === 100}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
aria-label="Reset font size"
title="Reset"
>
@@ -213,7 +315,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
variant="ghost"
onClick={() => setPadding(100)}
disabled={padding === 100}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
aria-label="Reset spacing"
title="Reset"
>
@@ -244,7 +346,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
variant="ghost"
onClick={() => setPadding(100)}
disabled={padding === 100}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
aria-label="Reset spacing"
title="Reset"
>
@@ -304,7 +406,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
variant="ghost"
onClick={() => setCornerRadius(12)}
disabled={cornerRadius === 12}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
aria-label="Reset corner radius"
title="Reset"
>
@@ -336,7 +438,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
variant="ghost"
onClick={() => setCornerRadius(12)}
disabled={cornerRadius === 12}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
aria-label="Reset corner radius"
title="Reset"
>
@@ -380,7 +482,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
variant="ghost"
onClick={() => setInputBarOffset(0)}
disabled={inputBarOffset === 0}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
aria-label="Reset input bar offset"
title="Reset"
>
@@ -412,7 +514,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
variant="ghost"
onClick={() => setInputBarOffset(0)}
disabled={inputBarOffset === 0}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
aria-label="Reset input bar offset"
title="Reset"
>
@@ -540,11 +642,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
{shouldShow('reasoning') && (
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
<Checkbox
checked={showReasoningTraces}
onChange={(event) => setShowReasoningTraces(event.target.checked)}
onChange={setShowReasoningTraces}
/>
<span className="typography-ui-header font-semibold text-foreground">
Show thinking / reasoning traces
@@ -554,11 +654,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
{shouldShow('textJustificationActivity') && (
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
<Checkbox
checked={showTextJustificationActivity}
onChange={(event) => setShowTextJustificationActivity(event.target.checked)}
onChange={setShowTextJustificationActivity}
/>
<span className="typography-ui-header font-semibold text-foreground">
Show text justification in activity
@@ -569,11 +667,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
{shouldShow('queueMode') && (
<div className="space-y-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
<Checkbox
checked={queueModeEnabled}
onChange={(event) => setQueueMode(event.target.checked)}
onChange={setQueueMode}
/>
<span className="typography-ui-header font-semibold text-foreground">
Queue messages by default
@@ -4,6 +4,7 @@ import { RiInformationLine } from '@remixicon/react';
import { NumberInput } from '@/components/ui/number-input';
import { ButtonSmall } from '@/components/ui/button-small';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Checkbox } from '@/components/ui/checkbox';
import { useDeviceInfo } from '@/lib/device';
import { useUIStore } from '@/stores/useUIStore';
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
@@ -60,11 +61,9 @@ export const SessionRetentionSettings: React.FC = () => {
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
<Checkbox
checked={autoDeleteEnabled}
onChange={(event) => setAutoDeleteEnabled(event.target.checked)}
onChange={setAutoDeleteEnabled}
/>
<span className="typography-ui-header font-semibold text-foreground">Enable auto-cleanup</span>
</label>
@@ -542,7 +542,7 @@ export const ProvidersPage: React.FC = () => {
type="button"
className={cn(
"flex w-fit items-center justify-between gap-2 rounded-lg border border-input bg-transparent px-3 py-2 typography-ui-label",
"hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
"hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
)}
>
<span className={candidateProviderId ? "text-foreground" : "text-muted-foreground"}>
@@ -77,7 +77,7 @@ export const ProvidersSidebar: React.FC<ProvidersSidebarProps> = ({ onItemSelect
key={provider.id}
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
)}
>
<button
@@ -70,8 +70,8 @@ export const SettingsSidebarItem: React.FC<SettingsSidebarItemProps> = ({
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
selected
? 'dark:bg-accent/80 bg-primary/12'
: 'hover:dark:bg-accent/40 hover:bg-primary/6',
? 'bg-interactive-selection'
: 'hover:bg-interactive-hover',
className
)}
>
@@ -509,7 +509,7 @@ export const SkillsPage: React.FC = () => {
{filesToShow.map((file) => (
<div
key={file.path}
className="flex items-center justify-between px-3 py-2 rounded-lg border bg-muted/30 hover:bg-muted/50 cursor-pointer transition-colors"
className="flex items-center justify-between px-3 py-2 rounded-lg border bg-muted/30 hover:bg-interactive-hover cursor-pointer transition-colors"
onClick={() => handleEditFile(file.path)}
>
<div className="flex items-center gap-2 min-w-0">
@@ -608,7 +608,7 @@ export const SkillsPage: React.FC = () => {
setIsFileDialogOpen(false);
setEditingFilePath(null);
}}
className="text-foreground hover:bg-muted hover:text-foreground"
className="text-foreground hover:bg-interactive-hover hover:text-foreground"
>
Cancel
</Button>
@@ -294,7 +294,7 @@ export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) =>
<Button
variant="ghost"
onClick={() => setRenameDialogSkill(null)}
className="text-foreground hover:bg-muted hover:text-foreground"
className="text-foreground hover:bg-interactive-hover hover:text-foreground"
>
Cancel
</Button>
@@ -329,7 +329,7 @@ const SkillListItem: React.FC<SkillListItemProps> = ({
<div
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
)}
>
<div className="flex min-w-0 flex-1 items-center">
@@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button';
import { ButtonLarge } from '@/components/ui/button-large';
import { Input } from '@/components/ui/input';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
@@ -362,16 +363,16 @@ export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ op
key={item.skillDir}
className={
'flex items-start gap-3 rounded-lg border bg-muted/10 px-3 py-2 cursor-pointer transition-colors ' +
(disabled ? 'opacity-60 cursor-not-allowed' : 'hover:bg-muted/20')
(disabled ? 'opacity-60 cursor-not-allowed' : 'hover:bg-interactive-hover/20')
}
>
<input
type="checkbox"
className="mt-1 h-4 w-4"
checked={checked}
disabled={disabled}
onChange={(e) => setSelected((prev) => ({ ...prev, [item.skillDir]: e.target.checked }))}
/>
<div className="mt-1">
<Checkbox
checked={checked}
disabled={disabled}
onChange={(newChecked) => setSelected((prev) => ({ ...prev, [item.skillDir]: newChecked }))}
/>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<div className="typography-ui-label truncate">{item.skillName}</div>
@@ -236,7 +236,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
return (
<div
key={branchName}
className="flex items-center gap-2 px-2.5 py-1.5 hover:bg-muted/30 rounded-md overflow-hidden"
className="flex items-center gap-2 px-2.5 py-1.5 hover:bg-interactive-hover/30 rounded-md overflow-hidden"
>
<RiGitBranchLine className="h-4 w-4 text-muted-foreground flex-shrink-0" />
@@ -327,7 +327,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
type="button"
onClick={() => beginRename(branchName)}
disabled={disableRename}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
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="Rename"
>
<RiPencilLine className="h-4 w-4" />
@@ -371,7 +371,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
type="button"
onClick={() => void commitRename(branchName)}
disabled={isRenaming}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
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="Confirm rename"
>
{isRenaming ? (
@@ -383,7 +383,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
<button
type="button"
onClick={cancelRename}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted/40 text-muted-foreground hover:text-foreground transition-colors"
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Cancel rename"
>
<RiCloseLine className="h-4 w-4" />
@@ -420,7 +420,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
<button
type="button"
onClick={cancelDelete}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted/40 text-muted-foreground hover:text-foreground transition-colors"
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Cancel delete"
>
<RiCloseLine className="h-4 w-4" />
@@ -292,7 +292,7 @@ export const DirectoryAutocomplete = React.forwardRef<DirectoryAutocompleteHandl
ref={(el) => { itemRefs.current[index] = el; }}
className={cn(
"flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label",
isSelected && "bg-muted"
isSelected && "bg-interactive-selection"
)}
onClick={() => { handleSelectSuggestion(entry); onClose(); }}
onMouseEnter={() => setSelectedIndex(index)}
@@ -208,7 +208,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
<button
type="button"
onClick={toggleShowHidden}
className="flex items-center gap-2 px-2 py-1 rounded-lg hover:bg-accent/40 transition-colors typography-meta text-muted-foreground flex-shrink-0"
className="flex items-center gap-2 px-2 py-1 rounded-lg hover:bg-interactive-hover/40 transition-colors typography-meta text-muted-foreground flex-shrink-0"
>
{showHidden ? (
<RiCheckboxLine className="h-4 w-4 text-primary" />
@@ -626,7 +626,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
e.stopPropagation();
toggleExpanded(item);
}}
className={cn("hover:bg-accent rounded", isMobile ? "p-0.5" : "p-0.5")}
className={cn("hover:bg-interactive-hover rounded", isMobile ? "p-0.5" : "p-0.5")}
>
{isExpanded ? (
<RiArrowDownSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
@@ -681,7 +681,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
startCreatingDirectory(item);
}}
className={cn(
"hover:bg-accent rounded transition-opacity",
"hover:bg-interactive-hover rounded transition-opacity",
isMobile ? "p-1.5" : "p-1",
alwaysShowActions ? "opacity-60" : "opacity-0 group-hover:opacity-100"
)}
@@ -696,7 +696,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
togglePin(item.path);
}}
className={cn(
"hover:bg-accent rounded transition-opacity",
"hover:bg-interactive-hover rounded transition-opacity",
isMobile ? "p-1.5" : "p-1",
alwaysShowActions ? "opacity-60" : "opacity-0 group-hover:opacity-100"
)}
@@ -720,7 +720,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
isMobile ? 'px-1.5 py-1' : 'px-2 py-1.5',
isSelected
? 'bg-primary/10 text-primary'
: 'hover:bg-accent/50 text-foreground'
: 'hover:bg-interactive-hover/50 text-foreground'
)}
style={{ paddingLeft: `${level * (isMobile ? 12 : 14) + (isMobile ? 4 : 6)}px` }}
>
@@ -751,7 +751,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
}
}}
onBlur={createDirectory}
className="h-6 typography-meta flex-1 selection:bg-muted selection:text-muted-foreground"
className="h-6 typography-meta flex-1 selection:bg-interactive-selection selection:text-interactive-selection-foreground"
placeholder="new_directory"
/>
<button
@@ -760,7 +760,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
e.stopPropagation();
createDirectory();
}}
className="p-1 hover:bg-accent rounded"
className="p-1 hover:bg-interactive-hover rounded"
title="Create directory"
>
<RiCheckLine className="h-3 w-3 text-green-600" />
@@ -771,7 +771,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
e.stopPropagation();
cancelCreatingDirectory();
}}
className="p-1 hover:bg-accent rounded"
className="p-1 hover:bg-interactive-hover rounded"
title="Cancel"
>
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
@@ -790,7 +790,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
<DropdownMenuItem
className={cn(
'flex items-center gap-1 cursor-pointer group',
currentPath === item.path && 'bg-accent'
currentPath === item.path && 'bg-interactive-selection'
)}
style={{ paddingLeft: `${level * 12 + 8}px` }}
onSelect={(e) => {
@@ -803,7 +803,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
e.stopPropagation();
toggleExpanded(item);
}}
className="p-0.5 hover:bg-accent rounded"
className="p-0.5 hover:bg-interactive-hover rounded"
>
{isExpanded ? (
<RiArrowDownSLine className="h-3 w-3" />
@@ -840,7 +840,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
}
}}
onBlur={createDirectory}
className="h-6 typography-meta flex-1 selection:bg-muted selection:text-muted-foreground"
className="h-6 typography-meta flex-1 selection:bg-interactive-selection selection:text-interactive-selection-foreground"
placeholder="new_directory"
/>
<button
@@ -849,7 +849,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
e.stopPropagation();
createDirectory();
}}
className="p-1 hover:bg-accent rounded"
className="p-1 hover:bg-interactive-hover rounded"
title="Create directory"
>
<RiCheckLine className="h-3 w-3 text-green-600" />
@@ -860,7 +860,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
e.stopPropagation();
cancelCreatingDirectory();
}}
className="p-1 hover:bg-accent rounded"
className="p-1 hover:bg-interactive-hover rounded"
title="Cancel"
>
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
@@ -885,7 +885,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
isMobile ? 'px-1.5 py-1' : 'px-2 py-1.5',
isSelected
? 'bg-primary/10'
: 'hover:bg-accent/50'
: 'hover:bg-interactive-hover/50'
)}
>
<button
@@ -923,7 +923,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
<button
onClick={() => togglePin(path)}
className={cn(
"hover:bg-accent rounded-md transition-opacity",
"hover:bg-interactive-hover rounded-md transition-opacity",
isMobile ? "p-1.5 opacity-60" : "p-1 opacity-0 group-hover:opacity-100"
)}
title="Unpin directory"
@@ -946,7 +946,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
}}
className={cn(
'flex items-start gap-2 cursor-pointer group py-2',
currentPath === path && 'bg-accent'
currentPath === path && 'bg-interactive-selection'
)}
>
<RiFolder6Line className="h-3.5 w-3.5 text-muted-foreground mt-0.5" />
@@ -962,7 +962,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
e.preventDefault();
togglePin(path);
}}
className="p-1 opacity-0 group-hover:opacity-100 hover:bg-accent rounded transition-opacity"
className="p-1 opacity-0 group-hover:opacity-100 hover:bg-interactive-hover rounded transition-opacity"
title="Unpin directory"
>
<RiPushpin2Line className="h-3 w-3 text-primary" />
@@ -985,7 +985,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
type="button"
onClick={() => setIsPinnedExpanded(prev => !prev)}
className={cn(
"flex w-full items-center gap-1.5 typography-meta font-medium text-muted-foreground/80 hover:bg-accent/30 rounded transition-colors uppercase tracking-wide",
"flex w-full items-center gap-1.5 typography-meta font-medium text-muted-foreground/80 hover:bg-interactive-hover/30 rounded transition-colors uppercase tracking-wide",
isMobile ? "px-1.5 py-1" : "px-2 py-1.5"
)}
>
@@ -505,8 +505,8 @@ Do not implement changes until I confirm; end with: “Next actions: <1 sentence
{directNumber && projectDirectory && github && connected ? (
<div
className={cn(
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
startingIssueNumber === directNumber && 'bg-muted/30'
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
startingIssueNumber === directNumber && 'bg-interactive-selection/30'
)}
onClick={() => void startSession(directNumber)}
>
@@ -530,8 +530,8 @@ Do not implement changes until I confirm; end with: “Next actions: <1 sentence
<div
key={issue.number}
className={cn(
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
startingIssueNumber === issue.number && 'bg-muted/30'
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
startingIssueNumber === issue.number && 'bg-interactive-selection/30'
)}
onClick={() => void startSession(issue.number)}
>
@@ -646,8 +646,8 @@ Nice-to-have:
{directNumber && projectDirectory && github && connected ? (
<div
className={cn(
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
startingNumber === directNumber && 'bg-muted/30'
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
startingNumber === directNumber && 'bg-interactive-selection/30'
)}
onClick={() => void startSession(directNumber)}
>
@@ -675,10 +675,10 @@ Nice-to-have:
key={pr.number}
className={cn(
'group flex items-start gap-2 py-1.5 rounded transition-colors',
startingNumber === pr.number && 'bg-muted/30',
startingNumber === pr.number && 'bg-interactive-selection/30',
disabledByWorktree
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-muted/30 cursor-pointer'
: 'hover:bg-interactive-hover/30 cursor-pointer'
)}
onClick={() => {
if (disabledByWorktree) return;
@@ -491,7 +491,7 @@ export const SessionDialogs: React.FC = () => {
{targetWorktree ? formatPathForDisplay(targetWorktree.path, homeDirectory) : 'Worktree path unavailable.'}
</p>
{hasDirtyWorktrees && (
<p className="typography-micro text-warning">Uncommitted changes will be discarded.</p>
<p className="typography-micro text-status-warning">Uncommitted changes will be discarded.</p>
)}
</div>
@@ -210,7 +210,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
? isStuck ? 'var(--sidebar-stuck-bg)' : 'transparent'
: undefined,
borderColor: isHovered
? 'var(--color-border)'
? 'var(--color-border-hover)'
: isCollapsed
? 'color-mix(in srgb, var(--color-border) 35%, transparent)'
: 'var(--color-border)'
@@ -320,7 +320,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
onNewWorktreeSession();
}}
className={cn(
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground flex-shrink-0',
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground hover:bg-interactive-hover/50 flex-shrink-0',
mobileVariant ? 'opacity-70' : 'opacity-100',
)}
aria-label="New session in worktree"
@@ -342,7 +342,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
e.stopPropagation();
onNewSession();
}}
className="inline-flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground flex-shrink-0 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
className="inline-flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 flex-shrink-0 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label="New session"
>
<RiAddLine className="h-4 w-4" />
@@ -1133,7 +1133,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
key={session.id}
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1',
'dark:bg-accent/80 bg-primary/12',
'bg-interactive-selection',
depth > 0 && 'pl-[20px]',
)}
>
@@ -1209,7 +1209,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const streamingIndicator = (() => {
if (!memoryState) return null;
if (memoryState.isZombie) {
return <RiErrorWarningLine className="h-4 w-4 text-warning" />;
return <RiErrorWarningLine className="h-4 w-4 text-status-warning" />;
}
return null;
})();
@@ -1219,7 +1219,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
<div
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1',
isActive ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6',
isActive ? 'bg-interactive-selection' : 'hover:bg-interactive-hover',
isMissingDirectory ? 'opacity-75' : '',
depth > 0 && 'pl-[20px]',
)}
@@ -1314,7 +1314,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
</span>
) : null}
{isMissingDirectory ? (
<span className="inline-flex items-center gap-0.5 text-warning flex-shrink-0">
<span className="inline-flex items-center gap-0.5 text-status-warning flex-shrink-0">
<RiErrorWarningLine className="h-3 w-3" />
Missing
</span>
@@ -1570,7 +1570,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
type="button"
onClick={handleOpenDirectoryDialog}
className={cn(
'inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
'inline-flex h-8 w-8 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',
!isDesktopRuntime && 'bg-sidebar/60 hover:bg-sidebar',
)}
aria-label="Add project"
@@ -145,6 +145,7 @@ export function CodeMirrorEditor({ value, onChange, extensions, className, readO
'h-full w-full',
'[&_.cm-editor]:h-full [&_.cm-editor]:w-full',
'[&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[var(--text-code)] [&_.cm-scroller]:leading-6',
'[&_.cm-lineNumbers]:text-[var(--tools-edit-line-number)]',
className,
)}
/>
@@ -11,6 +11,7 @@ interface ContextUsageDisplayProps {
outputLimit?: number;
size?: 'default' | 'compact';
isMobile?: boolean;
hideIcon?: boolean;
}
export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
@@ -20,6 +21,7 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
outputLimit,
size = 'default',
isMobile = false,
hideIcon = false,
}) => {
const [mobileTooltipOpen, setMobileTooltipOpen] = React.useState(false);
const longPressTimerRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
@@ -80,7 +82,7 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
onTouchStart={isMobile ? handleLongPressStart : undefined}
onTouchEnd={isMobile ? handleLongPressEnd : undefined}
>
{!isMobile && <RiDonutChartLine className="h-4 w-4 flex-shrink-0" />}
{!isMobile && !hideIcon && <RiDonutChartLine className="h-4 w-4 flex-shrink-0" />}
<span className={cn(getPercentageColor(percentage), 'font-medium')}>
{Math.min(percentage, 999).toFixed(1)}%
</span>
@@ -58,7 +58,7 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
{this.state.error && (
<details className="text-xs font-mono bg-muted p-3 rounded">
<summary className="cursor-pointer hover:bg-muted/80">Error details</summary>
<summary className="cursor-pointer hover:bg-interactive-hover/80">Error details</summary>
<pre className="mt-2 overflow-x-auto">
{this.state.error.toString()}
</pre>
@@ -79,4 +79,4 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
return this.props.children;
}
}
}
@@ -134,12 +134,12 @@ export const MemoryDebugPanel: React.FC<MemoryDebugPanelProps> = ({ onClose }) =
<RiPulseLine className="h-3 w-3 text-primary animate-pulse" />
)}
{stat.isZombie && (
<span className="text-warning">!</span>
<span className="text-status-warning">!</span>
)}
</div>
<div className="flex items-center gap-2">
<span className={`font-mono ${
stat.messageCount > MEMORY_LIMITS.VIEWPORT_MESSAGES ? 'text-warning' : ''
stat.messageCount > MEMORY_LIMITS.VIEWPORT_MESSAGES ? 'text-status-warning' : ''
}`}>
{stat.messageCount} msgs
</span>
@@ -89,7 +89,7 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
<button
type="button"
onClick={onClose}
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-accent"
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-interactive-hover"
>
<RiCloseLine className="h-4 w-4" />
</button>
@@ -78,10 +78,37 @@ export const OpenChamberLogo: React.FC<OpenChamberLogoProps> = ({
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
const strokeColor = isDark ? 'white' : 'black';
const fillColor = isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)';
const logoFillColor = isDark ? 'white' : 'black';
const cellHighlightColor = isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.4)';
const strokeColor = useMemo(() => {
if (typeof window !== 'undefined') {
const fromVars = getComputedStyle(document.documentElement).getPropertyValue('--splash-stroke').trim();
if (fromVars) {
return fromVars;
}
}
return isDark ? 'white' : 'black';
}, [isDark]);
const fillColor = useMemo(() => {
if (typeof window !== 'undefined') {
const fromVars = getComputedStyle(document.documentElement).getPropertyValue('--splash-face-fill').trim();
if (fromVars) {
return fromVars;
}
}
return isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)';
}, [isDark]);
const cellHighlightColor = useMemo(() => {
if (typeof window !== 'undefined') {
const fromVars = getComputedStyle(document.documentElement).getPropertyValue('--splash-cell-fill').trim();
if (fromVars) {
return fromVars;
}
}
return isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.4)';
}, [isDark]);
const logoFillColor = strokeColor;
@@ -360,7 +360,7 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
onClick={handleCopyCommand}
className={cn(
'flex items-center justify-center p-2 rounded-md',
'text-muted-foreground hover:text-foreground hover:bg-accent',
'text-muted-foreground hover:text-foreground hover:bg-interactive-hover',
'transition-colors',
copied && 'text-primary'
)}
@@ -406,7 +406,7 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
className={cn(
'flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md',
'text-sm text-muted-foreground',
'hover:text-foreground hover:bg-accent',
'hover:text-foreground hover:bg-interactive-hover',
'transition-colors'
)}
>
@@ -67,7 +67,7 @@ export function AnimatedTabs<T extends string>({
animate ? '[transition:clip-path_200ms_ease]' : null
)}
>
<div className="flex h-9 items-center gap-1 rounded-lg bg-accent px-1.5 text-accent-foreground">
<div className="flex h-9 items-center gap-1 rounded-lg bg-interactive-selection px-1.5 text-interactive-selection-foreground">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
@@ -101,7 +101,7 @@ export function AnimatedTabs<T extends string>({
className={cn(
'flex h-7 flex-1 items-center justify-center gap-1.25 rounded-lg px-2.5 text-sm font-semibold transition-colors duration-150',
isActive ? 'text-accent-foreground' : 'text-muted-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-background'
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-1 focus-visible:ring-offset-background'
)}
aria-pressed={isActive}
aria-disabled={!isInteractive}
+3 -3
View File
@@ -15,11 +15,11 @@ const buttonVariants = cva(
destructive:
"bg-destructive text-white shadow-none hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-none hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"border bg-background shadow-none hover:bg-interactive-hover hover:text-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-none hover:bg-secondary/80",
"bg-interactive-hover text-foreground shadow-none hover:bg-interactive-active",
ghost:
"text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
"text-foreground hover:bg-interactive-hover hover:text-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
@@ -0,0 +1,68 @@
import React from 'react';
import { RiCheckboxBlankLine, RiCheckboxLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
interface CheckboxProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
ariaLabel?: string;
className?: string;
iconClassName?: string;
}
export const Checkbox = React.memo<CheckboxProps>(function Checkbox({
checked,
onChange,
disabled = false,
ariaLabel,
className,
iconClassName,
}) {
const handleClick = React.useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (!disabled) {
onChange(!checked);
}
},
[checked, disabled, onChange]
);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
if (!disabled) {
onChange(!checked);
}
}
},
[checked, disabled, onChange]
);
return (
<button
type="button"
onClick={handleClick}
onKeyDown={handleKeyDown}
disabled={disabled}
aria-pressed={checked}
aria-label={ariaLabel}
className={cn(
'flex size-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',
disabled && 'cursor-not-allowed opacity-50',
className
)}
>
{checked ? (
<RiCheckboxLine className={cn('size-4 text-primary', iconClassName)} />
) : (
<RiCheckboxBlankLine className={cn('size-4', iconClassName)} />
)}
</button>
);
});
@@ -11,7 +11,7 @@ const CollapsibleTrigger = ({
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) => (
<CollapsiblePrimitive.CollapsibleTrigger
className={cn(
"flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-left text-foreground hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
"flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-left text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
className
)}
{...props}
+6 -2
View File
@@ -21,8 +21,12 @@ function Command({
return (
<CommandPrimitive
data-slot="command"
style={{
backgroundColor: 'var(--surface-elevated)',
color: 'var(--surface-elevated-foreground)',
}}
className={cn(
"bg-background text-foreground flex h-full w-full flex-col overflow-hidden rounded-xl",
"flex h-full w-full flex-col overflow-hidden rounded-xl",
className
)}
{...props}
@@ -154,7 +158,7 @@ function CommandItem({
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-muted hover:bg-muted [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 typography-meta outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-[selected=true]:bg-interactive-selection data-[selected=true]:text-interactive-selection-foreground data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 typography-meta outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
+1 -1
View File
@@ -86,7 +86,7 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-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"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-interactive-active data-[state=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"
>
<RiCloseLine/>
<span className="sr-only">Close</span>
@@ -39,8 +39,12 @@ function DropdownMenuContent({
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
style={{
backgroundColor: 'var(--surface-elevated)',
color: 'var(--surface-elevated-foreground)',
}}
className={cn(
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border p-1 shadow-none",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border-2 border-border/60 p-1 shadow-md",
className
)}
{...props}
@@ -72,7 +76,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"hover:bg-muted data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...props}
@@ -90,7 +94,7 @@ function DropdownMenuCheckboxItem({
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"hover:bg-muted relative flex cursor-default items-center gap-2 rounded-lg py-1 px-2 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1 px-2 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
checked={checked}
@@ -126,7 +130,7 @@ function DropdownMenuRadioItem({
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"relative flex cursor-default items-start gap-2 rounded-lg py-1 pl-2 pr-8 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-muted [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground relative flex cursor-default items-start gap-2 rounded-lg py-1 pl-2 pr-8 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...props}
@@ -209,7 +213,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"hover:bg-muted flex cursor-default items-center rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8",
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover flex cursor-default items-center rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
@@ -227,8 +231,12 @@ function DropdownMenuSubContent({
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
style={{
backgroundColor: 'var(--surface-elevated)',
color: 'var(--surface-elevated-foreground)',
}}
className={cn(
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border p-1 shadow-none",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border-2 border-border/60 p-1 shadow-md",
className
)}
{...props}
+4 -3
View File
@@ -8,9 +8,10 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"text-foreground border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 appearance-none hover:border-input focus:border-ring flex h-9 w-full min-w-0 rounded-lg border bg-transparent px-3 py-1 typography-markdown shadow-none transition-[color,box-shadow,border-color] outline-none focus-visible:outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:typography-ui-label file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"text-foreground border border-border/80 file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 appearance-none flex h-9 w-full min-w-0 rounded-lg bg-transparent px-3 py-1 typography-markdown outline-none focus-visible:outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:typography-ui-label file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
"hover:border-input",
"focus:ring-1 focus:ring-primary/50 focus:border-primary/70",
"aria-invalid:border-destructive aria-invalid:ring-destructive",
className
)}
spellCheck={false}
@@ -166,7 +166,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
onClick={handleIncrement}
className={cn(
"flex flex-1 items-center justify-center",
"text-muted-foreground hover:bg-accent hover:text-foreground",
"text-muted-foreground hover:bg-interactive-hover hover:text-foreground",
"disabled:pointer-events-none disabled:opacity-50"
)}
>
@@ -179,7 +179,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
onClick={handleDecrement}
className={cn(
"flex flex-1 items-center justify-center border-t border-border",
"text-muted-foreground hover:bg-accent hover:text-foreground",
"text-muted-foreground hover:bg-interactive-hover hover:text-foreground",
"disabled:pointer-events-none disabled:opacity-50"
)}
>
+7 -3
View File
@@ -38,7 +38,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none focus-visible:outline-none hover:bg-muted data-[state=open]:bg-muted focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=sm]:h-6 data-[size=lg]:h-8 data-[size=lg]:py-1.5 data-[size=chip]:h-7 data-[size=chip]:py-1 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none focus-visible:outline-none hover:bg-interactive-hover data-[state=open]:bg-interactive-active focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=sm]:h-6 data-[size=lg]:h-8 data-[size=lg]:py-1.5 data-[size=chip]:h-7 data-[size=chip]:py-1 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@@ -64,8 +64,12 @@ function SelectContent({
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
style={{
backgroundColor: 'var(--surface-elevated)',
color: 'var(--surface-elevated-foreground)',
}}
className={cn(
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden rounded-xl border shadow-none transform-gpu will-change-transform",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden rounded-xl border-2 border-border/60 shadow-md transform-gpu will-change-transform",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
fitContent && "w-max min-w-0",
@@ -117,7 +121,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"hover:bg-muted [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
+1 -1
View File
@@ -9,7 +9,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30',
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-[var(--interactive-border)]',
className
)}
style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }}
+6 -4
View File
@@ -3,17 +3,19 @@ import * as React from "react"
import { cn } from "@/lib/utils"
import { ScrollableOverlay } from "./ScrollableOverlay"
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
({ className, ...props }, ref) => {
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea"> & { outerClassName?: string }>(
({ className, outerClassName, ...props }, ref) => {
return (
<ScrollableOverlay
as="textarea"
ref={ref as React.Ref<HTMLTextAreaElement>}
disableHorizontal
fillContainer={false}
outerClassName="w-full"
outerClassName={cn("w-full rounded-lg focus-within:ring-1 focus-within:ring-primary/50", outerClassName)}
className={cn(
"text-foreground border-input placeholder:text-muted-foreground appearance-none hover:border-input focus:border-ring focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-lg border bg-transparent px-3 py-2 typography-markdown shadow-none transition-[color,box-shadow,border-color] outline-none focus-visible:outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
"text-foreground border border-border/80 placeholder:text-muted-foreground appearance-none dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-lg bg-transparent px-3 py-2 typography-markdown outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
"hover:border-input aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"focus:border-primary/70",
className
)}
spellCheck={false}
+2 -2
View File
@@ -6,13 +6,13 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-xl typography-ui-label font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
"inline-flex items-center justify-center gap-2 rounded-xl typography-ui-label font-medium hover:bg-interactive-hover hover:text-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-interactive-selection data-[state=on]:text-interactive-selection-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-none hover:bg-accent hover:text-accent-foreground",
"border border-input bg-transparent shadow-none hover:bg-interactive-hover hover:text-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
@@ -140,7 +140,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring">
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
{selectedFileEntry ? (
<div className="flex min-w-0 items-center gap-3">
<span className="min-w-0 flex-1 truncate typography-meta">
@@ -210,7 +210,7 @@ const DiffViewModeSelector = React.memo<DiffViewModeSelectorProps>(({ mode, onMo
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring">
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
<span className="min-w-0 truncate typography-meta">
{currentOption.label}
</span>
@@ -268,8 +268,8 @@ const FileList = React.memo<FileListProps>(({
className={cn(
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors',
isActive
? 'bg-accent/70 text-foreground'
: 'text-muted-foreground hover:bg-accent/40 hover:text-foreground'
? 'bg-interactive-selection text-interactive-selection-foreground'
: 'text-muted-foreground hover:bg-interactive-hover hover:text-foreground'
)}
>
<span
@@ -700,13 +700,13 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
'bg-background hover:bg-background',
isExpanded ? 'rounded-b-none' : 'rounded-b-xl',
isSelected
? 'text-primary'
? 'text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
<div className={cn(
'absolute inset-0 pointer-events-none transition-colors',
isSelected ? 'bg-primary/10' : 'group-hover:bg-accent/40'
isSelected ? 'bg-interactive-selection' : 'group-hover:bg-interactive-hover'
)} />
<div className="relative flex min-w-0 flex-1 items-center gap-2">
<span className="flex size-5 items-center justify-center opacity-70 group-hover:opacity-100 transition-opacity">
+16 -10
View File
@@ -226,13 +226,13 @@ const getFileIcon = (extension?: string): React.ReactNode => {
const ext = extension?.toLowerCase();
if (ext && CODE_EXTENSIONS.has(ext)) {
return <RiCodeLine className="h-4 w-4 flex-shrink-0 text-blue-500" />;
return <RiCodeLine className="h-4 w-4 flex-shrink-0 text-[var(--status-info)]" />;
}
if (ext && DATA_EXTENSIONS.has(ext)) {
return <RiCodeLine className="h-4 w-4 flex-shrink-0 text-yellow-500" />;
return <RiCodeLine className="h-4 w-4 flex-shrink-0 text-[var(--status-warning)]" />;
}
if (ext && IMAGE_EXTENSIONS.has(ext)) {
return <RiFileImageLine className="h-4 w-4 flex-shrink-0 text-green-500" />;
return <RiFileImageLine className="h-4 w-4 flex-shrink-0 text-[var(--status-success)]" />;
}
if (ext && DOCUMENT_EXTENSIONS.has(ext)) {
return <RiFileTextLine className="h-4 w-4 flex-shrink-0 text-muted-foreground" />;
@@ -1152,7 +1152,7 @@ export const FilesView: React.FC = () => {
}}
className={cn(
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors pr-8 select-none',
isActive ? 'bg-accent/70' : 'hover:bg-accent/40'
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40'
)}
>
{isDir ? (
@@ -1398,7 +1398,12 @@ export const FilesView: React.FC = () => {
className="flex flex-col items-center gap-2 px-4"
style={{ width: 'min(100vw - 1rem, 42rem)' }}
>
<div className="w-full rounded-xl border bg-background flex flex-col relative shadow-lg" style={{ borderColor: 'var(--primary)' }}>
<div
className="w-full rounded-xl flex flex-col relative shadow-lg border border-border/80 focus-within:border-primary/70 focus-within:ring-1 focus-within:ring-primary/50"
style={{
backgroundColor: currentTheme?.colors?.surface?.subtle,
}}
>
<Textarea
value={commentText}
onChange={(e) => {
@@ -1410,7 +1415,8 @@ export const FilesView: React.FC = () => {
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
}}
placeholder="Type your comment..."
className="min-h-[28px] max-h-[108px] resize-none border-0 px-3 pt-2 pb-1 shadow-none rounded-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent dark:bg-transparent focus-visible:outline-none overflow-y-auto"
outerClassName="focus-within:ring-0"
className="min-h-[28px] max-h-[108px] resize-none border-0 px-3 pt-2 pb-1 rounded-none appearance-none hover:border-transparent bg-transparent dark:bg-transparent overflow-y-auto focus:ring-0 focus:shadow-none"
autoFocus={!isMobile}
rows={1}
onKeyDown={(e) => {
@@ -1777,7 +1783,7 @@ export const FilesView: React.FC = () => {
) : selectedFile && isMarkdownFile(selectedFile.path) && getMdViewMode() === 'preview' ? (
<div className="h-full overflow-auto p-3">
{fileContent.length > 500 * 1024 && (
<div className="mb-3 rounded-md border border-warning/20 bg-warning/10 px-3 py-2 text-sm text-warning">
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited.
</div>
)}
@@ -1973,7 +1979,7 @@ export const FilesView: React.FC = () => {
onClick={() => void handleSelectFile(node)}
className={cn(
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors',
isActive ? 'bg-accent/70' : 'hover:bg-accent/40'
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40'
)}
>
{getFileIcon(node.extension)}
@@ -2156,7 +2162,7 @@ export const FilesView: React.FC = () => {
) : isMarkdownFile(selectedFile.path) && getMdViewMode() === 'preview' ? (
<div className="h-full overflow-auto p-4">
{fileContent.length > 500 * 1024 && (
<div className="mb-3 rounded-md border border-warning/20 bg-warning/10 px-3 py-2 text-sm text-warning">
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited.
</div>
)}
@@ -2200,7 +2206,7 @@ export const FilesView: React.FC = () => {
)
) : (
<div className="flex flex-1 min-h-0 min-w-0 gap-3 px-3 pb-3 pt-2">
{screenWidth >= 1024 && (
{screenWidth >= 700 && (
<div className="w-72 flex-shrink-0 min-h-0 overflow-hidden">
{treePanel}
</div>
@@ -6,8 +6,8 @@ import { RiSendPlane2Line } from '@remixicon/react';
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes';
import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes';
import { ensurePierreThemeRegistered, getResolvedShikiTheme } from '@/lib/shiki/appThemeRegistry';
import { getDefaultTheme } from '@/lib/theme/themes';
import { toast } from '@/components/ui';
import { Textarea } from '@/components/ui/textarea';
@@ -52,51 +52,51 @@ const WEBKIT_SCROLL_FIX_CSS = `
[data-code] {
-webkit-overflow-scrolling: touch;
}
/* Mobile touch selection support */
[data-line-number] {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
}
/* Ensure interactive line numbers work on touch */
pre[data-interactive-line-numbers] [data-line-number] {
touch-action: manipulation;
}
/* Reduce hunk separator height */
[data-separator-content] {
height: 24px !important;
}
[data-expand-button] {
height: 24px !important;
width: 24px !important;
}
[data-separator-multi-button] {
row-gap: 0 !important;
}
[data-expand-up] {
height: 12px !important;
min-height: 12px !important;
max-height: 12px !important;
margin: 0 !important;
margin-top: 3px !important;
padding: 0 !important;
border-radius: 4px 4px 0 0 !important;
}
[data-expand-down] {
height: 12px !important;
min-height: 12px !important;
max-height: 12px !important;
margin: 0 !important;
margin-top: -3px !important;
padding: 0 !important;
border-radius: 0 0 4px 4px !important;
}
// [data-separator-content] {
// height: 24px !important;
// }
// [data-expand-button] {
// height: 24px !important;
// width: 24px !important;
// }
// [data-separator-multi-button] {
// row-gap: 0 !important;
// }
// [data-expand-up] {
// height: 12px !important;
// min-height: 12px !important;
// max-height: 12px !important;
// margin: 0 !important;
// margin-top: 3px !important;
// padding: 0 !important;
// border-radius: 4px 4px 0 0 !important;
// }
// [data-expand-down] {
// height: 12px !important;
// min-height: 12px !important;
// max-height: 12px !important;
// margin: 0 !important;
// margin-top: -3px !important;
// padding: 0 !important;
// border-radius: 0 0 4px 4px !important;
// }
`;
// Fast cache key - use length + samples instead of full hash
function getCacheKey(fileName: string, original: string, modified: string): string {
function getCacheKey(fileName: string, original: string, modified: string, themeKey: string): string {
// Sample a few characters instead of hashing entire content
const sampleOriginal = original.length > 100
? `${original.slice(0, 50)}${original.slice(-50)}`
@@ -104,7 +104,7 @@ function getCacheKey(fileName: string, original: string, modified: string): stri
const sampleModified = modified.length > 100
? `${modified.slice(0, 50)}${modified.slice(-50)}`
: modified;
return `${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
return `${themeKey}::${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
}
const extractSelectedCode = (original: string, modified: string, range: SelectedLineRange): string => {
@@ -112,13 +112,13 @@ const extractSelectedCode = (original: string, modified: string, range: Selected
const isOriginal = range.side === 'deletions';
const content = isOriginal ? original : modified;
const lines = content.split('\n');
// Ensure bounds
const startLine = Math.max(1, range.start);
const endLine = Math.min(lines.length, range.end);
if (startLine > endLine) return '';
return lines.slice(startLine - 1, endLine).join('\n');
};
@@ -133,47 +133,65 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
}) => {
const { isMobile } = useDeviceInfo();
const { inputBarOffset, isKeyboardOpen } = useUIStore();
const themeSystem = useOptionalThemeSystem();
const isDark = themeSystem?.currentTheme?.metadata?.variant === 'dark';
const fallbackLight = getDefaultTheme(false);
const fallbackDark = getDefaultTheme(true);
const lightThemeId = themeSystem?.lightThemeId ?? fallbackLight.metadata.id;
const darkThemeId = themeSystem?.darkThemeId ?? fallbackDark.metadata.id;
const lightTheme =
themeSystem?.availableThemes.find((theme) => theme.metadata.id === lightThemeId) ??
fallbackLight;
const darkTheme =
themeSystem?.availableThemes.find((theme) => theme.metadata.id === darkThemeId) ??
fallbackDark;
const setActiveMainTab = useUIStore(state => state.setActiveMainTab);
const [selection, setSelection] = useState<SelectedLineRange | null>(null);
const [commentText, setCommentText] = useState('');
const commentContainerRef = useRef<HTMLDivElement>(null);
// Calculate initial center synchronously to avoid flicker
const getMainContentCenter = useCallback(() => {
if (isMobile) return '50%';
// Calculate initial center and width synchronously to avoid flicker
const getMainContentMetrics = useCallback(() => {
if (isMobile) return { center: '50%', width: '100vw' };
const mainContent = document.querySelector('main.flex-1');
if (mainContent) {
const rect = mainContent.getBoundingClientRect();
return `${rect.left + rect.width / 2}px`;
return {
center: `${rect.left + rect.width / 2}px`,
width: `${rect.width}px`
};
}
return '50%';
return { center: '50%', width: '100vw' };
}, [isMobile]);
const [mainContentCenter, setMainContentCenter] = useState<string>(getMainContentCenter);
const [mainContentMetrics, setMainContentMetrics] = useState(getMainContentMetrics);
const mainContentCenter = mainContentMetrics.center;
const mainContentWidth = mainContentMetrics.width;
const sendMessage = useSessionStore(state => state.sendMessage);
const currentSessionId = useSessionStore(state => state.currentSessionId);
const { currentProviderId, currentModelId, currentAgentName, currentVariant } = useConfigStore();
const getSessionAgentSelection = useContextStore(state => state.getSessionAgentSelection);
const getAgentModelForSession = useContextStore(state => state.getAgentModelForSession);
const getAgentModelVariantForSession = useContextStore(state => state.getAgentModelVariantForSession);
// Update main content center on resize
// Update main content metrics on resize
useEffect(() => {
if (isMobile) return;
const updateCenter = () => {
setMainContentCenter(getMainContentCenter());
const updateMetrics = () => {
setMainContentMetrics(getMainContentMetrics());
};
window.addEventListener('resize', updateCenter);
return () => window.removeEventListener('resize', updateCenter);
}, [isMobile, getMainContentCenter]);
window.addEventListener('resize', updateMetrics);
return () => window.removeEventListener('resize', updateMetrics);
}, [isMobile, getMainContentMetrics]);
const handleSelectionChange = useCallback((range: SelectedLineRange | null) => {
// On mobile: implement "tap to extend" behavior
@@ -182,11 +200,11 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const tappedLine = range.start;
const existingStart = selection.start;
const existingEnd = selection.end;
// Extend the selection to include the tapped line
const newStart = Math.min(existingStart, existingEnd, tappedLine);
const newEnd = Math.max(existingStart, existingEnd, tappedLine);
// Only extend if tapping outside current selection
if (tappedLine < existingStart || tappedLine > existingEnd) {
setSelection({
@@ -197,7 +215,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
return;
}
}
setSelection(range);
if (!range) {
setCommentText('');
@@ -207,16 +225,16 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
// Dismiss selection when clicking outside line numbers (desktop behavior)
useEffect(() => {
if (!selection) return;
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
// Check if click is inside the comment UI portal
if (commentContainerRef.current?.contains(target)) return;
// Check if click is inside toast (sonner)
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
// Check if click is on a line number (inside shadow DOM)
const path = e.composedPath();
const isLineNumber = path.some((el) => {
@@ -225,18 +243,18 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
}
return false;
});
if (!isLineNumber) {
setSelection(null);
setCommentText('');
}
};
// Use timeout to avoid immediate dismissal from the same click that selected
const timeoutId = setTimeout(() => {
document.addEventListener('click', handleClickOutside);
}, 100);
return () => {
clearTimeout(timeoutId);
document.removeEventListener('click', handleClickOutside);
@@ -264,19 +282,19 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const effectiveVariant = sessionAgent && effectiveProviderId && effectiveModelId
? getAgentModelVariantForSession(currentSessionId, sessionAgent, effectiveProviderId, effectiveModelId) ?? currentVariant
: currentVariant;
const code = extractSelectedCode(original, modified, selection);
const startLine = selection.start;
const endLine = selection.end;
const side = selection.side === 'deletions' ? 'original' : 'modified';
const message = `Comment on \`${fileName}\` lines ${startLine}-${endLine} (${side}):\n\`\`\`${language}\n${code}\n\`\`\`\n\n${commentText}`;
// Clear state and switch tab immediately for responsive UX
setCommentText('');
setSelection(null);
setActiveMainTab('chat');
void sendMessage(
message,
effectiveProviderId,
@@ -291,7 +309,89 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
});
}, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession]);
ensureFlexokiThemesRegistered();
ensurePierreThemeRegistered(lightTheme);
ensurePierreThemeRegistered(darkTheme);
const diffThemeKey = `${lightTheme.metadata.id}:${darkTheme.metadata.id}:${isDark ? 'dark' : 'light'}`;
const diffRootRef = useRef<HTMLDivElement | null>(null);
const lightResolvedTheme = useMemo(() => getResolvedShikiTheme(lightTheme), [lightTheme]);
const darkResolvedTheme = useMemo(() => getResolvedShikiTheme(darkTheme), [darkTheme]);
// Fast-path: update base diff theme vars immediately.
// Without this, already-mounted diffs can keep old bg/bars until async highlight completes.
React.useLayoutEffect(() => {
const root = diffRootRef.current;
if (!root) return;
const container = root.querySelector('diffs-container') as HTMLElement | null;
if (!container) return;
const currentResolved = isDark ? darkResolvedTheme : lightResolvedTheme;
const getColor = (
resolved: typeof currentResolved,
key: string,
): string | undefined => {
const colors = resolved.colors as Record<string, string> | undefined;
return colors?.[key];
};
const lightAdd = getColor(lightResolvedTheme, 'terminal.ansiGreen');
const lightDel = getColor(lightResolvedTheme, 'terminal.ansiRed');
const lightMod = getColor(lightResolvedTheme, 'terminal.ansiBlue');
const darkAdd = getColor(darkResolvedTheme, 'terminal.ansiGreen');
const darkDel = getColor(darkResolvedTheme, 'terminal.ansiRed');
const darkMod = getColor(darkResolvedTheme, 'terminal.ansiBlue');
// Apply on host; vars inherit into shadow root.
container.style.setProperty('--shiki-light', lightResolvedTheme.fg);
container.style.setProperty('--shiki-light-bg', lightResolvedTheme.bg);
if (lightAdd) container.style.setProperty('--shiki-light-addition-color', lightAdd);
if (lightDel) container.style.setProperty('--shiki-light-deletion-color', lightDel);
if (lightMod) container.style.setProperty('--shiki-light-modified-color', lightMod);
container.style.setProperty('--shiki-dark', darkResolvedTheme.fg);
container.style.setProperty('--shiki-dark-bg', darkResolvedTheme.bg);
if (darkAdd) container.style.setProperty('--shiki-dark-addition-color', darkAdd);
if (darkDel) container.style.setProperty('--shiki-dark-deletion-color', darkDel);
if (darkMod) container.style.setProperty('--shiki-dark-modified-color', darkMod);
container.style.setProperty('--diffs-bg', currentResolved.bg);
container.style.setProperty('--diffs-fg', currentResolved.fg);
const currentAdd = isDark ? darkAdd : lightAdd;
const currentDel = isDark ? darkDel : lightDel;
const currentMod = isDark ? darkMod : lightMod;
if (currentAdd) container.style.setProperty('--diffs-addition-color-override', currentAdd);
if (currentDel) container.style.setProperty('--diffs-deletion-color-override', currentDel);
if (currentMod) container.style.setProperty('--diffs-modified-color-override', currentMod);
// Pierre also inlines theme styles on <pre> inside shadow root.
// Patch it too so already-expanded diffs switch instantly.
const pre = container.shadowRoot?.querySelector('pre') as HTMLPreElement | null;
if (pre) {
pre.style.setProperty('--shiki-light', lightResolvedTheme.fg);
pre.style.setProperty('--shiki-light-bg', lightResolvedTheme.bg);
if (lightAdd) pre.style.setProperty('--shiki-light-addition-color', lightAdd);
if (lightDel) pre.style.setProperty('--shiki-light-deletion-color', lightDel);
if (lightMod) pre.style.setProperty('--shiki-light-modified-color', lightMod);
pre.style.setProperty('--shiki-dark', darkResolvedTheme.fg);
pre.style.setProperty('--shiki-dark-bg', darkResolvedTheme.bg);
if (darkAdd) pre.style.setProperty('--shiki-dark-addition-color', darkAdd);
if (darkDel) pre.style.setProperty('--shiki-dark-deletion-color', darkDel);
if (darkMod) pre.style.setProperty('--shiki-dark-modified-color', darkMod);
pre.style.setProperty('--diffs-bg', currentResolved.bg);
pre.style.setProperty('--diffs-fg', currentResolved.fg);
if (currentAdd) pre.style.setProperty('--diffs-addition-color-override', currentAdd);
if (currentDel) pre.style.setProperty('--diffs-deletion-color-override', currentDel);
if (currentMod) pre.style.setProperty('--diffs-modified-color-override', currentMod);
}
}, [darkResolvedTheme, diffThemeKey, isDark, lightResolvedTheme]);
// Cache the last computed diff to avoid recomputing on every render
const diffCacheRef = useRef<{
@@ -301,7 +401,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
// Pre-parse the diff with cacheKey for worker pool caching
const fileDiff = useMemo(() => {
const cacheKey = getCacheKey(fileName, original, modified);
const cacheKey = getCacheKey(fileName, original, modified, diffThemeKey);
// Return cached diff if inputs haven't changed
if (diffCacheRef.current?.key === cacheKey) {
@@ -328,12 +428,12 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
return diff;
}, [fileName, original, modified, language]);
}, [diffThemeKey, fileName, original, modified, language]);
const options = useMemo(() => ({
theme: {
dark: flexokiThemeNames.dark,
light: flexokiThemeNames.light,
dark: darkTheme.metadata.id,
light: lightTheme.metadata.id,
},
themeType: isDark ? ('dark' as const) : ('light' as const),
diffStyle: renderSideBySide ? ('split' as const) : ('unified' as const),
@@ -346,21 +446,26 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
enableHoverUtility: false,
onLineSelected: handleSelectionChange,
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
}), [isDark, renderSideBySide, wrapLines, handleSelectionChange]);
}), [darkTheme.metadata.id, isDark, lightTheme.metadata.id, renderSideBySide, wrapLines, handleSelectionChange]);
if (typeof window === 'undefined') {
return null;
}
// Extracted Comment Interface Content for reuse in Portal or In-Flow
const renderCommentContent = () => {
if (!selection) return null;
return (
<div
<div
className="flex flex-col items-center gap-2 px-4"
style={{ width: 'min(100vw - 1rem, 42rem)' }}
style={{ width: `min(calc(${mainContentWidth} - 2rem), 42rem)` }}
>
<div className="w-full rounded-xl border bg-sidebar flex flex-col relative shadow-lg" style={{ borderColor: 'var(--primary)' }}>
<div
className="w-full rounded-xl flex flex-col relative shadow-lg border border-border/80 focus-within:border-primary/70 focus-within:ring-1 focus-within:ring-primary/50"
style={{
backgroundColor: themeSystem?.currentTheme?.colors?.surface?.subtle,
}}
>
{/* Textarea - auto-grows from 1 line to max 5 lines */}
<Textarea
value={commentText}
@@ -374,7 +479,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
}}
placeholder="Type your comment..."
className="min-h-[28px] max-h-[108px] resize-none border-0 px-3 pt-2 pb-1 shadow-none rounded-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent dark:bg-transparent focus-visible:outline-none overflow-y-auto"
outerClassName="focus-within:ring-0"
className="min-h-[28px] max-h-[108px] resize-none border-0 px-3 pt-2 pb-1 rounded-none appearance-none hover:border-transparent bg-transparent dark:bg-transparent overflow-y-auto focus:ring-0 focus:shadow-none"
autoFocus={!isMobile}
rows={1}
onKeyDown={(e) => {
@@ -437,7 +543,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
// If we're in an inline diff ('inline' layout), render via Portal (fixed over content).
if (layout === 'fill') {
return (
<div
<div
className={cn("flex flex-col relative", "size-full")}
style={{
// Apply keyboard padding to the main container, just like ChatContainer
@@ -450,23 +556,26 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
disableHorizontal={false}
fillContainer={true}
>
<FileDiff
fileDiff={fileDiff}
options={options}
selectedLines={selection}
/>
<div ref={diffRootRef} className="size-full">
<FileDiff
key={diffThemeKey}
fileDiff={fileDiff}
options={options}
selectedLines={selection}
/>
</div>
</ScrollableOverlay>
</div>
{/* Render Input In-Flow at the bottom */}
{/* Render Input overlay at the bottom */}
{selection && (
<div
<div
className={cn(
"pointer-events-auto relative pb-2 transition-none z-50 flex justify-center",
"pointer-events-auto absolute bottom-0 left-0 right-0 pb-2 transition-none z-50 flex justify-center w-full",
isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area"
)}
style={{
marginBottom: isMobile
marginBottom: isMobile
? (!isKeyboardOpen && inputBarOffset > 0 ? `${inputBarOffset}px` : '16px')
: '16px'
}}
@@ -485,31 +594,32 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
// Use simple div with overflow-x-auto to avoid nested ScrollableOverlay issues in Chrome
return (
<div className={cn("relative", "w-full")}>
<div className="pierre-diff-wrapper w-full overflow-x-auto overflow-y-visible">
<div ref={diffRootRef} className="pierre-diff-wrapper w-full overflow-x-auto overflow-y-visible">
<FileDiff
key={diffThemeKey}
fileDiff={fileDiff}
options={options}
selectedLines={selection}
/>
</div>
{selection && createPortal(
<div
<div
className="fixed inset-0 z-50 flex flex-col justify-end items-start pointer-events-none transition-none transform-gpu"
style={{
style={{
paddingBottom: isMobile ? 'var(--oc-keyboard-inset, 0px)' : '0px',
isolation: 'isolate'
}}
>
<div
<div
className={cn(
"pointer-events-auto relative pb-2 transition-none",
isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area"
)}
style={{
style={{
marginLeft: mainContentCenter,
transform: 'translateX(-50%)',
marginBottom: isMobile
marginBottom: isMobile
? (!isKeyboardOpen && inputBarOffset > 0 ? `${inputBarOffset}px` : '16px')
: '16px'
}}
@@ -389,7 +389,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
className={cn(
'relative flex h-9 w-9 items-center justify-center rounded-md transition-colors',
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
isActive ? 'bg-secondary text-foreground shadow-sm' : 'text-muted-foreground'
isActive ? 'bg-interactive-selection text-interactive-selection-foreground shadow-sm' : 'text-muted-foreground hover:bg-interactive-hover/50'
)}
aria-pressed={isActive}
aria-label={label}
@@ -412,7 +412,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
className={cn(
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
isActive ? 'app-region-drag bg-interactive-selection text-interactive-selection-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-interactive-hover/50 hover:text-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
)}
aria-pressed={isActive}
@@ -436,7 +436,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
type="button"
aria-label="Switch project"
title={activeProjectLabel}
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
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 border border-[var(--interactive-border)]"
>
<RiFolderLine className="h-5 w-5" />
</button>
@@ -446,7 +446,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
aria-label="Switch project"
title={activeProjectLabel}
className={cn(
'flex h-9 max-w-[18rem] items-center gap-1.5 bg-transparent px-2 text-foreground outline-none hover:text-foreground/80 focus-visible:ring-2 focus-visible:ring-ring',
'flex h-9 max-w-[18rem] items-center gap-1.5 bg-transparent px-2 rounded-lg text-foreground outline-none hover:bg-interactive-hover/50 focus-visible:ring-2 focus-visible:ring-ring border border-[var(--interactive-border)]',
!isMobile && 'app-region-no-drag'
)}
>
@@ -487,7 +487,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
onClick={onClose}
aria-label="Close settings"
className={cn(
'inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
'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',
!isMobile && 'app-region-no-drag'
)}
>
@@ -22,6 +22,7 @@ import { BranchSelector, useBranchOptions } from '@/components/multirun/BranchSe
import { AgentSelector } from '@/components/multirun/AgentSelector';
import { isIMECompositionEvent } from '@/lib/ime';
import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import type { ProjectRef } from '@/lib/openchamberConfig';
import type { CreateMultiRunParams, MultiRunFileAttachment } from '@/types/multirun';
@@ -66,6 +67,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
const fileInputRef = React.useRef<HTMLInputElement>(null);
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const { currentTheme } = useThemeSystem();
const currentDirectory = useDirectoryStore((state) => state.currentDirectory ?? null);
const { isGitRepository, isLoading: isLoadingBranches } = useBranchOptions(currentDirectory);
@@ -312,7 +314,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
{/* Setup commands collapsible */}
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:opacity-80 transition-opacity">
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:bg-[var(--interactive-hover)] rounded-md px-1 -mx-1 transition-colors">
<p className="typography-ui-label font-medium text-foreground">
Setup commands
{setupCommands.filter(cmd => cmd.trim()).length > 0 && (
@@ -409,7 +411,10 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
<label htmlFor="prompt" className="typography-ui-label font-medium text-foreground">
Prompt
</label>
<div className="rounded-xl border border-border/60 bg-input/10 dark:bg-input/30 overflow-hidden">
<div
className="rounded-xl border border-border/80 overflow-hidden focus-within:ring-1 focus-within:ring-primary/50"
style={{ backgroundColor: currentTheme?.colors?.surface?.subtle }}
>
{/* Text Area */}
<Textarea
ref={textareaRef}
@@ -418,7 +423,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask anything..."
className="min-h-[100px] max-h-[300px] resize-none border-0 bg-transparent px-4 py-3 typography-markdown focus-visible:ring-0 focus-visible:ring-offset-0"
className="min-h-[100px] max-h-[300px] resize-none border-0 bg-transparent dark:bg-transparent px-4 py-3 typography-markdown focus-visible:ring-0 focus-visible:ring-offset-0"
/>
{/* Attached Files Display */}
@@ -450,7 +455,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
)}
{/* Footer Controls */}
<div className="flex items-center justify-between px-3 py-2 border-t border-border/40">
<div className="flex items-center justify-between px-3 py-2 border-t border-border/40 bg-transparent">
{/* Left Controls - Attachments */}
<div className="flex items-center gap-2">
<input
@@ -74,7 +74,7 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, onSe
<div
className={cn(
'group relative flex items-center rounded-md px-1.5 py-1.5 cursor-pointer',
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6',
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover',
)}
onClick={onSelect}
>
@@ -192,7 +192,7 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
const remainingCount = filteredGroups.length - MAX_VISIBLE;
return (
<div className={cn('flex h-full flex-col bg-background/50 dark:bg-neutral-900/80 text-foreground border-r border-border/30', className)}>
<div className={cn('flex h-full flex-col text-foreground border-r border-border/30', className)}>
{/* Search Input */}
<div className="px-2.5 pt-3 pb-2">
<div className="relative">
@@ -33,13 +33,13 @@ function formatCommitDate(date: string) {
function getChangeTypeColor(changeType: string) {
switch (changeType) {
case 'A':
return 'text-emerald-500';
return 'text-[var(--status-success)]';
case 'D':
return 'text-red-500';
return 'text-[var(--status-error)]';
case 'M':
return 'text-amber-500';
return 'text-[var(--status-warning)]';
case 'R':
return 'text-blue-500';
return 'text-[var(--status-info)]';
default:
return 'text-muted-foreground';
}