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:
committed by
GitHub
parent
5bb2c56bc0
commit
ddedc02687
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user