diff --git a/ui/src/App.tsx b/ui/src/App.tsx index c9ab07be..8ba2fe22 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -43,16 +43,18 @@ function Layout() { return ( -
-
+
+
- }> - - +
+ }> + + +
diff --git a/ui/src/components/localhost-disclaimer.tsx b/ui/src/components/localhost-disclaimer.tsx index 23a1c778..c7947163 100644 --- a/ui/src/components/localhost-disclaimer.tsx +++ b/ui/src/components/localhost-disclaimer.tsx @@ -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 ( -
+
diff --git a/ui/src/components/profile-editor.tsx b/ui/src/components/profile-editor.tsx index 11190a95..8808ee00 100644 --- a/ui/src/components/profile-editor.tsx +++ b/ui/src/components/profile-editor.tsx @@ -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>({}); const [conflictDialog, setConflictDialog] = useState(false); const [rawJsonEdits, setRawJsonEdits] = useState(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 = () => ( +
+ +
+ + + Environment Variables + + + Info & Usage + + +
+ +
+ + {/* Scrollable Environment Variables List */} + +
+ {currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? ( + <> + {Object.entries(currentSettings.env).map(([key, value]) => ( +
+ + {isSensitiveKey(key) ? ( + updateEnvValue(key, e.target.value)} + className="font-mono text-sm h-8" + /> + ) : ( + updateEnvValue(key, e.target.value)} + className="font-mono text-sm h-8" + /> + )} +
+ ))} + + ) : ( +
+

No environment variables configured.

+

+ Add variables using the input below or edit the JSON directly. +

+
+ )} +
+
+ + {/* Fixed Add Input at Bottom */} +
+ +
+ setNewEnvKey(e.target.value.toUpperCase())} + className="font-mono text-sm h-8" + onKeyDown={(e) => e.key === 'Enter' && addNewEnvVar()} + /> + +
+
+
+ + + +
+ {/* Profile Information */} +
+

+ + Profile Information +

+
+ {data && ( + <> +
+ Profile Name + {data.profile} +
+
+ File Path +
+ + {data.path} + + +
+
+
+ Last Modified + {new Date(data.mtime).toLocaleString()} +
+ + )} +
+
+ + {/* Usage */} +
+

Quick Usage

+
+
+ +
+ + ccs {profileName} "prompt" + + +
+
+
+ +
+ + ccs default {profileName} + + +
+
+
+
+
+
+
+
+
+
+ ); + + // Render Right Column Content (Raw JSON Editor) + const renderRawEditor = () => ( + + + Loading editor... +
+ } + > +
+ {!isRawJsonValid && rawJsonEdits !== null && ( +
+ + Invalid JSON syntax +
+ )} +
+
+ +
+
+
+ + ); + return (
{/* Header */} -
+

{profileName}

@@ -243,9 +451,7 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
) : ( - -
- - - - Environment - - - - Raw JSON - - - - Usage - - - - Info - - + // Split Layout (40% Left / 60% Right) +
+ {/* Left Column: Friendly UI */} +
{renderFriendlyUI()}
+ + {/* Right Column: Raw Editor */} +
+
+ + + Raw Configuration (JSON) + +
+ {renderRawEditor()}
- - {/* Environment Tab */} - - -
- {currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? ( - <> - {Object.entries(currentSettings.env).map(([key, value]) => ( -
- - {isSensitiveKey(key) ? ( - updateEnvValue(key, e.target.value)} - className="font-mono text-sm" - /> - ) : ( - updateEnvValue(key, e.target.value)} - className="font-mono text-sm" - /> - )} -
- ))} - - {/* Add new env var */} -
- -
- setNewEnvKey(e.target.value.toUpperCase())} - className="font-mono text-sm" - onKeyDown={(e) => e.key === 'Enter' && addNewEnvVar()} - /> - -
-
- - ) : ( -
-

No environment variables configured.

-

- Add variables using the Raw JSON tab or the form below. -

-
- setNewEnvKey(e.target.value.toUpperCase())} - className="font-mono text-sm max-w-xs" - onKeyDown={(e) => e.key === 'Enter' && addNewEnvVar()} - /> - -
-
- )} -
-
-
- - {/* Raw JSON Tab */} - - - - Loading editor... -
- } - > -
- {!isRawJsonValid && rawJsonEdits !== null && ( -
- - Invalid JSON syntax -
- )} -
- -
-
- - - - {/* Usage Tab */} - - -
- - - CLI Usage - Use this profile from the command line - - -
- - - ccs {profileName} "your prompt here" - -
-
- - - ccs {profileName} - -
-
- - - ccs default {profileName} - -
-
-
- - - - Environment Variables - Variables set when using this profile - - -
- {currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? ( - Object.entries(currentSettings.env).map(([key, value]) => ( -
- - {key} - - - {isSensitiveKey(key) ? '••••••••' : value} - -
- )) - ) : ( -

- No environment variables set -

- )} -
-
-
-
-
-
- - {/* Info Tab */} - - -
- - - Profile Information - Details about this configuration file - - - {data && ( - <> -
- Profile Name - {data.profile} -
-
- File Path - - {data.path} - -
-
- Last Modified - {new Date(data.mtime).toLocaleString()} -
-
- Variables - {Object.keys(currentSettings?.env || {}).length} configured -
- - )} -
-
-
-
-
- +
)} { + navigator.clipboard.writeText(value); + setHasCopied(true); + setTimeout(() => setHasCopied(false), 2000); + }; + + return ( + + + + + + +

{hasCopied ? 'Copied!' : label}

+
+
+
+ ); +} diff --git a/ui/src/pages/api.tsx b/ui/src/pages/api.tsx index d0229df3..aa21b1d2 100644 --- a/ui/src/pages/api.tsx +++ b/ui/src/pages/api.tsx @@ -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 ( -
+
{/* Left Panel - Profiles List */}
{/* Header */} @@ -248,7 +249,16 @@ function ProfileListItem({ {/* Profile info */}
{profile.name}
-
{profile.settingsPath}
+
+
+ {profile.settingsPath} +
+ +
{/* Actions */}