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}