From 1d9353555167ec42c83b5dfc1aaf548629640875 Mon Sep 17 00:00:00 2001 From: francy51 Date: Sat, 4 Apr 2026 21:04:05 -0400 Subject: [PATCH] Add dedicated settings workspace --- MosaicIQ/src/App.tsx | 71 ++- .../components/Settings/AgentSettingsForm.tsx | 308 ++++++++++ .../Settings/AgentSettingsModal.tsx | 544 ------------------ .../src/components/Settings/SettingsPage.tsx | 274 +++++++++ MosaicIQ/src/components/Sidebar/Sidebar.tsx | 62 +- MosaicIQ/src/components/TabBar/TabBar.tsx | 17 - 6 files changed, 686 insertions(+), 590 deletions(-) create mode 100644 MosaicIQ/src/components/Settings/AgentSettingsForm.tsx delete mode 100644 MosaicIQ/src/components/Settings/AgentSettingsModal.tsx create mode 100644 MosaicIQ/src/components/Settings/SettingsPage.tsx diff --git a/MosaicIQ/src/App.tsx b/MosaicIQ/src/App.tsx index c9c0b83..97b9d0c 100644 --- a/MosaicIQ/src/App.tsx +++ b/MosaicIQ/src/App.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useRef } from 'react'; import { Terminal } from './components/Terminal/Terminal'; import { Sidebar } from './components/Sidebar/Sidebar'; import { TabBar } from './components/TabBar/TabBar'; -import { AgentSettingsModal } from './components/Settings/AgentSettingsModal'; +import { SettingsPage } from './components/Settings/SettingsPage'; import { useTabs } from './hooks/useTabs'; import { createEntry } from './hooks/useTerminal'; import { agentSettingsBridge } from './lib/agentSettingsBridge'; @@ -10,12 +10,14 @@ import { terminalBridge } from './lib/terminalBridge'; import { AgentConfigStatus } from './types/agentSettings'; import './App.css'; +type AppView = 'terminal' | 'settings'; + function App() { const tabs = useTabs(); const [sidebarOpen, setSidebarOpen] = React.useState(true); const [isProcessing, setIsProcessing] = React.useState(false); const [agentStatus, setAgentStatus] = React.useState(null); - const [isSettingsOpen, setIsSettingsOpen] = React.useState(false); + const [activeView, setActiveView] = React.useState('terminal'); const commandHistoryRefs = useRef>({}); const commandIndexRefs = useRef>({}); @@ -55,10 +57,14 @@ function App() { try { await refreshAgentStatus(); } finally { - setIsSettingsOpen(true); + setActiveView('settings'); } }, [refreshAgentStatus]); + const handleReturnToTerminal = useCallback(() => { + setActiveView('terminal'); + }, []); + const handleCommand = useCallback(async (command: string) => { const trimmedCommand = command.trim(); const workspaceId = tabs.activeWorkspaceId; @@ -71,6 +77,8 @@ function App() { return; } + setActiveView('terminal'); + if (trimmedCommand === '/clear' || trimmedCommand.toLowerCase() === 'clear') { clearWorkspaceSession(workspaceId); return; @@ -239,10 +247,12 @@ function App() { }, [tabs.workspaces]); const clearTerminal = useCallback(() => { + setActiveView('terminal'); clearWorkspaceSession(tabs.activeWorkspaceId); }, [clearWorkspaceSession, tabs.activeWorkspaceId]); const handleCreateWorkspace = useCallback(() => { + setActiveView('terminal'); tabs.createWorkspace(); }, [tabs]); @@ -267,11 +277,16 @@ function App() { e.preventDefault(); clearTerminal(); } + + if ((e.metaKey || e.ctrlKey) && e.key === ',') { + e.preventDefault(); + void handleOpenSettings(); + } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [tabs, clearTerminal, handleCreateWorkspace]); + }, [tabs, clearTerminal, handleCreateWorkspace, handleOpenSettings]); useEffect(() => { return () => { @@ -290,7 +305,11 @@ function App() {
{/* Sidebar */} { + void handleOpenSettings(); + }} onToggle={() => setSidebarOpen(!sidebarOpen)} onCommand={handleCommand} /> @@ -300,34 +319,34 @@ function App() { {/* Tab Bar */} tabs.setActiveWorkspace(id)} + onTabClick={(id) => { + setActiveView('terminal'); + tabs.setActiveWorkspace(id); + }} onTabClose={(id) => tabs.closeWorkspace(id)} onNewTab={handleCreateWorkspace} - onOpenSettings={() => { - void handleOpenSettings(); - }} - isAgentReady={Boolean(agentStatus?.configured)} onTabRename={(id, name) => tabs.renameWorkspace(id, name)} /> - {/* Terminal */} - + {activeView === 'settings' ? ( + + ) : ( + + )}
- - setIsSettingsOpen(false)} - onStatusChange={handleAgentStatusChange} - /> ); } diff --git a/MosaicIQ/src/components/Settings/AgentSettingsForm.tsx b/MosaicIQ/src/components/Settings/AgentSettingsForm.tsx new file mode 100644 index 0000000..35ed44d --- /dev/null +++ b/MosaicIQ/src/components/Settings/AgentSettingsForm.tsx @@ -0,0 +1,308 @@ +import React, { useEffect, useState } from 'react'; +import { agentSettingsBridge } from '../../lib/agentSettingsBridge'; +import { + AgentConfigStatus, + AgentTaskRoute, + TASK_LABELS, + TASK_PROFILES, + TaskProfile, +} from '../../types/agentSettings'; + +interface AgentSettingsFormProps { + status: AgentConfigStatus | null; + onStatusChange: (status: AgentConfigStatus) => void; +} + +const inputClassName = + 'w-full rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-sm font-mono text-[#e0e0e0] outline-none transition-colors focus:border-[#58a6ff]'; + +const buttonClassName = + 'rounded border border-[#2a2a2a] bg-[#151515] px-3 py-2 text-xs font-mono text-[#e0e0e0] transition-colors hover:border-[#58a6ff] hover:text-[#58a6ff] disabled:cursor-not-allowed disabled:opacity-50'; + +const mergeTaskDefaults = ( + taskDefaults: Partial>, + defaultRemoteModel: string, +): Record => + TASK_PROFILES.reduce((acc, profile) => { + acc[profile] = taskDefaults[profile] ?? { model: defaultRemoteModel }; + return acc; + }, {} as Record); + +export const AgentSettingsForm: React.FC = ({ + status, + onStatusChange, +}) => { + const [remoteEnabled, setRemoteEnabled] = useState(true); + const [remoteBaseUrl, setRemoteBaseUrl] = useState(''); + const [defaultRemoteModel, setDefaultRemoteModel] = useState(''); + const [taskDefaults, setTaskDefaults] = useState>( + mergeTaskDefaults({}, ''), + ); + const [remoteApiKey, setRemoteApiKey] = useState(''); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [isBusy, setIsBusy] = useState(false); + + useEffect(() => { + if (!status) { + return; + } + + setRemoteEnabled(status.remoteEnabled); + setRemoteBaseUrl(status.remoteBaseUrl); + setDefaultRemoteModel(status.defaultRemoteModel); + setTaskDefaults(mergeTaskDefaults(status.taskDefaults, status.defaultRemoteModel)); + setRemoteApiKey(''); + setError(null); + setSuccess(null); + }, [status]); + + if (!status) { + return ( +
+ Loading AI settings... +
+ ); + } + + const runtimeRequest = { + remoteEnabled, + remoteBaseUrl, + defaultRemoteModel, + taskDefaults, + }; + + const setTaskRoute = ( + task: TaskProfile, + updater: (route: AgentTaskRoute) => AgentTaskRoute, + ) => { + setTaskDefaults((current) => ({ + ...current, + [task]: updater(current[task]), + })); + }; + + const saveRuntimeConfig = async () => { + const nextStatus = await agentSettingsBridge.saveRuntimeConfig(runtimeRequest); + onStatusChange(nextStatus); + return nextStatus; + }; + + const handleSaveRuntime = async () => { + setIsBusy(true); + setError(null); + setSuccess(null); + try { + await saveRuntimeConfig(); + setSuccess('Remote settings saved.'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save runtime settings.'); + } finally { + setIsBusy(false); + } + }; + + const handleSaveRemoteApiKey = async () => { + setIsBusy(true); + setError(null); + setSuccess(null); + try { + const savedStatus = await saveRuntimeConfig(); + const nextStatus = await agentSettingsBridge.updateRemoteApiKey({ apiKey: remoteApiKey }); + onStatusChange({ ...savedStatus, ...nextStatus }); + setRemoteApiKey(''); + setSuccess(status.hasRemoteApiKey ? 'Remote API key updated.' : 'Remote API key saved.'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save remote API key.'); + } finally { + setIsBusy(false); + } + }; + + const handleClearRemoteApiKey = async () => { + setIsBusy(true); + setError(null); + setSuccess(null); + try { + const savedStatus = await saveRuntimeConfig(); + const nextStatus = await agentSettingsBridge.clearRemoteApiKey(); + onStatusChange({ ...savedStatus, ...nextStatus }); + setRemoteApiKey(''); + setSuccess('Remote API key cleared.'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to clear remote API key.'); + } finally { + setIsBusy(false); + } + }; + + const handleDefaultRemoteModelChange = (nextValue: string) => { + const previousValue = defaultRemoteModel; + setDefaultRemoteModel(nextValue); + setTaskDefaults((current) => { + const next = { ...current }; + for (const profile of TASK_PROFILES) { + if (next[profile].model.trim() === previousValue.trim()) { + next[profile] = { model: nextValue }; + } + } + return next; + }); + }; + + return ( +
+
+
+
+

Runtime Status

+

+ {status.configured ? 'Configured' : 'Configuration incomplete'} +

+
+
+
Remote ready: {status.remoteConfigured ? 'yes' : 'no'}
+
API key stored: {status.hasRemoteApiKey ? 'yes' : 'no'}
+
+
+
+ +
+
+
+

Remote Provider

+

+ OpenAI-compatible HTTP endpoint. +

+
+ +
+ +
+ + + +
+
+ +
+
+

Task Models

+

+ Choose the default remote model for each harness task. +

+
+ +
+ {TASK_PROFILES.map((task) => ( +
+
{TASK_LABELS[task]}
+ +
+ ))} +
+ +
+ +
+
+ +
+

Remote API Key

+

+ Stored in plain text for the remote OpenAI-compatible provider. +

+ + +
+
+ {status.hasRemoteApiKey ? ( + + ) : null} +
+ +
+
+ + {success ? ( +
+ {success} +
+ ) : null} + + {error ? ( +
+ {error} +
+ ) : null} +
+ ); +}; diff --git a/MosaicIQ/src/components/Settings/AgentSettingsModal.tsx b/MosaicIQ/src/components/Settings/AgentSettingsModal.tsx deleted file mode 100644 index eed47db..0000000 --- a/MosaicIQ/src/components/Settings/AgentSettingsModal.tsx +++ /dev/null @@ -1,544 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { agentSettingsBridge } from '../../lib/agentSettingsBridge'; -import { - AgentConfigStatus, - AgentTaskRoute, - ProviderMode, - TASK_LABELS, - TASK_PROFILES, - TaskProfile, -} from '../../types/agentSettings'; - -interface AgentSettingsModalProps { - isOpen: boolean; - status: AgentConfigStatus | null; - onClose: () => void; - onStatusChange: (status: AgentConfigStatus) => void; -} - -const inputClassName = - 'w-full rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-sm font-mono text-[#e0e0e0] outline-none transition-colors focus:border-[#58a6ff]'; - -const textareaClassName = `${inputClassName} min-h-28 resize-y`; - -const buttonClassName = - 'rounded border border-[#2a2a2a] bg-[#151515] px-3 py-2 text-xs font-mono text-[#e0e0e0] transition-colors hover:border-[#58a6ff] hover:text-[#58a6ff] disabled:cursor-not-allowed disabled:opacity-50'; - -const normalizeModelList = (value: string): string[] => { - const items = value - .split(/[\n,]/) - .map((item) => item.trim()) - .filter(Boolean); - - return Array.from(new Set(items)); -}; - -const mergeTaskDefaults = ( - taskDefaults: Partial>, - defaultRemoteModel: string, - localModels: string[], -): Record => { - const fallbackLocalModel = localModels[0] ?? ''; - - return TASK_PROFILES.reduce((acc, profile) => { - const existing = taskDefaults[profile]; - acc[profile] = existing ?? { - providerMode: 'remote', - model: defaultRemoteModel || fallbackLocalModel, - }; - return acc; - }, {} as Record); -}; - -export const AgentSettingsModal: React.FC = ({ - isOpen, - status, - onClose, - onStatusChange, -}) => { - const [remoteEnabled, setRemoteEnabled] = useState(true); - const [remoteBaseUrl, setRemoteBaseUrl] = useState(''); - const [defaultRemoteModel, setDefaultRemoteModel] = useState(''); - const [localEnabled, setLocalEnabled] = useState(false); - const [localBaseUrl, setLocalBaseUrl] = useState(''); - const [localAvailableModelsText, setLocalAvailableModelsText] = useState(''); - const [taskDefaults, setTaskDefaults] = useState>( - mergeTaskDefaults({}, '', []), - ); - const [remoteApiKey, setRemoteApiKey] = useState(''); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - const [localStatusMessage, setLocalStatusMessage] = useState(null); - const [isBusy, setIsBusy] = useState(false); - - useEffect(() => { - if (!status || !isOpen) { - return; - } - - setRemoteEnabled(status.remoteEnabled); - setRemoteBaseUrl(status.remoteBaseUrl); - setDefaultRemoteModel(status.defaultRemoteModel); - setLocalEnabled(status.localEnabled); - setLocalBaseUrl(status.localBaseUrl); - setLocalAvailableModelsText(status.localAvailableModels.join('\n')); - setTaskDefaults( - mergeTaskDefaults( - status.taskDefaults, - status.defaultRemoteModel, - status.localAvailableModels, - ), - ); - setRemoteApiKey(''); - setError(null); - setSuccess(null); - setLocalStatusMessage(null); - }, [isOpen, status]); - - if (!isOpen || !status) { - return null; - } - - const localAvailableModels = normalizeModelList(localAvailableModelsText); - - const runtimeRequest = { - remoteEnabled, - remoteBaseUrl, - defaultRemoteModel, - localEnabled, - localBaseUrl, - localAvailableModels, - taskDefaults, - }; - - const setTaskRoute = ( - task: TaskProfile, - updater: (route: AgentTaskRoute) => AgentTaskRoute, - ) => { - setTaskDefaults((current) => ({ - ...current, - [task]: updater(current[task]), - })); - }; - - const saveRuntimeConfig = async () => { - const nextStatus = await agentSettingsBridge.saveRuntimeConfig(runtimeRequest); - onStatusChange(nextStatus); - return nextStatus; - }; - - const handleSaveRuntime = async () => { - setIsBusy(true); - setError(null); - setSuccess(null); - try { - await saveRuntimeConfig(); - setSuccess('Provider routing saved.'); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to save runtime settings.'); - } finally { - setIsBusy(false); - } - }; - - const handleSaveRemoteApiKey = async () => { - setIsBusy(true); - setError(null); - setSuccess(null); - try { - const savedStatus = await saveRuntimeConfig(); - const nextStatus = await agentSettingsBridge.updateRemoteApiKey({ apiKey: remoteApiKey }); - onStatusChange({ ...savedStatus, ...nextStatus }); - setRemoteApiKey(''); - setSuccess( - status.hasRemoteApiKey - ? 'Remote API key updated.' - : 'Remote API key saved.', - ); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to save remote API key.'); - } finally { - setIsBusy(false); - } - }; - - const handleClearRemoteApiKey = async () => { - setIsBusy(true); - setError(null); - setSuccess(null); - try { - const savedStatus = await saveRuntimeConfig(); - const nextStatus = await agentSettingsBridge.clearRemoteApiKey(); - onStatusChange({ ...savedStatus, ...nextStatus }); - setRemoteApiKey(''); - setSuccess('Remote API key cleared.'); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to clear remote API key.'); - } finally { - setIsBusy(false); - } - }; - - const handleDiscoverLocalModels = async () => { - setIsBusy(true); - setError(null); - setSuccess(null); - setLocalStatusMessage(null); - try { - const result = await agentSettingsBridge.listLocalModels(); - setLocalAvailableModelsText(result.models.join('\n')); - setTaskDefaults((current) => { - const next = { ...current }; - for (const profile of TASK_PROFILES) { - if ( - next[profile].providerMode === 'local' && - !next[profile].model.trim() && - result.models[0] - ) { - next[profile] = { ...next[profile], model: result.models[0] }; - } - } - return next; - }); - setLocalStatusMessage( - result.reachable - ? `Discovered ${result.models.length} local model(s).` - : 'Loaded stored local models because the sidecar was unreachable.', - ); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to list local models.'); - } finally { - setIsBusy(false); - } - }; - - const handleCheckLocalHealth = async () => { - setIsBusy(true); - setError(null); - setSuccess(null); - try { - const result = await agentSettingsBridge.checkLocalProviderHealth(); - setLocalStatusMessage(result.message); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to reach local provider.'); - } finally { - setIsBusy(false); - } - }; - - const handleTaskProviderChange = (task: TaskProfile, providerMode: ProviderMode) => { - const suggestedLocalModel = localAvailableModels[0] ?? ''; - setTaskRoute(task, (route) => ({ - providerMode, - model: - providerMode === 'remote' - ? route.providerMode === 'remote' && route.model.trim() - ? route.model - : defaultRemoteModel - : route.providerMode === 'local' && route.model.trim() - ? route.model - : suggestedLocalModel, - })); - }; - - const handleDefaultRemoteModelChange = (nextValue: string) => { - const previousValue = defaultRemoteModel; - setDefaultRemoteModel(nextValue); - setTaskDefaults((current) => { - const next = { ...current }; - for (const profile of TASK_PROFILES) { - if ( - next[profile].providerMode === 'remote' && - next[profile].model.trim() === previousValue.trim() - ) { - next[profile] = { ...next[profile], model: nextValue }; - } - } - return next; - }); - }; - - return ( -
-
-
-
-

AI Settings

-

- Configure remote and local providers, then assign a default provider/model per task. -

-
- -
- -
-
-
-
-

- Runtime Status -

-

- {status.configured ? 'Configured' : 'Routing incomplete'} -

-
-
-
Remote ready: {status.remoteConfigured ? 'yes' : 'no'}
-
Local ready: {status.localConfigured ? 'yes' : 'no'}
-
-
-
- -
-
-
-

- Remote Provider -

-

- OpenAI-compatible HTTP endpoint. -

-
- -
- -
- - - -
-
- -
-
-
-

- Local Provider -

-

- Running Mistral HTTP sidecar on localhost. -

-
- -
- -
- -
- - -
-
- -