Add dedicated settings workspace
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useRef } from 'react';
|
|||||||
import { Terminal } from './components/Terminal/Terminal';
|
import { Terminal } from './components/Terminal/Terminal';
|
||||||
import { Sidebar } from './components/Sidebar/Sidebar';
|
import { Sidebar } from './components/Sidebar/Sidebar';
|
||||||
import { TabBar } from './components/TabBar/TabBar';
|
import { TabBar } from './components/TabBar/TabBar';
|
||||||
import { AgentSettingsModal } from './components/Settings/AgentSettingsModal';
|
import { SettingsPage } from './components/Settings/SettingsPage';
|
||||||
import { useTabs } from './hooks/useTabs';
|
import { useTabs } from './hooks/useTabs';
|
||||||
import { createEntry } from './hooks/useTerminal';
|
import { createEntry } from './hooks/useTerminal';
|
||||||
import { agentSettingsBridge } from './lib/agentSettingsBridge';
|
import { agentSettingsBridge } from './lib/agentSettingsBridge';
|
||||||
@@ -10,12 +10,14 @@ import { terminalBridge } from './lib/terminalBridge';
|
|||||||
import { AgentConfigStatus } from './types/agentSettings';
|
import { AgentConfigStatus } from './types/agentSettings';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
|
type AppView = 'terminal' | 'settings';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const tabs = useTabs();
|
const tabs = useTabs();
|
||||||
const [sidebarOpen, setSidebarOpen] = React.useState(true);
|
const [sidebarOpen, setSidebarOpen] = React.useState(true);
|
||||||
const [isProcessing, setIsProcessing] = React.useState(false);
|
const [isProcessing, setIsProcessing] = React.useState(false);
|
||||||
const [agentStatus, setAgentStatus] = React.useState<AgentConfigStatus | null>(null);
|
const [agentStatus, setAgentStatus] = React.useState<AgentConfigStatus | null>(null);
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = React.useState(false);
|
const [activeView, setActiveView] = React.useState<AppView>('terminal');
|
||||||
const commandHistoryRefs = useRef<Record<string, string[]>>({});
|
const commandHistoryRefs = useRef<Record<string, string[]>>({});
|
||||||
const commandIndexRefs = useRef<Record<string, number>>({});
|
const commandIndexRefs = useRef<Record<string, number>>({});
|
||||||
|
|
||||||
@@ -55,10 +57,14 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
await refreshAgentStatus();
|
await refreshAgentStatus();
|
||||||
} finally {
|
} finally {
|
||||||
setIsSettingsOpen(true);
|
setActiveView('settings');
|
||||||
}
|
}
|
||||||
}, [refreshAgentStatus]);
|
}, [refreshAgentStatus]);
|
||||||
|
|
||||||
|
const handleReturnToTerminal = useCallback(() => {
|
||||||
|
setActiveView('terminal');
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCommand = useCallback(async (command: string) => {
|
const handleCommand = useCallback(async (command: string) => {
|
||||||
const trimmedCommand = command.trim();
|
const trimmedCommand = command.trim();
|
||||||
const workspaceId = tabs.activeWorkspaceId;
|
const workspaceId = tabs.activeWorkspaceId;
|
||||||
@@ -71,6 +77,8 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveView('terminal');
|
||||||
|
|
||||||
if (trimmedCommand === '/clear' || trimmedCommand.toLowerCase() === 'clear') {
|
if (trimmedCommand === '/clear' || trimmedCommand.toLowerCase() === 'clear') {
|
||||||
clearWorkspaceSession(workspaceId);
|
clearWorkspaceSession(workspaceId);
|
||||||
return;
|
return;
|
||||||
@@ -239,10 +247,12 @@ function App() {
|
|||||||
}, [tabs.workspaces]);
|
}, [tabs.workspaces]);
|
||||||
|
|
||||||
const clearTerminal = useCallback(() => {
|
const clearTerminal = useCallback(() => {
|
||||||
|
setActiveView('terminal');
|
||||||
clearWorkspaceSession(tabs.activeWorkspaceId);
|
clearWorkspaceSession(tabs.activeWorkspaceId);
|
||||||
}, [clearWorkspaceSession, tabs.activeWorkspaceId]);
|
}, [clearWorkspaceSession, tabs.activeWorkspaceId]);
|
||||||
|
|
||||||
const handleCreateWorkspace = useCallback(() => {
|
const handleCreateWorkspace = useCallback(() => {
|
||||||
|
setActiveView('terminal');
|
||||||
tabs.createWorkspace();
|
tabs.createWorkspace();
|
||||||
}, [tabs]);
|
}, [tabs]);
|
||||||
|
|
||||||
@@ -267,11 +277,16 @@ function App() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
clearTerminal();
|
clearTerminal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === ',') {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleOpenSettings();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [tabs, clearTerminal, handleCreateWorkspace]);
|
}, [tabs, clearTerminal, handleCreateWorkspace, handleOpenSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -290,7 +305,11 @@ function App() {
|
|||||||
<div className="flex h-screen bg-[#0a0a0a]">
|
<div className="flex h-screen bg-[#0a0a0a]">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<Sidebar
|
<Sidebar
|
||||||
|
isSettingsActive={activeView === 'settings'}
|
||||||
isOpen={sidebarOpen}
|
isOpen={sidebarOpen}
|
||||||
|
onOpenSettings={() => {
|
||||||
|
void handleOpenSettings();
|
||||||
|
}}
|
||||||
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||||
onCommand={handleCommand}
|
onCommand={handleCommand}
|
||||||
/>
|
/>
|
||||||
@@ -300,34 +319,34 @@ function App() {
|
|||||||
{/* Tab Bar */}
|
{/* Tab Bar */}
|
||||||
<TabBar
|
<TabBar
|
||||||
tabs={tabBarTabs}
|
tabs={tabBarTabs}
|
||||||
onTabClick={(id) => tabs.setActiveWorkspace(id)}
|
onTabClick={(id) => {
|
||||||
|
setActiveView('terminal');
|
||||||
|
tabs.setActiveWorkspace(id);
|
||||||
|
}}
|
||||||
onTabClose={(id) => tabs.closeWorkspace(id)}
|
onTabClose={(id) => tabs.closeWorkspace(id)}
|
||||||
onNewTab={handleCreateWorkspace}
|
onNewTab={handleCreateWorkspace}
|
||||||
onOpenSettings={() => {
|
|
||||||
void handleOpenSettings();
|
|
||||||
}}
|
|
||||||
isAgentReady={Boolean(agentStatus?.configured)}
|
|
||||||
onTabRename={(id, name) => tabs.renameWorkspace(id, name)}
|
onTabRename={(id, name) => tabs.renameWorkspace(id, name)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Terminal */}
|
{activeView === 'settings' ? (
|
||||||
<Terminal
|
<SettingsPage
|
||||||
history={getActiveHistory()}
|
status={agentStatus}
|
||||||
isProcessing={isProcessing}
|
onRefreshStatus={refreshAgentStatus}
|
||||||
outputRef={outputRef}
|
onStatusChange={handleAgentStatusChange}
|
||||||
onSubmit={handleCommand}
|
onBackToTerminal={handleReturnToTerminal}
|
||||||
getPreviousCommand={getPreviousCommand}
|
/>
|
||||||
getNextCommand={getNextCommand}
|
) : (
|
||||||
resetCommandIndex={resetCommandIndex}
|
<Terminal
|
||||||
/>
|
history={getActiveHistory()}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
outputRef={outputRef}
|
||||||
|
onSubmit={handleCommand}
|
||||||
|
getPreviousCommand={getPreviousCommand}
|
||||||
|
getNextCommand={getNextCommand}
|
||||||
|
resetCommandIndex={resetCommandIndex}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AgentSettingsModal
|
|
||||||
isOpen={isSettingsOpen}
|
|
||||||
status={agentStatus}
|
|
||||||
onClose={() => setIsSettingsOpen(false)}
|
|
||||||
onStatusChange={handleAgentStatusChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
308
MosaicIQ/src/components/Settings/AgentSettingsForm.tsx
Normal file
308
MosaicIQ/src/components/Settings/AgentSettingsForm.tsx
Normal file
@@ -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<Record<TaskProfile, AgentTaskRoute>>,
|
||||||
|
defaultRemoteModel: string,
|
||||||
|
): Record<TaskProfile, AgentTaskRoute> =>
|
||||||
|
TASK_PROFILES.reduce((acc, profile) => {
|
||||||
|
acc[profile] = taskDefaults[profile] ?? { model: defaultRemoteModel };
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<TaskProfile, AgentTaskRoute>);
|
||||||
|
|
||||||
|
export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||||
|
status,
|
||||||
|
onStatusChange,
|
||||||
|
}) => {
|
||||||
|
const [remoteEnabled, setRemoteEnabled] = useState(true);
|
||||||
|
const [remoteBaseUrl, setRemoteBaseUrl] = useState('');
|
||||||
|
const [defaultRemoteModel, setDefaultRemoteModel] = useState('');
|
||||||
|
const [taskDefaults, setTaskDefaults] = useState<Record<TaskProfile, AgentTaskRoute>>(
|
||||||
|
mergeTaskDefaults({}, ''),
|
||||||
|
);
|
||||||
|
const [remoteApiKey, setRemoteApiKey] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(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 (
|
||||||
|
<div className="rounded border border-[#2a2a2a] bg-[#111111] px-4 py-3 text-xs font-mono text-[#888888]">
|
||||||
|
Loading AI settings...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-mono font-semibold text-[#e0e0e0]">Runtime Status</h3>
|
||||||
|
<p className="mt-1 text-xs font-mono text-[#888888]">
|
||||||
|
{status.configured ? 'Configured' : 'Configuration incomplete'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right text-xs font-mono text-[#888888]">
|
||||||
|
<div>Remote ready: {status.remoteConfigured ? 'yes' : 'no'}</div>
|
||||||
|
<div>API key stored: {status.hasRemoteApiKey ? 'yes' : 'no'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-mono font-semibold text-[#e0e0e0]">Remote Provider</h3>
|
||||||
|
<p className="mt-1 text-xs font-mono text-[#888888]">
|
||||||
|
OpenAI-compatible HTTP endpoint.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-xs font-mono text-[#e0e0e0]">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={remoteEnabled}
|
||||||
|
onChange={(event) => setRemoteEnabled(event.target.checked)}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<label className="block">
|
||||||
|
<span className="mb-2 block text-xs font-mono text-[#888888]">Remote Base URL</span>
|
||||||
|
<input
|
||||||
|
className={inputClassName}
|
||||||
|
value={remoteBaseUrl}
|
||||||
|
onChange={(event) => setRemoteBaseUrl(event.target.value)}
|
||||||
|
placeholder="https://api.z.ai/api/coding/paas/v4"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="block">
|
||||||
|
<span className="mb-2 block text-xs font-mono text-[#888888]">
|
||||||
|
Default Remote Model
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
className={inputClassName}
|
||||||
|
value={defaultRemoteModel}
|
||||||
|
onChange={(event) => handleDefaultRemoteModelChange(event.target.value)}
|
||||||
|
placeholder="glm-5.1"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-sm font-mono font-semibold text-[#e0e0e0]">Task Models</h3>
|
||||||
|
<p className="mt-1 text-xs font-mono text-[#888888]">
|
||||||
|
Choose the default remote model for each harness task.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{TASK_PROFILES.map((task) => (
|
||||||
|
<div
|
||||||
|
key={task}
|
||||||
|
className="grid gap-3 rounded border border-[#1d1d1d] bg-[#0d0d0d] p-3 md:grid-cols-[180px_minmax(0,1fr)]"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-mono text-[#e0e0e0]">{TASK_LABELS[task]}</div>
|
||||||
|
<label className="block">
|
||||||
|
<span className="mb-2 block text-xs font-mono text-[#888888]">Model</span>
|
||||||
|
<input
|
||||||
|
className={inputClassName}
|
||||||
|
value={taskDefaults[task].model}
|
||||||
|
onChange={(event) => setTaskRoute(task, () => ({ model: event.target.value }))}
|
||||||
|
placeholder={defaultRemoteModel || 'Remote model'}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveRuntime}
|
||||||
|
disabled={isBusy}
|
||||||
|
className={buttonClassName}
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
||||||
|
<h3 className="text-sm font-mono font-semibold text-[#e0e0e0]">Remote API Key</h3>
|
||||||
|
<p className="mt-2 text-xs font-mono text-[#888888]">
|
||||||
|
Stored in plain text for the remote OpenAI-compatible provider.
|
||||||
|
</p>
|
||||||
|
<label className="mt-4 block">
|
||||||
|
<span className="mb-2 block text-xs font-mono text-[#888888]">
|
||||||
|
{status.hasRemoteApiKey ? 'Replace Remote API Key' : 'Remote API Key'}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className={inputClassName}
|
||||||
|
value={remoteApiKey}
|
||||||
|
onChange={(event) => setRemoteApiKey(event.target.value)}
|
||||||
|
placeholder="Enter remote API key"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
{status.hasRemoteApiKey ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClearRemoteApiKey}
|
||||||
|
disabled={isBusy}
|
||||||
|
className="rounded border border-[#2a2a2a] bg-[#151515] px-3 py-2 text-xs font-mono text-[#ff7b72] transition-colors hover:border-[#ff7b72] disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Clear Key
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveRemoteApiKey}
|
||||||
|
disabled={isBusy || !remoteApiKey.trim()}
|
||||||
|
className={buttonClassName}
|
||||||
|
>
|
||||||
|
{status.hasRemoteApiKey ? 'Save Settings & Update Key' : 'Save Settings & Save Key'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{success ? (
|
||||||
|
<div className="rounded border border-[#214f31] bg-[#102417] px-3 py-2 text-xs font-mono text-[#9ee6b3]">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<div className="rounded border border-[#5c2b2b] bg-[#211313] px-3 py-2 text-xs font-mono text-[#ffb4b4]">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<Record<TaskProfile, AgentTaskRoute>>,
|
|
||||||
defaultRemoteModel: string,
|
|
||||||
localModels: string[],
|
|
||||||
): Record<TaskProfile, AgentTaskRoute> => {
|
|
||||||
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<TaskProfile, AgentTaskRoute>);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AgentSettingsModal: React.FC<AgentSettingsModalProps> = ({
|
|
||||||
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<Record<TaskProfile, AgentTaskRoute>>(
|
|
||||||
mergeTaskDefaults({}, '', []),
|
|
||||||
);
|
|
||||||
const [remoteApiKey, setRemoteApiKey] = useState('');
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
|
||||||
const [localStatusMessage, setLocalStatusMessage] = useState<string | null>(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 (
|
|
||||||
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black/70 px-4">
|
|
||||||
<div className="max-h-[90vh] w-full max-w-4xl overflow-y-auto rounded-xl border border-[#2a2a2a] bg-[#0a0a0a] shadow-2xl">
|
|
||||||
<div className="flex items-center justify-between border-b border-[#2a2a2a] px-5 py-4">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0]">AI Settings</h2>
|
|
||||||
<p className="mt-1 text-xs font-mono text-[#888888]">
|
|
||||||
Configure remote and local providers, then assign a default provider/model per task.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="rounded px-2 py-1 text-xs font-mono text-[#888888] transition-colors hover:bg-[#1a1a1a] hover:text-[#e0e0e0]"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-5 px-5 py-5">
|
|
||||||
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xs font-mono uppercase tracking-wide text-[#888888]">
|
|
||||||
Runtime Status
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm font-mono text-[#e0e0e0]">
|
|
||||||
{status.configured ? 'Configured' : 'Routing incomplete'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right text-xs font-mono text-[#888888]">
|
|
||||||
<div>Remote ready: {status.remoteConfigured ? 'yes' : 'no'}</div>
|
|
||||||
<div>Local ready: {status.localConfigured ? 'yes' : 'no'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xs font-mono uppercase tracking-wide text-[#888888]">
|
|
||||||
Remote Provider
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-xs font-mono text-[#888888]">
|
|
||||||
OpenAI-compatible HTTP endpoint.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<label className="flex items-center gap-2 text-xs font-mono text-[#e0e0e0]">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={remoteEnabled}
|
|
||||||
onChange={(event) => setRemoteEnabled(event.target.checked)}
|
|
||||||
/>
|
|
||||||
Enabled
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
<label className="block">
|
|
||||||
<span className="mb-2 block text-xs font-mono text-[#888888]">
|
|
||||||
Remote Base URL
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className={inputClassName}
|
|
||||||
value={remoteBaseUrl}
|
|
||||||
onChange={(event) => setRemoteBaseUrl(event.target.value)}
|
|
||||||
placeholder="https://api.z.ai/api/coding/paas/v4"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="block">
|
|
||||||
<span className="mb-2 block text-xs font-mono text-[#888888]">
|
|
||||||
Default Remote Model
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className={inputClassName}
|
|
||||||
value={defaultRemoteModel}
|
|
||||||
onChange={(event) => handleDefaultRemoteModelChange(event.target.value)}
|
|
||||||
placeholder="glm-5.1"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xs font-mono uppercase tracking-wide text-[#888888]">
|
|
||||||
Local Provider
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-xs font-mono text-[#888888]">
|
|
||||||
Running Mistral HTTP sidecar on localhost.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<label className="flex items-center gap-2 text-xs font-mono text-[#e0e0e0]">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={localEnabled}
|
|
||||||
onChange={(event) => setLocalEnabled(event.target.checked)}
|
|
||||||
/>
|
|
||||||
Enabled
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]">
|
|
||||||
<label className="block">
|
|
||||||
<span className="mb-2 block text-xs font-mono text-[#888888]">
|
|
||||||
Local Base URL
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
className={inputClassName}
|
|
||||||
value={localBaseUrl}
|
|
||||||
onChange={(event) => setLocalBaseUrl(event.target.value)}
|
|
||||||
placeholder="http://127.0.0.1:1234"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="flex items-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCheckLocalHealth}
|
|
||||||
disabled={isBusy}
|
|
||||||
className={buttonClassName}
|
|
||||||
>
|
|
||||||
Check Local
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleDiscoverLocalModels}
|
|
||||||
disabled={isBusy}
|
|
||||||
className={buttonClassName}
|
|
||||||
>
|
|
||||||
Discover Models
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="mt-4 block">
|
|
||||||
<span className="mb-2 block text-xs font-mono text-[#888888]">
|
|
||||||
Available Local Models
|
|
||||||
</span>
|
|
||||||
<textarea
|
|
||||||
className={textareaClassName}
|
|
||||||
value={localAvailableModelsText}
|
|
||||||
onChange={(event) => setLocalAvailableModelsText(event.target.value)}
|
|
||||||
placeholder={'qwen2.5:3b-instruct\nmistral-small'}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{localStatusMessage ? (
|
|
||||||
<div className="mt-4 rounded border border-[#2a2a2a] bg-[#0d0d0d] px-3 py-2 text-xs font-mono text-[#9fb3c8]">
|
|
||||||
{localStatusMessage}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-xs font-mono uppercase tracking-wide text-[#888888]">
|
|
||||||
Task Routing
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-xs font-mono text-[#888888]">
|
|
||||||
Choose the default provider and model for each harness task.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{TASK_PROFILES.map((task) => (
|
|
||||||
<div
|
|
||||||
key={task}
|
|
||||||
className="grid gap-3 rounded border border-[#1d1d1d] bg-[#0d0d0d] p-3 md:grid-cols-[180px_140px_minmax(0,1fr)]"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-mono text-[#e0e0e0]">{TASK_LABELS[task]}</div>
|
|
||||||
<label className="block">
|
|
||||||
<span className="mb-2 block text-xs font-mono text-[#888888]">Provider</span>
|
|
||||||
<select
|
|
||||||
className={inputClassName}
|
|
||||||
value={taskDefaults[task].providerMode}
|
|
||||||
onChange={(event) =>
|
|
||||||
handleTaskProviderChange(task, event.target.value as ProviderMode)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="remote">remote</option>
|
|
||||||
<option value="local">local</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="block">
|
|
||||||
<span className="mb-2 block text-xs font-mono text-[#888888]">Model</span>
|
|
||||||
<input
|
|
||||||
list={taskDefaults[task].providerMode === 'local' ? `${task}-local-models` : undefined}
|
|
||||||
className={inputClassName}
|
|
||||||
value={taskDefaults[task].model}
|
|
||||||
onChange={(event) =>
|
|
||||||
setTaskRoute(task, (route) => ({ ...route, model: event.target.value }))
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
taskDefaults[task].providerMode === 'remote'
|
|
||||||
? defaultRemoteModel || 'Remote model'
|
|
||||||
: 'Local model'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{taskDefaults[task].providerMode === 'local' ? (
|
|
||||||
<datalist id={`${task}-local-models`}>
|
|
||||||
{localAvailableModels.map((model) => (
|
|
||||||
<option key={`${task}-${model}`} value={model} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
) : null}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSaveRuntime}
|
|
||||||
disabled={isBusy}
|
|
||||||
className={buttonClassName}
|
|
||||||
>
|
|
||||||
Save Routing
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4">
|
|
||||||
<h3 className="text-xs font-mono uppercase tracking-wide text-[#888888]">
|
|
||||||
Remote API Key
|
|
||||||
</h3>
|
|
||||||
<p className="mt-2 text-xs font-mono text-[#888888]">
|
|
||||||
Stored in plain text for the remote OpenAI-compatible provider only.
|
|
||||||
</p>
|
|
||||||
<label className="mt-4 block">
|
|
||||||
<span className="mb-2 block text-xs font-mono text-[#888888]">
|
|
||||||
{status.hasRemoteApiKey ? 'Replace Remote API Key' : 'Remote API Key'}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className={inputClassName}
|
|
||||||
value={remoteApiKey}
|
|
||||||
onChange={(event) => setRemoteApiKey(event.target.value)}
|
|
||||||
placeholder="Enter remote API key"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="mt-4 flex justify-between">
|
|
||||||
<div>
|
|
||||||
{status.hasRemoteApiKey ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClearRemoteApiKey}
|
|
||||||
disabled={isBusy}
|
|
||||||
className="rounded border border-[#2a2a2a] bg-[#151515] px-3 py-2 text-xs font-mono text-[#ff7b72] transition-colors hover:border-[#ff7b72] disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Clear Key
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSaveRemoteApiKey}
|
|
||||||
disabled={isBusy}
|
|
||||||
className={buttonClassName}
|
|
||||||
>
|
|
||||||
{status.hasRemoteApiKey ? 'Save Routing & Update Key' : 'Save Routing & Save Key'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{success ? (
|
|
||||||
<div className="rounded border border-[#214f31] bg-[#102417] px-3 py-2 text-xs font-mono text-[#9ee6b3]">
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{error ? (
|
|
||||||
<div className="rounded border border-[#5c2b2b] bg-[#211313] px-3 py-2 text-xs font-mono text-[#ffb4b4]">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
274
MosaicIQ/src/components/Settings/SettingsPage.tsx
Normal file
274
MosaicIQ/src/components/Settings/SettingsPage.tsx
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { AgentConfigStatus } from '../../types/agentSettings';
|
||||||
|
import { AgentSettingsForm } from './AgentSettingsForm';
|
||||||
|
|
||||||
|
type SettingsSectionId = 'general' | 'ai' | 'workspace' | 'about';
|
||||||
|
|
||||||
|
interface SettingsPageProps {
|
||||||
|
status: AgentConfigStatus | null;
|
||||||
|
onRefreshStatus: () => Promise<AgentConfigStatus>;
|
||||||
|
onStatusChange: (status: AgentConfigStatus) => void;
|
||||||
|
onBackToTerminal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsSection {
|
||||||
|
id: SettingsSectionId;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections: SettingsSection[] = [
|
||||||
|
{
|
||||||
|
id: 'general',
|
||||||
|
label: 'General',
|
||||||
|
description: 'Configuration hub and runtime overview.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai',
|
||||||
|
label: 'AI & Models',
|
||||||
|
description: 'Remote provider, model routing, and credentials.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workspace',
|
||||||
|
label: 'Workspace',
|
||||||
|
description: 'Shell, tabs, and terminal behavior.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'about',
|
||||||
|
label: 'About',
|
||||||
|
description: 'Product details and settings conventions.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusBadgeClassName = (active: boolean) =>
|
||||||
|
active
|
||||||
|
? 'border-[#1d4c7d] bg-[#0f1f31] text-[#8dc3ff]'
|
||||||
|
: 'border-[#3d3420] bg-[#1d170c] text-[#e7bb62]';
|
||||||
|
|
||||||
|
export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||||
|
status,
|
||||||
|
onRefreshStatus,
|
||||||
|
onStatusChange,
|
||||||
|
onBackToTerminal,
|
||||||
|
}) => {
|
||||||
|
const [activeSection, setActiveSection] = useState<SettingsSectionId>('general');
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const [refreshError, setRefreshError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const statusSummary = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: 'Settings health',
|
||||||
|
value: status?.configured ? 'Ready' : 'Needs attention',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remote provider',
|
||||||
|
value: status?.remoteEnabled ? 'Enabled' : 'Disabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'API key',
|
||||||
|
value: status?.hasRemoteApiKey ? 'Stored' : 'Missing',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[status],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
setRefreshError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nextStatus = await onRefreshStatus();
|
||||||
|
onStatusChange(nextStatus);
|
||||||
|
} catch (error) {
|
||||||
|
setRefreshError(error instanceof Error ? error.message : 'Failed to refresh settings status.');
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (activeSection === 'general') {
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-5">
|
||||||
|
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0]">
|
||||||
|
Settings are centralized here
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 max-w-2xl text-xs font-mono leading-6 text-[#888888]">
|
||||||
|
This page is now the dedicated control surface for MosaicIQ configuration. New
|
||||||
|
settings categories should be added as submenu entries here instead of modal dialogs
|
||||||
|
or one-off controls elsewhere in the shell.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-4 lg:grid-cols-3">
|
||||||
|
{statusSummary.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.label}
|
||||||
|
className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-4"
|
||||||
|
>
|
||||||
|
<div className="text-[11px] font-mono text-[#888888]">{item.label}</div>
|
||||||
|
<div className="mt-2 text-sm font-mono text-[#e0e0e0]">{item.value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-5">
|
||||||
|
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0]">
|
||||||
|
Settings roadmap
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 grid gap-3 md:grid-cols-2">
|
||||||
|
<div className="rounded border border-[#1d1d1d] bg-[#0d0d0d] p-4">
|
||||||
|
<div className="text-xs font-mono text-[#e0e0e0]">AI & Models</div>
|
||||||
|
<p className="mt-2 text-xs font-mono leading-6 text-[#888888]">
|
||||||
|
Active now. Provider routing, default models, and credential storage are managed
|
||||||
|
in this section.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded border border-[#1d1d1d] bg-[#0d0d0d] p-4">
|
||||||
|
<div className="text-xs font-mono text-[#e0e0e0]">Workspace</div>
|
||||||
|
<p className="mt-2 text-xs font-mono leading-6 text-[#888888]">
|
||||||
|
Reserved for terminal defaults, tab naming conventions, and shell preferences as
|
||||||
|
those controls are added.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSection === 'ai') {
|
||||||
|
return <AgentSettingsForm status={status} onStatusChange={onStatusChange} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSection === 'workspace') {
|
||||||
|
return (
|
||||||
|
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-5">
|
||||||
|
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0]">Workspace settings</h2>
|
||||||
|
<p className="mt-2 max-w-2xl text-xs font-mono leading-6 text-[#888888]">
|
||||||
|
This submenu is in place for shell-wide controls such as sidebar defaults, startup
|
||||||
|
behavior, and terminal preferences. Future workspace configuration should be added here
|
||||||
|
rather than inside the terminal view.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 grid gap-3 md:grid-cols-2">
|
||||||
|
<div className="rounded border border-[#1d1d1d] bg-[#0d0d0d] p-4">
|
||||||
|
<div className="text-xs font-mono text-[#e0e0e0]">Planned controls</div>
|
||||||
|
<p className="mt-2 text-xs font-mono leading-6 text-[#888888]">
|
||||||
|
Default workspace names, sidebar visibility at launch, terminal input behavior, and
|
||||||
|
session retention rules.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded border border-[#1d1d1d] bg-[#0d0d0d] p-4">
|
||||||
|
<div className="text-xs font-mono text-[#e0e0e0]">Implementation note</div>
|
||||||
|
<p className="mt-2 text-xs font-mono leading-6 text-[#888888]">
|
||||||
|
Keep future settings grouped by submenu and avoid reintroducing feature-specific
|
||||||
|
controls in headers, toolbars, or modal overlays.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-lg border border-[#2a2a2a] bg-[#111111] p-5">
|
||||||
|
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0]">About this settings page</h2>
|
||||||
|
<div className="mt-4 space-y-3 text-xs font-mono leading-6 text-[#888888]">
|
||||||
|
<p>
|
||||||
|
The settings page is designed as a durable home for configuration instead of scattering
|
||||||
|
controls across the product shell.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Each submenu is a stable category. Future settings work should add to these categories
|
||||||
|
or introduce a new submenu here when the information architecture truly expands.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Current shell shortcut: use the bottom-left cog or press <kbd className="rounded border border-[#2a2a2a] bg-[#0d0d0d] px-1.5 py-0.5 text-[11px] text-[#e0e0e0]">Cmd+,</kbd> to open settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col bg-[#0a0a0a]">
|
||||||
|
<div className="flex items-center justify-between border-b border-[#2a2a2a] px-6 py-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-base font-mono font-semibold text-[#e0e0e0]">Settings</h1>
|
||||||
|
<p className="mt-1 text-xs font-mono text-[#888888]">
|
||||||
|
Centralized configuration for MosaicIQ.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
className="rounded border border-[#2a2a2a] bg-[#111111] 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"
|
||||||
|
>
|
||||||
|
{isRefreshing ? 'Refreshing...' : 'Refresh status'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onBackToTerminal}
|
||||||
|
className="rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-xs font-mono text-[#e0e0e0] transition-colors hover:border-[#58a6ff] hover:text-[#58a6ff]"
|
||||||
|
>
|
||||||
|
Back to terminal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||||
|
<aside className="border-b border-[#2a2a2a] bg-[#0d0d0d] lg:w-[260px] lg:border-b-0 lg:border-r">
|
||||||
|
<nav className="flex gap-2 overflow-x-auto px-4 py-4 lg:flex-col lg:gap-1 lg:overflow-visible">
|
||||||
|
{sections.map((section) => {
|
||||||
|
const isActive = activeSection === section.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={section.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveSection(section.id)}
|
||||||
|
className={`min-w-[180px] rounded border px-3 py-3 text-left transition-colors lg:min-w-0 ${
|
||||||
|
isActive
|
||||||
|
? 'border-[#3a3a3a] bg-[#111111] text-[#e0e0e0]'
|
||||||
|
: 'border-transparent bg-transparent text-[#888888] hover:border-[#2a2a2a] hover:bg-[#111111] hover:text-[#e0e0e0]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-xs font-mono">{section.label}</div>
|
||||||
|
<div className="mt-1 text-[11px] font-mono leading-5 text-[#666666]">
|
||||||
|
{section.description}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="min-h-0 flex-1 overflow-y-auto px-4 py-4 lg:px-6 lg:py-5">
|
||||||
|
<div className="mb-5 flex flex-wrap items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={`rounded border px-2.5 py-1 text-[11px] font-mono ${statusBadgeClassName(
|
||||||
|
Boolean(status?.configured),
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{status?.configured ? 'Configured' : 'Needs configuration'}
|
||||||
|
</div>
|
||||||
|
<div className="rounded border border-[#2a2a2a] bg-[#111111] px-2.5 py-1 text-[11px] font-mono text-[#888888]">
|
||||||
|
Active section: {sections.find((section) => section.id === activeSection)?.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{refreshError ? (
|
||||||
|
<div className="mb-5 rounded border border-[#5c2b2b] bg-[#211313] px-3 py-2 text-xs font-mono text-[#ffb4b4]">
|
||||||
|
{refreshError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{renderContent()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,13 +5,21 @@ import { useMockData } from '../../hooks/useMockData';
|
|||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
onCommand: (command: string) => void;
|
onCommand: (command: string) => void;
|
||||||
|
onOpenSettings: () => void;
|
||||||
|
isSettingsActive: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SidebarState = 'closed' | 'minimized' | 'open';
|
type SidebarState = 'closed' | 'minimized' | 'open';
|
||||||
|
|
||||||
export const Sidebar: React.FC<SidebarProps> = ({ onCommand, isOpen, onToggle }) => {
|
export const Sidebar: React.FC<SidebarProps> = ({
|
||||||
|
onCommand,
|
||||||
|
onOpenSettings,
|
||||||
|
isSettingsActive,
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
|
}) => {
|
||||||
const { getAllCompanies, getPortfolio } = useMockData();
|
const { getAllCompanies, getPortfolio } = useMockData();
|
||||||
const companies = getAllCompanies();
|
const companies = getAllCompanies();
|
||||||
const portfolio = getPortfolio();
|
const portfolio = getPortfolio();
|
||||||
@@ -96,6 +104,31 @@ export const Sidebar: React.FC<SidebarProps> = ({ onCommand, isOpen, onToggle })
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-[#2a2a2a] p-2">
|
||||||
|
<button
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
className={`group relative flex w-full items-center justify-center rounded border px-2 py-3 transition-colors ${
|
||||||
|
isSettingsActive
|
||||||
|
? 'border-[#58a6ff] bg-[#111827] text-[#58a6ff]'
|
||||||
|
: 'border-[#2a2a2a] bg-[#111111] text-[#888888] hover:text-[#e0e0e0]'
|
||||||
|
}`}
|
||||||
|
title="Open settings"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.8}
|
||||||
|
d="M10.325 4.317a1 1 0 011.35-.936l.35.14a1 1 0 00.95 0l.35-.14a1 1 0 011.35.936l.05.373a1 1 0 00.62.79l.347.143a1 1 0 01.527 1.282l-.145.346a1 1 0 000 .95l.145.347a1 1 0 01-.527 1.282l-.347.143a1 1 0 00-.62.79l-.05.373a1 1 0 01-1.35.936l-.35-.14a1 1 0 00-.95 0l-.35.14a1 1 0 01-1.35-.936l-.05-.373a1 1 0 00-.62-.79l-.347-.143a1 1 0 01-.527-1.282l.145-.347a1 1 0 000-.95l-.145-.346a1 1 0 01.527-1.282l.347-.143a1 1 0 00.62-.79l.05-.373z"
|
||||||
|
/>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="absolute left-full ml-2 px-2 py-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded text-xs text-[#e0e0e0] opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-20 pointer-events-none">
|
||||||
|
Settings
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -133,8 +166,31 @@ export const Sidebar: React.FC<SidebarProps> = ({ onCommand, isOpen, onToggle })
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="p-4 border-t border-[#2a2a2a]">
|
<div className="p-4 border-t border-[#2a2a2a]">
|
||||||
<div className="text-[10px] text-[#888888] font-mono text-center">
|
<div className="flex items-center justify-between gap-3">
|
||||||
Press <kbd className="px-1.5 py-0.5 bg-[#1a1a1a] rounded text-[#e0e0e0]">Cmd+B</kbd> to toggle
|
<button
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
className={`flex items-center gap-2 rounded border px-3 py-2 text-xs font-mono transition-colors ${
|
||||||
|
isSettingsActive
|
||||||
|
? 'border-[#58a6ff] bg-[#111827] text-[#58a6ff]'
|
||||||
|
: 'border-[#2a2a2a] bg-[#111111] text-[#888888] hover:border-[#58a6ff] hover:text-[#e0e0e0]'
|
||||||
|
}`}
|
||||||
|
title="Open settings"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.8}
|
||||||
|
d="M10.325 4.317a1 1 0 011.35-.936l.35.14a1 1 0 00.95 0l.35-.14a1 1 0 011.35.936l.05.373a1 1 0 00.62.79l.347.143a1 1 0 01.527 1.282l-.145.346a1 1 0 000 .95l.145.347a1 1 0 01-.527 1.282l-.347.143a1 1 0 00-.62.79l-.05.373a1 1 0 01-1.35.936l-.35-.14a1 1 0 00-.95 0l-.35.14a1 1 0 01-1.35-.936l-.05-.373a1 1 0 00-.62-.79l-.347-.143a1 1 0 01-.527-1.282l.145-.347a1 1 0 000-.95l-.145-.346a1 1 0 01.527-1.282l.347-.143a1 1 0 00.62-.79l.05-.373z"
|
||||||
|
/>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>Settings</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-[10px] text-[#888888] font-mono text-right">
|
||||||
|
Press <kbd className="px-1.5 py-0.5 bg-[#1a1a1a] rounded text-[#e0e0e0]">Cmd+B</kbd> to toggle
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ interface TabBarProps {
|
|||||||
onTabClick: (id: string) => void;
|
onTabClick: (id: string) => void;
|
||||||
onTabClose: (id: string) => void;
|
onTabClose: (id: string) => void;
|
||||||
onNewTab: () => void;
|
onNewTab: () => void;
|
||||||
onOpenSettings: () => void;
|
|
||||||
isAgentReady?: boolean;
|
|
||||||
onTabRename?: (id: string, newName: string) => void;
|
onTabRename?: (id: string, newName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,8 +19,6 @@ export const TabBar: React.FC<TabBarProps> = ({
|
|||||||
onTabClick,
|
onTabClick,
|
||||||
onTabClose,
|
onTabClose,
|
||||||
onNewTab,
|
onNewTab,
|
||||||
onOpenSettings,
|
|
||||||
isAgentReady = false,
|
|
||||||
onTabRename
|
onTabRename
|
||||||
}) => {
|
}) => {
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
@@ -122,19 +118,6 @@ export const TabBar: React.FC<TabBarProps> = ({
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onOpenSettings}
|
|
||||||
className={`ml-1 flex items-center gap-1 rounded px-2 py-1 text-[10px] font-mono transition-colors ${
|
|
||||||
isAgentReady
|
|
||||||
? 'text-[#00d26a] hover:bg-[#102417]'
|
|
||||||
: 'text-[#ffb000] hover:bg-[#241b08]'
|
|
||||||
}`}
|
|
||||||
title="AI settings"
|
|
||||||
>
|
|
||||||
<span className={`inline-block h-2 w-2 rounded-full ${isAgentReady ? 'bg-[#00d26a]' : 'bg-[#ffb000]'}`} />
|
|
||||||
AI
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user