diff --git a/apps/ui/src/components/ui/json-syntax-editor.tsx b/apps/ui/src/components/ui/json-syntax-editor.tsx
new file mode 100644
index 00000000..236b265a
--- /dev/null
+++ b/apps/ui/src/components/ui/json-syntax-editor.tsx
@@ -0,0 +1,140 @@
+import CodeMirror from '@uiw/react-codemirror';
+import { StreamLanguage } from '@codemirror/language';
+import { javascript } from '@codemirror/legacy-modes/mode/javascript';
+import { EditorView } from '@codemirror/view';
+import { Extension } from '@codemirror/state';
+import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
+import { tags as t } from '@lezer/highlight';
+import { cn } from '@/lib/utils';
+
+interface JsonSyntaxEditorProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ className?: string;
+ minHeight?: string;
+ maxHeight?: string;
+ readOnly?: boolean;
+ 'data-testid'?: string;
+}
+
+// Syntax highlighting using CSS variables for theme compatibility
+const syntaxColors = HighlightStyle.define([
+ // Property names (keys)
+ { tag: t.propertyName, color: 'var(--chart-2, oklch(0.6 0.118 184.704))' },
+
+ // Strings (values)
+ { tag: t.string, color: 'var(--chart-1, oklch(0.646 0.222 41.116))' },
+
+ // Numbers
+ { tag: t.number, color: 'var(--chart-3, oklch(0.7 0.15 150))' },
+
+ // Booleans and null
+ { tag: t.bool, color: 'var(--chart-4, oklch(0.7 0.15 280))' },
+ { tag: t.null, color: 'var(--chart-4, oklch(0.7 0.15 280))' },
+ { tag: t.keyword, color: 'var(--chart-4, oklch(0.7 0.15 280))' },
+
+ // Brackets and punctuation
+ { tag: t.bracket, color: 'var(--muted-foreground)' },
+ { tag: t.punctuation, color: 'var(--muted-foreground)' },
+
+ // Default text
+ { tag: t.content, color: 'var(--foreground)' },
+]);
+
+// Editor theme using CSS variables
+const editorTheme = EditorView.theme({
+ '&': {
+ height: '100%',
+ fontSize: '0.8125rem',
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
+ backgroundColor: 'transparent',
+ color: 'var(--foreground)',
+ },
+ '.cm-scroller': {
+ overflow: 'auto',
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
+ },
+ '.cm-content': {
+ padding: '0.75rem',
+ minHeight: '100%',
+ caretColor: 'var(--primary)',
+ },
+ '.cm-cursor, .cm-dropCursor': {
+ borderLeftColor: 'var(--primary)',
+ },
+ '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
+ backgroundColor: 'oklch(0.55 0.25 265 / 0.3)',
+ },
+ '.cm-activeLine': {
+ backgroundColor: 'var(--accent)',
+ opacity: '0.3',
+ },
+ '.cm-line': {
+ padding: '0 0.25rem',
+ },
+ '&.cm-focused': {
+ outline: 'none',
+ },
+ '.cm-gutters': {
+ backgroundColor: 'transparent',
+ color: 'var(--muted-foreground)',
+ border: 'none',
+ paddingRight: '0.5rem',
+ },
+ '.cm-lineNumbers .cm-gutterElement': {
+ minWidth: '2.5rem',
+ textAlign: 'right',
+ paddingRight: '0.5rem',
+ },
+ '.cm-placeholder': {
+ color: 'var(--muted-foreground)',
+ fontStyle: 'italic',
+ },
+});
+
+// JavaScript language in JSON mode
+const jsonLanguage = StreamLanguage.define(javascript);
+
+// Combine all extensions
+const extensions: Extension[] = [jsonLanguage, syntaxHighlighting(syntaxColors), editorTheme];
+
+export function JsonSyntaxEditor({
+ value,
+ onChange,
+ placeholder,
+ className,
+ minHeight = '300px',
+ maxHeight,
+ readOnly = false,
+ 'data-testid': testId,
+}: JsonSyntaxEditorProps) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx
index ff614302..1ddf0a39 100644
--- a/apps/ui/src/components/views/settings-view.tsx
+++ b/apps/ui/src/components/views/settings-view.tsx
@@ -29,6 +29,7 @@ import {
import { MCPServersSection } from './settings-view/mcp-servers';
import { PromptCustomizationSection } from './settings-view/prompts';
import { EventHooksSection } from './settings-view/event-hooks';
+import { ImportExportDialog } from './settings-view/components/import-export-dialog';
import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
import type { Project as ElectronProject } from '@/lib/electron';
@@ -114,6 +115,7 @@ export function SettingsView() {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
+ const [showImportExportDialog, setShowImportExportDialog] = useState(false);
// Mobile navigation state - default to showing on desktop, hidden on mobile
const [showNavigation, setShowNavigation] = useState(() => {
@@ -239,6 +241,7 @@ export function SettingsView() {
setShowNavigation(!showNavigation)}
+ onImportExportClick={() => setShowImportExportDialog(true)}
/>
{/* Content Area with Sidebar */}
@@ -269,6 +272,9 @@ export function SettingsView() {
project={currentProject}
onConfirm={moveProjectToTrash}
/>
+
+ {/* Import/Export Settings Dialog */}
+
);
}
diff --git a/apps/ui/src/components/views/settings-view/components/import-export-dialog.tsx b/apps/ui/src/components/views/settings-view/components/import-export-dialog.tsx
new file mode 100644
index 00000000..9d609f71
--- /dev/null
+++ b/apps/ui/src/components/views/settings-view/components/import-export-dialog.tsx
@@ -0,0 +1,203 @@
+import { useState, useEffect } from 'react';
+import { Copy, Check, AlertCircle, Save } from 'lucide-react';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { JsonSyntaxEditor } from '@/components/ui/json-syntax-editor';
+import { apiGet, apiPut } from '@/lib/api-fetch';
+import { toast } from 'sonner';
+import type { GlobalSettings } from '@automaker/types';
+
+interface ImportExportDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+interface SettingsResponse {
+ success: boolean;
+ settings: GlobalSettings;
+}
+
+export function ImportExportDialog({ open, onOpenChange }: ImportExportDialogProps) {
+ const [jsonValue, setJsonValue] = useState('');
+ const [originalValue, setOriginalValue] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [copied, setCopied] = useState(false);
+ const [parseError, setParseError] = useState(null);
+
+ // Load current settings when dialog opens
+ useEffect(() => {
+ if (open) {
+ loadSettings();
+ }
+ }, [open]);
+
+ const loadSettings = async () => {
+ setIsLoading(true);
+ try {
+ const response = await apiGet('/api/settings/global');
+ if (response.success) {
+ const formatted = JSON.stringify(response.settings, null, 2);
+ setJsonValue(formatted);
+ setOriginalValue(formatted);
+ setParseError(null);
+ }
+ } catch (error) {
+ toast.error('Failed to load settings');
+ console.error('Failed to load settings:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Validate JSON on change
+ const handleJsonChange = (value: string) => {
+ setJsonValue(value);
+ try {
+ JSON.parse(value);
+ setParseError(null);
+ } catch {
+ setParseError('Invalid JSON syntax');
+ }
+ };
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(jsonValue);
+ setCopied(true);
+ toast.success('Settings copied to clipboard');
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ toast.error('Failed to copy to clipboard');
+ }
+ };
+
+ const handleSave = async () => {
+ if (parseError) {
+ toast.error('Please fix JSON syntax errors before saving');
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ const settings = JSON.parse(jsonValue);
+ const response = await apiPut('/api/settings/global', settings);
+ if (response.success) {
+ const formatted = JSON.stringify(response.settings, null, 2);
+ setJsonValue(formatted);
+ setOriginalValue(formatted);
+ toast.success('Settings saved successfully', {
+ description: 'Your changes have been applied. Some settings may require a refresh.',
+ });
+ onOpenChange(false);
+ }
+ } catch (error) {
+ toast.error('Failed to save settings');
+ console.error('Failed to save settings:', error);
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleReset = () => {
+ setJsonValue(originalValue);
+ setParseError(null);
+ };
+
+ const hasChanges = jsonValue !== originalValue;
+
+ return (
+
+ );
+}
diff --git a/apps/ui/src/components/views/settings-view/components/settings-header.tsx b/apps/ui/src/components/views/settings-view/components/settings-header.tsx
index 9d1b9ff5..25cf8dc5 100644
--- a/apps/ui/src/components/views/settings-view/components/settings-header.tsx
+++ b/apps/ui/src/components/views/settings-view/components/settings-header.tsx
@@ -1,4 +1,4 @@
-import { Settings, PanelLeft, PanelLeftClose } from 'lucide-react';
+import { Settings, PanelLeft, PanelLeftClose, FileJson } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
@@ -7,6 +7,7 @@ interface SettingsHeaderProps {
description?: string;
showNavigation?: boolean;
onToggleNavigation?: () => void;
+ onImportExportClick?: () => void;
}
export function SettingsHeader({
@@ -14,6 +15,7 @@ export function SettingsHeader({
description = 'Configure your API keys and preferences',
showNavigation,
onToggleNavigation,
+ onImportExportClick,
}: SettingsHeaderProps) {
return (
-
- {/* Mobile menu toggle button - only visible on mobile */}
- {onToggleNavigation && (
-