feat(api-profile-ux): implement tabbed profile editor and fix disclaimer visibility

This commit is contained in:
kaitranntt
2025-12-10 22:14:01 -05:00
parent 83570050ef
commit 8c9d669cce
5 changed files with 348 additions and 315 deletions
+7 -5
View File
@@ -43,16 +43,18 @@ function Layout() {
return (
<SidebarProvider>
<AppSidebar />
<main className="flex-1 overflow-auto">
<header className="flex h-12 items-center justify-end px-4 border-b">
<main className="flex-1 flex flex-col min-h-0 overflow-hidden">
<header className="flex h-12 items-center justify-end px-4 border-b shrink-0">
<div className="flex items-center gap-4">
<ConnectionIndicator />
<ThemeToggle />
</div>
</header>
<Suspense fallback={<PageLoader />}>
<Outlet />
</Suspense>
<div className="flex-1 overflow-auto pb-12">
<Suspense fallback={<PageLoader />}>
<Outlet />
</Suspense>
</div>
<LocalhostDisclaimer />
</main>
</SidebarProvider>
+1 -18
View File
@@ -1,30 +1,13 @@
import { Shield, X } from 'lucide-react';
import { useState } from 'react';
import { useSidebar } from '@/hooks/use-sidebar';
export function LocalhostDisclaimer() {
const [dismissed, setDismissed] = useState(false);
const { state, isMobile } = useSidebar();
if (dismissed) return null;
// Calculate the left margin based on sidebar state
// When expanded: sidebar width is 16rem
// When collapsed: sidebar width is 3rem
// On mobile: sidebar is overlay, no margin needed
const getLeftMargin = () => {
if (isMobile) return '0';
return state === 'expanded' ? '16rem' : '3rem';
};
return (
<div
className="fixed bottom-0 bg-yellow-50 dark:bg-yellow-900/20 border-t border-yellow-200 dark:border-yellow-800 px-4 py-2 transition-all duration-200 ease-linear z-50"
style={{
left: getLeftMargin(),
right: '0',
}}
>
<div className="fixed bottom-0 left-0 right-0 bg-yellow-50 dark:bg-yellow-900/20 border-t border-yellow-200 dark:border-yellow-800 px-4 py-2 z-50 md:pl-[var(--sidebar-width)] transition-[padding] duration-200">
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2 text-sm text-yellow-800 dark:text-yellow-200">
<Shield className="w-4 h-4 flex-shrink-0" />
+268 -290
View File
@@ -1,6 +1,6 @@
/**
* Profile Editor Component
* Inline editor for API profile settings with tabs for Environment/Raw JSON/Info
* Inline editor for API profile settings with 2-column layout (Friendly UI + Raw JSON)
*/
import { useState, useMemo, useCallback, lazy, Suspense } from 'react';
@@ -9,24 +9,13 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { MaskedInput } from '@/components/ui/masked-input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ConfirmDialog } from '@/components/confirm-dialog';
import {
Save,
Loader2,
Code2,
Settings,
Info,
Terminal,
Trash2,
RefreshCw,
Plus,
X,
} from 'lucide-react';
import { Save, Loader2, Code2, Trash2, RefreshCw, Plus, X, Info } from 'lucide-react';
import { toast } from 'sonner';
import { CopyButton } from '@/components/ui/copy-button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
// Lazy load CodeEditor to reduce initial bundle size
const CodeEditor = lazy(() =>
@@ -53,7 +42,6 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
const [localEdits, setLocalEdits] = useState<Record<string, string>>({});
const [conflictDialog, setConflictDialog] = useState(false);
const [rawJsonEdits, setRawJsonEdits] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState('env');
const [newEnvKey, setNewEnvKey] = useState('');
const queryClient = useQueryClient();
@@ -80,7 +68,18 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
}, []);
// Derive current settings by merging original data with local edits
// Prioritize rawJsonEdits if available
const currentSettings = useMemo((): Settings | undefined => {
if (rawJsonEdits !== null) {
try {
return JSON.parse(rawJsonEdits);
} catch {
// If invalid JSON, fall back to undefined or partial state
// The UI will likely show empty or potentially broken state if JSON is invalid,
// but the Raw Editor will show the error.
}
}
if (!settings) return undefined;
return {
...settings,
@@ -89,7 +88,38 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
...localEdits,
},
};
}, [settings, localEdits]);
}, [settings, localEdits, rawJsonEdits]);
// Sync Visual Editor changes to Raw JSON
const updateEnvValue = (key: string, value: string) => {
const newEnv = { ...(currentSettings?.env || {}), [key]: value };
// Update local edits
setLocalEdits((prev) => ({
...prev,
[key]: value,
}));
// Update rawJsonEdits to keep sync
const newSettings = { ...currentSettings, env: newEnv };
setRawJsonEdits(JSON.stringify(newSettings, null, 2));
};
const addNewEnvVar = () => {
if (!newEnvKey.trim()) return;
const key = newEnvKey.trim();
const newEnv = { ...(currentSettings?.env || {}), [key]: '' };
setLocalEdits((prev) => ({
...prev,
[key]: '',
}));
const newSettings = { ...currentSettings, env: newEnv };
setRawJsonEdits(JSON.stringify(newSettings, null, 2));
setNewEnvKey('');
};
// Check if raw JSON is valid
const isRawJsonValid = useMemo(() => {
@@ -103,24 +133,22 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
// Check if there are unsaved changes
const hasChanges = useMemo(() => {
if (activeTab === 'raw') {
return rawJsonEdits !== null;
if (rawJsonEdits !== null) {
return rawJsonEdits !== JSON.stringify(settings, null, 2);
}
return Object.keys(localEdits).length > 0;
}, [activeTab, rawJsonEdits, localEdits]);
}, [rawJsonEdits, localEdits, settings]);
// Save mutation
const saveMutation = useMutation({
mutationFn: async () => {
let settingsToSave: Settings;
if (activeTab === 'raw') {
try {
settingsToSave = JSON.parse(rawJsonContent);
} catch {
throw new Error('Invalid JSON');
}
} else {
try {
// Always save from rawJsonContent as it's the source of truth
settingsToSave = JSON.parse(rawJsonContent);
} catch {
// Fallback (should typically not happen if validation is correct)
settingsToSave = {
...data?.settings,
env: {
@@ -180,22 +208,6 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
}
};
const updateEnvValue = (key: string, value: string) => {
setLocalEdits((prev) => ({
...prev,
[key]: value,
}));
};
const addNewEnvVar = () => {
if (!newEnvKey.trim()) return;
setLocalEdits((prev) => ({
...prev,
[newEnvKey.trim()]: '',
}));
setNewEnvKey('');
};
const isSensitiveKey = (key: string): boolean => {
const sensitivePatterns = [
/^ANTHROPIC_AUTH_TOKEN$/,
@@ -212,10 +224,206 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
// Reset state when profile changes
const profileKey = profileName;
// Render Left Column Content (Environment + Info + Usage)
const renderFriendlyUI = () => (
<div className="h-full flex flex-col">
<Tabs defaultValue="env" className="h-full flex flex-col">
<div className="px-4 pt-4 shrink-0">
<TabsList className="w-full">
<TabsTrigger value="env" className="flex-1">
Environment Variables
</TabsTrigger>
<TabsTrigger value="info" className="flex-1">
Info & Usage
</TabsTrigger>
</TabsList>
</div>
<div className="flex-1 overflow-hidden flex flex-col">
<TabsContent
value="env"
className="flex-1 mt-0 border-0 p-0 data-[state=inactive]:hidden flex flex-col overflow-hidden"
>
{/* Scrollable Environment Variables List */}
<ScrollArea className="flex-1">
<div className="p-4 space-y-4">
{currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? (
<>
{Object.entries(currentSettings.env).map(([key, value]) => (
<div key={key} className="space-y-1.5">
<Label className="text-xs font-medium flex items-center gap-2 text-muted-foreground">
{key}
{isSensitiveKey(key) && (
<Badge variant="secondary" className="text-[10px] px-1 py-0 h-4">
sensitive
</Badge>
)}
</Label>
{isSensitiveKey(key) ? (
<MaskedInput
value={value}
onChange={(e) => updateEnvValue(key, e.target.value)}
className="font-mono text-sm h-8"
/>
) : (
<Input
value={value}
onChange={(e) => updateEnvValue(key, e.target.value)}
className="font-mono text-sm h-8"
/>
)}
</div>
))}
</>
) : (
<div className="py-8 text-center text-muted-foreground bg-muted/30 rounded-lg border border-dashed text-sm">
<p>No environment variables configured.</p>
<p className="text-xs mt-1 opacity-70">
Add variables using the input below or edit the JSON directly.
</p>
</div>
)}
</div>
</ScrollArea>
{/* Fixed Add Input at Bottom */}
<div className="p-4 border-t bg-background shrink-0">
<Label className="text-xs font-medium text-muted-foreground">
Add Environment Variable
</Label>
<div className="flex gap-2 mt-2">
<Input
placeholder="VARIABLE_NAME"
value={newEnvKey}
onChange={(e) => setNewEnvKey(e.target.value.toUpperCase())}
className="font-mono text-sm h-8"
onKeyDown={(e) => e.key === 'Enter' && addNewEnvVar()}
/>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={addNewEnvVar}
disabled={!newEnvKey.trim()}
>
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
</TabsContent>
<TabsContent
value="info"
className="h-full mt-0 border-0 p-0 data-[state=inactive]:hidden"
>
<ScrollArea className="h-full">
<div className="p-4 space-y-6">
{/* Profile Information */}
<div>
<h3 className="text-sm font-medium flex items-center gap-2 mb-3">
<Info className="w-4 h-4" />
Profile Information
</h3>
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
{data && (
<>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">Profile Name</span>
<span className="font-mono">{data.profile}</span>
</div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">File Path</span>
<div className="flex items-center gap-2 min-w-0">
<code className="bg-muted px-1.5 py-0.5 rounded text-xs break-all">
{data.path}
</code>
<CopyButton value={data.path} size="icon" className="h-5 w-5" />
</div>
</div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">Last Modified</span>
<span className="text-xs">{new Date(data.mtime).toLocaleString()}</span>
</div>
</>
)}
</div>
</div>
{/* Usage */}
<div>
<h3 className="text-sm font-medium mb-3">Quick Usage</h3>
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
<div>
<Label className="text-xs text-muted-foreground">Run with profile</Label>
<div className="mt-1 flex gap-2">
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
ccs {profileName} "prompt"
</code>
<CopyButton
value={`ccs ${profileName} "prompt"`}
size="icon"
className="h-6 w-6"
/>
</div>
</div>
<div>
<Label className="text-xs text-muted-foreground">Set as default</Label>
<div className="mt-1 flex gap-2">
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
ccs default {profileName}
</code>
<CopyButton
value={`ccs default ${profileName}`}
size="icon"
className="h-6 w-6"
/>
</div>
</div>
</div>
</div>
</div>
</ScrollArea>
</TabsContent>
</div>
</Tabs>
</div>
);
// Render Right Column Content (Raw JSON Editor)
const renderRawEditor = () => (
<Suspense
fallback={
<div className="flex items-center justify-center h-full">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">Loading editor...</span>
</div>
}
>
<div className="h-full flex flex-col">
{!isRawJsonValid && rawJsonEdits !== null && (
<div className="mb-2 px-3 py-2 bg-destructive/10 text-destructive text-sm rounded-md flex items-center gap-2 mx-6 mt-4 shrink-0">
<X className="w-4 h-4" />
Invalid JSON syntax
</div>
)}
<div className="flex-1 overflow-hidden px-6 pb-6 pt-4">
<div className="h-full border rounded-md overflow-hidden bg-background">
<CodeEditor
value={rawJsonContent}
onChange={handleRawJsonChange}
language="json"
minHeight="100%"
/>
</div>
</div>
</div>
</Suspense>
);
return (
<div key={profileKey} className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b bg-background flex items-center justify-between">
<div className="px-6 py-4 border-b bg-background flex items-center justify-between shrink-0">
<div>
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">{profileName}</h2>
@@ -243,9 +451,7 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
<Button
size="sm"
onClick={handleSave}
disabled={
saveMutation.isPending || !hasChanges || (activeTab === 'raw' && !isRawJsonValid)
}
disabled={saveMutation.isPending || !hasChanges || !isRawJsonValid}
>
{saveMutation.isPending ? (
<>
@@ -268,250 +474,22 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
<span className="ml-3 text-muted-foreground">Loading settings...</span>
</div>
) : (
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex-1 flex flex-col overflow-hidden"
>
<div className="px-6 pt-4">
<TabsList className="w-full justify-start bg-muted/50">
<TabsTrigger value="env" className="gap-1.5">
<Settings className="w-4 h-4" />
Environment
</TabsTrigger>
<TabsTrigger value="raw" className="gap-1.5">
<Code2 className="w-4 h-4" />
Raw JSON
</TabsTrigger>
<TabsTrigger value="usage" className="gap-1.5">
<Terminal className="w-4 h-4" />
Usage
</TabsTrigger>
<TabsTrigger value="info" className="gap-1.5">
<Info className="w-4 h-4" />
Info
</TabsTrigger>
</TabsList>
// Split Layout (40% Left / 60% Right)
<div className="flex-1 grid grid-cols-[40%_60%] divide-x overflow-hidden">
{/* Left Column: Friendly UI */}
<div className="flex flex-col overflow-hidden bg-muted/5">{renderFriendlyUI()}</div>
{/* Right Column: Raw Editor */}
<div className="flex flex-col overflow-hidden">
<div className="px-6 py-2 bg-muted/30 border-b flex items-center gap-2 shrink-0 h-[45px]">
<Code2 className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium text-muted-foreground">
Raw Configuration (JSON)
</span>
</div>
{renderRawEditor()}
</div>
{/* Environment Tab */}
<TabsContent value="env" className="flex-1 overflow-hidden px-6 pb-6 mt-0">
<ScrollArea className="h-full">
<div className="space-y-6 py-4">
{currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? (
<>
{Object.entries(currentSettings.env).map(([key, value]) => (
<div key={key} className="space-y-1.5">
<Label className="text-sm font-medium flex items-center gap-2">
{key}
{isSensitiveKey(key) && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
sensitive
</Badge>
)}
</Label>
{isSensitiveKey(key) ? (
<MaskedInput
value={value}
onChange={(e) => updateEnvValue(key, e.target.value)}
className="font-mono text-sm"
/>
) : (
<Input
value={value}
onChange={(e) => updateEnvValue(key, e.target.value)}
className="font-mono text-sm"
/>
)}
</div>
))}
{/* Add new env var */}
<div className="pt-4 border-t">
<Label className="text-sm font-medium text-muted-foreground">
Add Environment Variable
</Label>
<div className="flex gap-2 mt-2">
<Input
placeholder="VARIABLE_NAME"
value={newEnvKey}
onChange={(e) => setNewEnvKey(e.target.value.toUpperCase())}
className="font-mono text-sm"
onKeyDown={(e) => e.key === 'Enter' && addNewEnvVar()}
/>
<Button
variant="outline"
size="sm"
onClick={addNewEnvVar}
disabled={!newEnvKey.trim()}
>
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
</>
) : (
<div className="py-12 text-center text-muted-foreground bg-muted/30 rounded-lg border border-dashed">
<p>No environment variables configured.</p>
<p className="text-xs mt-1">
Add variables using the Raw JSON tab or the form below.
</p>
<div className="flex gap-2 mt-4 justify-center">
<Input
placeholder="VARIABLE_NAME"
value={newEnvKey}
onChange={(e) => setNewEnvKey(e.target.value.toUpperCase())}
className="font-mono text-sm max-w-xs"
onKeyDown={(e) => e.key === 'Enter' && addNewEnvVar()}
/>
<Button
variant="outline"
size="sm"
onClick={addNewEnvVar}
disabled={!newEnvKey.trim()}
>
<Plus className="w-4 h-4 mr-1" />
Add
</Button>
</div>
</div>
)}
</div>
</ScrollArea>
</TabsContent>
{/* Raw JSON Tab */}
<TabsContent value="raw" className="flex-1 overflow-hidden px-6 pb-6 mt-0">
<Suspense
fallback={
<div className="flex items-center justify-center h-full">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">Loading editor...</span>
</div>
}
>
<div className="h-full flex flex-col">
{!isRawJsonValid && rawJsonEdits !== null && (
<div className="mb-2 px-3 py-2 bg-destructive/10 text-destructive text-sm rounded-md flex items-center gap-2">
<X className="w-4 h-4" />
Invalid JSON syntax
</div>
)}
<div className="flex-1 border rounded-md overflow-hidden">
<CodeEditor
value={rawJsonContent}
onChange={handleRawJsonChange}
language="json"
minHeight="100%"
/>
</div>
</div>
</Suspense>
</TabsContent>
{/* Usage Tab */}
<TabsContent value="usage" className="flex-1 overflow-hidden px-6 pb-6 mt-0">
<ScrollArea className="h-full">
<div className="py-4 space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-base">CLI Usage</CardTitle>
<CardDescription>Use this profile from the command line</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label className="text-xs text-muted-foreground">
Run Claude with this profile
</Label>
<code className="block mt-1 p-3 bg-muted rounded-md text-sm font-mono">
ccs {profileName} "your prompt here"
</code>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Start interactive session
</Label>
<code className="block mt-1 p-3 bg-muted rounded-md text-sm font-mono">
ccs {profileName}
</code>
</div>
<div>
<Label className="text-xs text-muted-foreground">Set as default</Label>
<code className="block mt-1 p-3 bg-muted rounded-md text-sm font-mono">
ccs default {profileName}
</code>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Environment Variables</CardTitle>
<CardDescription>Variables set when using this profile</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? (
Object.entries(currentSettings.env).map(([key, value]) => (
<div key={key} className="flex items-center gap-2 text-sm">
<Badge variant="outline" className="font-mono text-xs">
{key}
</Badge>
<span className="text-muted-foreground font-mono truncate">
{isSensitiveKey(key) ? '••••••••' : value}
</span>
</div>
))
) : (
<p className="text-sm text-muted-foreground">
No environment variables set
</p>
)}
</div>
</CardContent>
</Card>
</div>
</ScrollArea>
</TabsContent>
{/* Info Tab */}
<TabsContent value="info" className="flex-1 overflow-hidden px-6 pb-6 mt-0">
<ScrollArea className="h-full">
<div className="py-4">
<Card>
<CardHeader>
<CardTitle className="text-base">Profile Information</CardTitle>
<CardDescription>Details about this configuration file</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{data && (
<>
<div className="grid grid-cols-[120px_1fr] gap-2 text-sm">
<span className="font-medium text-muted-foreground">Profile Name</span>
<span className="font-mono">{data.profile}</span>
</div>
<div className="grid grid-cols-[120px_1fr] gap-2 text-sm">
<span className="font-medium text-muted-foreground">File Path</span>
<code className="bg-muted p-1 rounded text-xs break-all">
{data.path}
</code>
</div>
<div className="grid grid-cols-[120px_1fr] gap-2 text-sm">
<span className="font-medium text-muted-foreground">Last Modified</span>
<span>{new Date(data.mtime).toLocaleString()}</span>
</div>
<div className="grid grid-cols-[120px_1fr] gap-2 text-sm">
<span className="font-medium text-muted-foreground">Variables</span>
<span>{Object.keys(currentSettings?.env || {}).length} configured</span>
</div>
</>
)}
</CardContent>
</Card>
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</div>
)}
<ConfirmDialog
+60
View File
@@ -0,0 +1,60 @@
import { Check, Copy } from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
interface CopyButtonProps {
value: string;
className?: string;
variant?: 'default' | 'outline' | 'ghost' | 'secondary';
size?: 'default' | 'sm' | 'lg' | 'icon';
label?: string;
}
export function CopyButton({
value,
className,
variant = 'ghost',
size = 'icon',
label = 'Copy to clipboard',
}: CopyButtonProps) {
const [hasCopied, setHasCopied] = useState(false);
const onCopy = () => {
navigator.clipboard.writeText(value);
setHasCopied(true);
setTimeout(() => setHasCopied(false), 2000);
};
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size={size}
variant={variant}
className={cn(
'h-6 w-6 relative z-10 text-foreground/70 hover:text-foreground',
className
)}
onClick={(e) => {
e.stopPropagation();
onCopy();
}}
>
{hasCopied ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{hasCopied ? 'Copied!' : label}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
+12 -2
View File
@@ -26,6 +26,7 @@ import { useProfiles, useDeleteProfile } from '@/hooks/use-profiles';
import { ConfirmDialog } from '@/components/confirm-dialog';
import type { Profile } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { CopyButton } from '@/components/ui/copy-button';
export function ApiPage() {
const { data, isLoading } = useProfiles();
@@ -74,7 +75,7 @@ export function ApiPage() {
const selectedProfileData = profiles.find((p) => p.name === effectiveSelectedProfile);
return (
<div className="h-[calc(100vh-60px)] flex">
<div className="h-[calc(100vh-100px)] flex">
{/* Left Panel - Profiles List */}
<div className="w-80 border-r flex flex-col bg-muted/30">
{/* Header */}
@@ -248,7 +249,16 @@ function ProfileListItem({
{/* Profile info */}
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{profile.name}</div>
<div className="text-xs text-muted-foreground truncate">{profile.settingsPath}</div>
<div className="flex items-center gap-1.5 min-w-0">
<div className="text-xs text-muted-foreground truncate flex-1">
{profile.settingsPath}
</div>
<CopyButton
value={profile.settingsPath}
size="icon"
className="h-5 w-5 opacity-0 group-hover:opacity-100 transition-opacity"
/>
</div>
</div>
{/* Actions */}