feat(settings): add SEC EDGAR configuration controls

This commit is contained in:
2026-04-05 21:11:23 -04:00
parent cfc5a615e3
commit d89a1ec84b
5 changed files with 423 additions and 142 deletions

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Settings,
Eye,
EyeOff,
Loader2,
@@ -10,8 +9,6 @@ import {
Save,
KeyRound,
Globe,
ShieldCheck,
ShieldAlert,
AlertCircle,
} from 'lucide-react';
import { agentSettingsBridge } from '../../lib/agentSettingsBridge';
@@ -37,6 +34,7 @@ interface FormState {
remoteBaseUrl: string;
defaultRemoteModel: string;
taskDefaults: Record<TaskProfile, AgentTaskRoute>;
secEdgarUserAgent: string;
remoteApiKey: string;
}
@@ -44,6 +42,8 @@ interface ValidationState {
baseUrl: ValidationStatus;
baseUrlError?: string;
defaultModel: ValidationStatus;
secEdgarUserAgent: ValidationStatus;
secEdgarUserAgentError?: string;
apiKey: ValidationStatus;
}
@@ -68,6 +68,24 @@ const validateUrl = (url: string): { valid: boolean; error?: string } => {
}
};
const validateSecEdgarUserAgent = (
value: string,
): { valid: boolean; error?: string } => {
const trimmed = value.trim();
if (!trimmed) {
return { valid: true };
}
if (!trimmed.includes(' ') || !trimmed.includes('@')) {
return {
valid: false,
error: 'Use a format like `MosaicIQ admin@example.com`',
};
}
return { valid: true };
};
export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
status,
onStatusChange,
@@ -77,12 +95,14 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
remoteBaseUrl: '',
defaultRemoteModel: '',
taskDefaults: mergeTaskDefaults({}, ''),
secEdgarUserAgent: '',
remoteApiKey: '',
});
const [initialState, setInitialState] = useState<FormState | null>(null);
const [validation, setValidation] = useState<ValidationState>({
baseUrl: 'idle',
defaultModel: 'idle',
secEdgarUserAgent: 'idle',
apiKey: 'idle',
});
const [showApiKey, setShowApiKey] = useState(false);
@@ -103,6 +123,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
remoteBaseUrl: status.remoteBaseUrl,
defaultRemoteModel: status.defaultRemoteModel,
taskDefaults: mergeTaskDefaults(status.taskDefaults, status.defaultRemoteModel),
secEdgarUserAgent: status.secEdgarUserAgent,
remoteApiKey: '',
};
@@ -114,6 +135,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
setValidation({
baseUrl: 'idle',
defaultModel: 'idle',
secEdgarUserAgent: 'idle',
apiKey: 'idle',
});
}, [status]);
@@ -127,7 +149,8 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
formState.remoteBaseUrl !== initialState.remoteBaseUrl ||
formState.defaultRemoteModel !== initialState.defaultRemoteModel ||
JSON.stringify(formState.taskDefaults) !== JSON.stringify(initialState.taskDefaults) ||
(formState.remoteApiKey && formState.remoteApiKey.length > 0);
formState.secEdgarUserAgent !== initialState.secEdgarUserAgent ||
Boolean(formState.remoteApiKey && formState.remoteApiKey.length > 0);
setHasUnsavedChanges(hasChanges);
}, [formState, initialState]);
@@ -155,6 +178,23 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
}
}, [formState.defaultRemoteModel]);
useEffect(() => {
if (formState.secEdgarUserAgent.trim()) {
const result = validateSecEdgarUserAgent(formState.secEdgarUserAgent);
setValidation((prev) => ({
...prev,
secEdgarUserAgent: result.valid ? 'valid' : 'invalid',
secEdgarUserAgentError: result.error,
}));
} else {
setValidation((prev) => ({
...prev,
secEdgarUserAgent: 'idle',
secEdgarUserAgentError: undefined,
}));
}
}, [formState.secEdgarUserAgent]);
// Validate API key
useEffect(() => {
if (formState.remoteApiKey) {
@@ -180,6 +220,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
remoteBaseUrl: formState.remoteBaseUrl,
defaultRemoteModel: formState.defaultRemoteModel,
taskDefaults: formState.taskDefaults,
secEdgarUserAgent: formState.secEdgarUserAgent,
};
const setTaskRoute = useCallback(
@@ -279,19 +320,24 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
const handleDefaultRemoteModelChange = (nextValue: string) => {
const previousValue = formState.defaultRemoteModel;
setFormState((prev) => ({ ...prev, defaultRemoteModel: nextValue }));
setTaskDefaults((current) => {
const next = { ...current };
setFormState((current) => {
const nextTaskDefaults = { ...current.taskDefaults };
for (const profile of TASK_PROFILES) {
if (next[profile].model.trim() === previousValue.trim()) {
next[profile] = { model: nextValue };
if (nextTaskDefaults[profile].model.trim() === previousValue.trim()) {
nextTaskDefaults[profile] = { model: nextValue };
}
}
return next;
return {
...current,
defaultRemoteModel: nextValue,
taskDefaults: nextTaskDefaults,
};
});
};
const isFormValid = validation.baseUrl === 'valid' || validation.baseUrl === 'idle';
const isFormValid =
(validation.baseUrl === 'valid' || validation.baseUrl === 'idle') &&
(validation.secEdgarUserAgent === 'valid' || validation.secEdgarUserAgent === 'idle');
return (
<div className="space-y-6">
@@ -501,7 +547,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
validationStatus={validation.apiKey}
helperText={
status.hasRemoteApiKey && !formState.remoteApiKey
? `Current key ending in ••••${status.remoteApiKey?.slice(-4) || '****'}`
? 'A remote API key is currently stored.'
: 'Required for API requests'
}
disabled={isBusy}

View File

@@ -8,6 +8,7 @@ export interface ModelOption {
}
export interface ModelSelectorProps {
id?: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
@@ -29,6 +30,7 @@ const DEFAULT_MODEL_OPTIONS: ModelOption[] = [
];
export const ModelSelector: React.FC<ModelSelectorProps> = ({
id,
value,
onChange,
placeholder = 'Select or enter a model',
@@ -97,15 +99,16 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
>
{!isCustomMode ? (
<button
id={id}
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
className="w-full rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-left text-sm font-mono text-[#e0e0e0] outline-none transition-colors hover:border-[#3a3a3a] focus:border-[#58a6ff] disabled:cursor-not-allowed disabled:opacity-50"
className="w-full border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-left text-sm font-mono text-[#e0e0e0] outline-none transition-all hover:border-[#3a3a3a] focus:border-[#58a6ff] disabled:cursor-not-allowed disabled:opacity-50 border-l-2 focus:border-l-[#58a6ff] hover:border-l-[#3a3a3a]"
aria-haspopup="listbox"
aria-expanded={isOpen}
>
<div className="flex items-center justify-between">
<span>
<div className="flex items-center justify-between gap-3">
<span className="flex-1 min-w-0 truncate">
{selectedOption ? (
<span>
{selectedOption.label}
@@ -117,13 +120,14 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<span className="text-[#666666]">{placeholder}</span>
)}
</span>
<span className="text-[#666666]" aria-hidden="true">
<span className="text-[#666666] flex-shrink-0" aria-hidden="true">
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</span>
</div>
</button>
) : (
<input
id={id}
ref={inputRef}
type="text"
value={value}
@@ -135,29 +139,33 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}}
onKeyDown={handleKeyDown}
placeholder="Enter custom model name"
className="w-full rounded border border-[#58a6ff] bg-[#111111] px-3 py-2 text-sm font-mono text-[#e0e0e0] outline-none focus:ring-2 focus:ring-[#58a6ff] focus:ring-offset-2 focus:ring-offset-[#111111]"
className="w-full border border-[#58a6ff] bg-[#111111] px-3 py-2 text-sm font-mono text-[#e0e0e0] outline-none focus:border-l-[#58a6ff] border-l-2 transition-all"
/>
)}
{isOpen && !isCustomMode && (
<div className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-[#2a2a2a] bg-[#111111] shadow-lg">
<div className="border-b border-[#2a2a2a] p-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#666666]" />
<div className="absolute z-10 mt-1 max-h-60 w-full overflow-auto border border-[#2a2a2a] bg-[#111111] shadow-lg">
<div className="border-b border-[#1a1a1a] p-3">
<div className="relative group">
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[#666666] group-focus-within:text-[#58a6ff] transition-colors" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search models..."
className="w-full rounded border border-[#2a2a2a] bg-[#0d0d0d] py-2 pl-9 pr-3 text-sm font-mono text-[#e0e0e0] outline-none focus:border-[#58a6ff]"
className="w-full bg-transparent py-1.5 pl-8 pr-2 text-sm font-mono text-[#e0e0e0] outline-none border-l-2 border-[#1a1a1a] focus:border-l-[#58a6ff] transition-colors"
autoFocus
/>
</div>
</div>
<ul role="listbox" className="py-1">
{filteredOptions.length === 0 ? (
<li className="px-3 py-2 text-sm font-mono text-[#888888]">
No models found
<li className="px-3 py-4 text-center">
<div className="flex items-center justify-center gap-2 text-xs font-mono text-[#666666]">
<div className="h-px w-8 bg-[#2a2a2a]" />
<span>No models found</span>
<div className="h-px w-8 bg-[#2a2a2a]" />
</div>
</li>
) : (
filteredOptions.map((option) => (
@@ -166,16 +174,16 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
role="option"
aria-selected={value === option.value}
onClick={() => handleSelect(option.value)}
className={`cursor-pointer px-3 py-2 text-sm font-mono transition-colors ${
className={`cursor-pointer px-3 py-2 text-sm font-mono transition-colors border-l-2 ${
value === option.value
? 'bg-[#1d4c7d] text-[#8dc3ff]'
: 'text-[#e0e0e0] hover:bg-[#1d1d1d]'
? 'border-l-[#58a6ff] bg-[#1d4c7d]/30 text-[#8dc3ff]'
: 'border-l-transparent text-[#e0e0e0] hover:border-l-[#2a2a2a] hover:bg-[#1a1a1a]'
}`}
>
<div className="flex items-center justify-between">
<span>{option.label}</span>
<span className="truncate">{option.label}</span>
{option.provider && (
<span className="text-xs text-[#666666]">{option.provider}</span>
<span className="text-xs text-[#666666] ml-2">{option.provider}</span>
)}
</div>
</li>
@@ -183,13 +191,13 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
)}
{allowCustom && (
<>
<li className="my-1 border-t border-[#2a2a2a]" role="separator" />
<li className="my-1 border-t border-[#1a1a1a]" role="separator" />
<li
role="option"
onClick={handleCustomMode}
className="cursor-pointer px-3 py-2 text-sm font-mono text-[#888888] transition-colors hover:bg-[#1d1d1d] hover:text-[#e0e0e0]"
className="cursor-pointer px-3 py-2 text-sm font-mono text-[#888888] transition-colors hover:text-[#e0e0e0] hover:bg-[#1a1a1a]"
>
Custom model...
+ Custom model...
</li>
</>
)}

View File

@@ -0,0 +1,208 @@
import React, { useEffect, useMemo, useState } from 'react';
import { CheckCircle, Globe, Loader2, Save, XCircle } from 'lucide-react';
import { agentSettingsBridge } from '../../lib/agentSettingsBridge';
import { AgentConfigStatus } from '../../types/agentSettings';
import { ValidatedInput, ValidationStatus } from './ValidatedInput';
import { HelpIcon } from './Tooltip';
interface SecEdgarSettingsCardProps {
status: AgentConfigStatus | null;
onStatusChange: (status: AgentConfigStatus) => void;
}
const validateSecEdgarUserAgent = (
value: string,
): { valid: boolean; error?: string } => {
const trimmed = value.trim();
if (!trimmed) {
return { valid: true };
}
if (!trimmed.includes(' ') || !trimmed.includes('@')) {
return {
valid: false,
error: 'Use a format like `MosaicIQ admin@example.com`',
};
}
return { valid: true };
};
export const SecEdgarSettingsCard: React.FC<SecEdgarSettingsCardProps> = ({
status,
onStatusChange,
}) => {
const [value, setValue] = useState('');
const [initialValue, setInitialValue] = useState('');
const [validation, setValidation] = useState<{
status: ValidationStatus;
error?: string;
}>({
status: 'idle',
});
const [isBusy, setIsBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => {
if (!status) {
return;
}
setValue(status.secEdgarUserAgent);
setInitialValue(status.secEdgarUserAgent);
setValidation({ status: 'idle' });
setError(null);
setSuccess(null);
}, [status]);
useEffect(() => {
if (!value.trim()) {
setValidation({ status: 'idle' });
return;
}
const result = validateSecEdgarUserAgent(value);
setValidation({
status: result.valid ? 'valid' : 'invalid',
error: result.error,
});
}, [value]);
const hasChanges = value !== initialValue;
const isValid = validation.status === 'valid' || validation.status === 'idle';
const helperText = useMemo(() => {
if (status?.hasSecEdgarUserAgent) {
return 'Stored in app settings and used before env fallback.';
}
return 'Leave blank to fall back to the `SEC_EDGAR_USER_AGENT` environment variable.';
}, [status]);
const handleSave = async () => {
if (!status || !isValid) {
return;
}
setIsBusy(true);
setError(null);
setSuccess(null);
try {
const nextStatus = await agentSettingsBridge.saveRuntimeConfig({
remoteEnabled: status.remoteEnabled,
remoteBaseUrl: status.remoteBaseUrl,
defaultRemoteModel: status.defaultRemoteModel,
taskDefaults: status.taskDefaults,
secEdgarUserAgent: value,
});
onStatusChange(nextStatus);
setInitialValue(value);
setSuccess('SEC EDGAR setting saved successfully');
} catch (saveError) {
setError(
saveError instanceof Error
? saveError.message
: 'Failed to save SEC EDGAR setting.',
);
} finally {
setIsBusy(false);
}
};
return (
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
<div className="mb-6 flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2">
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">
SEC EDGAR
</h2>
<HelpIcon tooltip="Identity string sent with SEC EDGAR requests. The SEC asks automated clients to provide a descriptive product name and contact email." />
</div>
<p className="mt-1.5 text-xs font-mono leading-6 text-[#888888]">
Configure how MosaicIQ identifies itself when calling SEC EDGAR.
</p>
</div>
<div className="rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-right">
<div className="flex items-center gap-2 text-[11px] font-mono text-[#888888]">
<Globe className="h-3.5 w-3.5" />
<span>{status?.hasSecEdgarUserAgent ? 'Stored in app' : 'Env fallback / unset'}</span>
</div>
</div>
</div>
<ValidatedInput
label="SEC EDGAR User Agent"
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder="MosaicIQ admin@example.com"
validationStatus={validation.status}
errorMessage={validation.error}
helperText={helperText}
disabled={isBusy}
/>
<div className="mt-5 flex justify-end gap-3">
{hasChanges && (
<button
type="button"
onClick={() => setValue(initialValue)}
className="rounded border border-[#2a2a2a] bg-[#151515] px-4 py-2 text-xs font-mono text-[#888888] transition-colors hover:border-[#3a3a3a] hover:text-[#e0e0e0]"
disabled={isBusy}
>
Discard Changes
</button>
)}
<button
type="button"
onClick={handleSave}
disabled={isBusy || !hasChanges || !isValid || !status}
className="flex items-center gap-2 rounded border border-[#58a6ff] bg-[#0f1f31] px-4 py-2 text-xs font-mono text-[#58a6ff] transition-colors hover:bg-[#1a2d3d] disabled:cursor-not-allowed disabled:opacity-50"
>
{isBusy ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Saving...
</>
) : (
<>
<Save className="h-3.5 w-3.5" />
Save SEC Setting
</>
)}
</button>
</div>
{success && (
<div className="mt-4 flex items-center gap-3 rounded-lg border border-[#214f31] bg-[#102417] px-4 py-3 text-sm font-mono text-[#9ee6b3]">
<CheckCircle className="h-5 w-5" />
<div className="flex-1">{success}</div>
<button
type="button"
onClick={() => setSuccess(null)}
className="text-[#9ee6b3] opacity-60 transition-opacity hover:opacity-100"
aria-label="Dismiss success message"
>
<XCircle className="h-4 w-4" />
</button>
</div>
)}
{error && (
<div className="mt-4 flex items-center gap-3 rounded-lg border border-[#5c2b2b] bg-[#211313] px-4 py-3 text-sm font-mono text-[#ffb4b4]">
<XCircle className="h-5 w-5" />
<div className="flex-1">{error}</div>
<button
type="button"
onClick={() => setError(null)}
className="text-[#ffb4b4] opacity-60 transition-opacity hover:opacity-100"
aria-label="Dismiss error message"
>
<XCircle className="h-4 w-4" />
</button>
</div>
)}
</section>
);
};

View File

@@ -9,7 +9,6 @@ import {
X,
XCircle,
Loader2,
Globe,
Wifi,
WifiOff,
KeyRound,
@@ -26,6 +25,7 @@ import {
} from 'lucide-react';
import { AgentConfigStatus } from '../../types/agentSettings';
import { AgentSettingsForm } from './AgentSettingsForm';
import { SecEdgarSettingsCard } from './SecEdgarSettingsCard';
import { ToastContainer, type Toast, type ToastType } from './Toast';
type SettingsSectionId = 'general' | 'ai' | 'workspace' | 'about';
@@ -154,6 +154,12 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement | null;
const isEditingField =
activeElement?.tagName === 'INPUT' ||
activeElement?.tagName === 'TEXTAREA' ||
activeElement?.isContentEditable === true;
// Cmd/Ctrl + S to save (prevent default)
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
@@ -170,12 +176,13 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
document.getElementById('settings-search')?.focus();
}
if (isEditingField) {
return;
}
// ? to show shortcuts
if (e.key === '?' && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
const activeElement = document.activeElement;
if (activeElement?.tagName !== 'INPUT' && activeElement?.tagName !== 'TEXTAREA') {
setShowKeyboardShortcuts((prev) => !prev);
}
setShowKeyboardShortcuts((prev) => !prev);
}
// Escape to close shortcuts modal
@@ -199,66 +206,70 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
const renderContent = () => {
if (activeSection === 'general') {
return (
<div className="space-y-6">
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">
<div className="space-y-8">
<section>
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0] mb-3">
Settings are centralized here
</h2>
<p className="mt-3 max-w-2xl text-sm font-mono leading-7 text-[#888888]">
<p className="max-w-2xl text-sm font-mono leading-7 text-[#888888]">
This page is 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-[#1a1a1a] bg-[#0f0f0f] p-5 transition-shadow hover:shadow-md"
>
<div className="mb-2 flex items-center gap-2">
<span className="text-[#888888]" aria-hidden="true">
{item.icon}
</span>
<span className="text-xs font-mono text-[#888888]">{item.label}</span>
<section>
<div className="flex items-center gap-3 border-b border-[#1a1a1a] pb-3 mb-4">
<div className="h-px flex-1 bg-[#1a1a1a]" />
<span className="text-xs font-mono text-[#666666] uppercase tracking-wider">Status Overview</span>
<div className="h-px flex-1 bg-[#1a1a1a]" />
</div>
<div className="grid gap-6 sm:grid-cols-3">
{statusSummary.map((item) => (
<div key={item.label} className="space-y-1">
<div className="flex items-center gap-2 text-[#666666]">
<span className="text-sm">{item.icon}</span>
<span className="text-xs font-mono uppercase tracking-wide">{item.label}</span>
</div>
<div className="text-sm font-mono text-[#e0e0e0]">{item.value}</div>
</div>
<div className="text-sm font-mono text-[#e0e0e0]">{item.value}</div>
</div>
))}
))}
</div>
</section>
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">
<section>
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0] mb-4">
Settings roadmap
</h2>
<div className="mt-5 grid gap-4 md:grid-cols-2">
<div className="rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
<div className="mb-2 flex items-center gap-2">
<span className="text-[#888888]" aria-hidden="true">
<Bot className="h-5 w-5" />
</span>
<span className="text-sm font-mono text-[#e0e0e0]">AI & Models</span>
<div className="space-y-4">
<div className="flex gap-4 p-4 border-l-2 border-[#1a1a1a] hover:border-[#58a6ff] transition-colors">
<span className="text-[#666666] mt-0.5">
<Bot className="h-5 w-5" />
</span>
<div className="flex-1">
<h3 className="text-sm font-mono text-[#e0e0e0] mb-1">AI & Models</h3>
<p className="text-sm font-mono leading-6 text-[#888888]">
Active now. Provider routing, default models, and credential storage are managed
in this section.
</p>
</div>
<p className="text-sm font-mono leading-7 text-[#888888]">
Active now. Provider routing, default models, and credential storage are managed
in this section.
</p>
</div>
<div className="rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
<div className="mb-2 flex items-center gap-2">
<span className="text-[#888888]" aria-hidden="true">
<FolderOpen className="h-5 w-5" />
</span>
<span className="text-sm font-mono text-[#e0e0e0]">Workspace</span>
<div className="flex gap-4 p-4 border-l-2 border-[#1a1a1a] hover:border-[#58a6ff] transition-colors">
<span className="text-[#666666] mt-0.5">
<FolderOpen className="h-5 w-5" />
</span>
<div className="flex-1">
<h3 className="text-sm font-mono text-[#e0e0e0] mb-1">Workspace</h3>
<p className="text-sm font-mono leading-6 text-[#888888]">
Reserved for terminal defaults, tab naming conventions, and shell preferences as
those controls are added.
</p>
</div>
<p className="text-sm font-mono leading-7 text-[#888888]">
Reserved for terminal defaults, tab naming conventions, and shell preferences as
those controls are added.
</p>
</div>
</div>
</section>
<SecEdgarSettingsCard status={status} onStatusChange={onStatusChange} />
</div>
);
}
@@ -269,41 +280,43 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
if (activeSection === 'workspace') {
return (
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">Workspace settings</h2>
<p className="mt-3 max-w-2xl text-sm font-mono leading-7 text-[#888888]">
<section>
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0] mb-3">Workspace settings</h2>
<p className="max-w-2xl text-sm font-mono leading-7 text-[#888888] mb-6">
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-6 grid gap-4 md:grid-cols-2">
<div className="rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
<div className="mb-2 flex items-center gap-2">
<span className="text-[#888888]" aria-hidden="true">
<Zap className="h-5 w-5" />
</span>
<span className="text-sm font-mono text-[#e0e0e0]">Planned controls</span>
<div className="space-y-4">
<div className="flex gap-4 p-4 border-l-2 border-[#1a1a1a] hover:border-[#58a6ff] transition-colors">
<span className="text-[#666666] mt-0.5">
<Zap className="h-5 w-5" />
</span>
<div className="flex-1">
<h3 className="text-sm font-mono text-[#e0e0e0] mb-1">Planned controls</h3>
<p className="text-sm font-mono leading-6 text-[#888888]">
Default workspace names, sidebar visibility at launch, terminal input behavior, and
session retention rules.
</p>
</div>
<p className="text-sm font-mono leading-7 text-[#888888]">
Default workspace names, sidebar visibility at launch, terminal input behavior, and
session retention rules.
</p>
</div>
<div className="rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
<div className="mb-2 flex items-center gap-2">
<span className="text-[#888888]" aria-hidden="true">
<ClipboardList className="h-5 w-5" />
</span>
<span className="text-sm font-mono text-[#e0e0e0]">Implementation note</span>
<div className="flex gap-4 p-4 border-l-2 border-[#1a1a1a] hover:border-[#58a6ff] transition-colors">
<span className="text-[#666666] mt-0.5">
<ClipboardList className="h-5 w-5" />
</span>
<div className="flex-1">
<h3 className="text-sm font-mono text-[#e0e0e0] mb-1">Implementation note</h3>
<p className="text-sm 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>
<p className="text-sm font-mono leading-7 text-[#888888]">
Keep future settings grouped by submenu and avoid reintroducing feature-specific
controls in headers, toolbars, or modal overlays.
</p>
</div>
</div>
<div className="mt-6 rounded-lg border border-[#3d3420] bg-[#1d170c] px-4 py-3">
<div className="mt-8 p-4 border-l-2 border-[#3d3420] bg-[#1d170c]/30">
<div className="flex items-center gap-3">
<Clock className="h-5 w-5 text-[#e7bb62]" aria-hidden="true" />
<div>
@@ -319,9 +332,9 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
}
return (
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">About this settings page</h2>
<div className="mt-4 space-y-4 text-sm font-mono leading-7 text-[#888888]">
<section>
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0] mb-3">About this settings page</h2>
<div className="space-y-4 text-sm font-mono leading-7 text-[#888888]">
<p>
The settings page is designed as a durable home for configuration instead of scattering
controls across the product shell.
@@ -332,7 +345,7 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
</p>
<p>
Current shell shortcut: use the bottom-left cog or press{' '}
<kbd className="rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-1 text-xs text-[#e0e0e0]">
<kbd className="mx-1 px-1.5 py-0.5 text-xs text-[#e0e0e0] bg-[#1a1a1a] rounded">
<Command className="inline h-3 w-3" /> + ,
</kbd>{' '}
to open settings.
@@ -340,39 +353,39 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
</div>
{/* Keyboard shortcuts help */}
<div className="mt-6 rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
<div className="mt-6 pt-6 border-t border-[#1a1a1a]">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-mono font-semibold text-[#e0e0e0]">Keyboard shortcuts</h3>
<button
type="button"
onClick={() => setShowKeyboardShortcuts(true)}
className="rounded border border-[#2a2a2a] bg-[#111111] px-3 py-1.5 text-xs font-mono text-[#888888] transition-colors hover:border-[#58a6ff] hover:text-[#58a6ff]"
className="px-3 py-1.5 text-xs font-mono text-[#888888] border border-[#2a2a2a] hover:border-[#58a6ff] hover:text-[#58a6ff] transition-colors rounded"
>
View all
</button>
</div>
<div className="grid gap-2 md:grid-cols-2">
<div className="grid gap-3 sm:grid-cols-2">
<div className="flex items-center justify-between text-xs font-mono">
<span className="text-[#888888]">Save settings</span>
<kbd className="flex items-center gap-1 rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-0.5 text-[#e0e0e0]">
<kbd className="flex items-center gap-1 px-2 py-0.5 text-[#e0e0e0] bg-[#1a1a1a] rounded">
<Command className="h-3 w-3" />S
</kbd>
</div>
<div className="flex items-center justify-between text-xs font-mono">
<span className="text-[#888888]">Search settings</span>
<kbd className="flex items-center gap-1 rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-0.5 text-[#e0e0e0]">
<kbd className="flex items-center gap-1 px-2 py-0.5 text-[#e0e0e0] bg-[#1a1a1a] rounded">
<Command className="h-3 w-3" />K
</kbd>
</div>
<div className="flex items-center justify-between text-xs font-mono">
<span className="text-[#888888]">Show shortcuts</span>
<kbd className="rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-0.5 text-[#e0e0e0]">
<kbd className="px-2 py-0.5 text-[#e0e0e0] bg-[#1a1a1a] rounded">
?
</kbd>
</div>
<div className="flex items-center justify-between text-xs font-mono">
<span className="text-[#888888]">Navigate sections</span>
<kbd className="rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-0.5 text-[#e0e0e0]">
<kbd className="px-2 py-0.5 text-[#e0e0e0] bg-[#1a1a1a] rounded">
1-4
</kbd>
</div>
@@ -436,26 +449,30 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
{/* Sidebar */}
<aside className="border-b border-[#1a1a1a] bg-[#0d0d0d] lg:w-[280px] lg:border-b-0 lg:border-r">
{/* Search */}
<div className="border-b border-[#1a1a1a] p-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#666666]" />
<div className="border-b border-[#1a1a1a] px-4 py-3">
<div className="relative group">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#666666] group-focus-within:text-[#58a6ff] transition-colors" />
<input
id="settings-search"
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search settings... (⌘K)"
className="w-full rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 pl-9 text-sm font-mono text-[#e0e0e0] outline-none transition-colors focus:border-[#58a6ff]"
className="w-full border-l-2 border-[#1a1a1a] bg-transparent px-3 py-2 pl-9 text-sm font-mono text-[#e0e0e0] outline-none transition-all focus:border-l-[#58a6ff] focus:bg-[#0a0a0a]"
/>
</div>
{searchQuery && filteredSections.length === 0 && (
<p className="mt-2 text-xs font-mono text-[#888888]">No settings found</p>
<div className="mt-3 flex items-center gap-2 px-1">
<div className="h-px flex-1 bg-[#2a2a2a]" />
<span className="text-xs font-mono text-[#666666]">No results</span>
<div className="h-px flex-1 bg-[#2a2a2a]" />
</div>
)}
</div>
{/* Navigation */}
<nav
className="flex gap-2 overflow-x-auto px-4 py-4 lg:flex-col lg:gap-1 lg:overflow-visible"
className="flex gap-1 overflow-x-auto px-4 py-3 lg:flex-col lg:gap-0.5 lg:overflow-visible"
role="navigation"
aria-label="Settings sections"
>
@@ -466,25 +483,23 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
key={section.id}
type="button"
onClick={() => setActiveSection(section.id)}
className={`min-w-[180px] rounded border px-4 py-3 text-left transition-all lg:min-w-0 ${
className={`min-w-[180px] flex items-center gap-3 px-3 py-2.5 text-left transition-all lg:min-w-0 ${
isActive
? 'border-[#3a3a3a] bg-[#111111] text-[#e0e0e0] shadow-sm'
: 'border-transparent bg-transparent text-[#888888] hover:border-[#2a2a2a] hover:bg-[#111111] hover:text-[#e0e0e0]'
? 'bg-[#1a1a1a] text-[#e0e0e0]'
: 'text-[#888888] hover:bg-[#111111] hover:text-[#e0e0e0]'
}`}
aria-current={isActive ? 'page' : undefined}
>
<div className="flex items-center gap-3">
<span className={isActive ? 'text-[#e0e0e0]' : 'text-[#888888]'}>
{section.icon}
</span>
<div className="flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-mono">{section.label}</span>
<span className="text-[10px] text-[#666666]">{index + 1}</span>
</div>
<div className="mt-1 text-[11px] font-mono leading-tight text-[#666666]">
{section.description}
</div>
<span className={`transition-colors ${isActive ? 'text-[#e0e0e0]' : 'text-[#666666]'}`}>
{section.icon}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-mono truncate">{section.label}</span>
<kbd className="text-[10px] text-[#444444] font-mono">{index + 1}</kbd>
</div>
<div className="mt-0.5 text-[11px] font-mono leading-tight truncate opacity-70">
{section.description}
</div>
</div>
</button>
@@ -509,9 +524,9 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
</nav>
{/* Status badges */}
<div className="mb-6 flex flex-wrap items-center gap-3">
<div className="mb-6 flex flex-wrap items-center gap-4">
<div
className={`flex items-center gap-2 rounded-md border px-3 py-1.5 text-[11px] font-mono ${statusBadgeClassName(
className={`flex items-center gap-2 px-3 py-1.5 text-[11px] font-mono border-l-2 ${statusBadgeClassName(
Boolean(status?.configured),
)}`}
>
@@ -527,19 +542,20 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
</>
)}
</div>
<div className="rounded-md border border-[#2a2a2a] bg-[#111111] px-3 py-1.5 text-[11px] font-mono text-[#888888]">
Active section: {currentSectionLabel}
<div className="flex items-center gap-2 px-3 py-1.5 text-[11px] font-mono text-[#888888] border-l-2 border-[#2a2a2a]">
<span className="w-1.5 h-1.5 rounded-full bg-[#666666]" />
<span>Active section: {currentSectionLabel}</span>
</div>
</div>
{/* Refresh error */}
{refreshError ? (
<div
className="mb-6 rounded-lg border border-[#5c2b2b] bg-[#211313] px-4 py-3 text-sm font-mono text-[#ffb4b4]"
className="mb-6 p-4 border-l-2 border-[#5c2b2b] bg-[#211313]/30 text-sm font-mono text-[#ffb4b4]"
role="alert"
>
<div className="flex items-center gap-3">
<XCircle className="h-5 w-5" aria-hidden="true" />
<div className="flex items-start gap-3">
<XCircle className="h-5 w-5 mt-0.5" aria-hidden="true" />
<div className="flex-1">{refreshError}</div>
<button
type="button"

View File

@@ -13,9 +13,11 @@ export interface AgentConfigStatus {
remoteConfigured: boolean;
remoteEnabled: boolean;
hasRemoteApiKey: boolean;
hasSecEdgarUserAgent: boolean;
remoteBaseUrl: string;
defaultRemoteModel: string;
taskDefaults: Record<TaskProfile, AgentTaskRoute>;
secEdgarUserAgent: string;
}
export interface SaveAgentRuntimeConfigRequest {
@@ -23,6 +25,7 @@ export interface SaveAgentRuntimeConfigRequest {
remoteBaseUrl: string;
defaultRemoteModel: string;
taskDefaults: Record<TaskProfile, AgentTaskRoute>;
secEdgarUserAgent: string;
}
export interface UpdateRemoteApiKeyRequest {