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...
+