refactor(settings): split 73-line god file into 5 sections + auto-save preferences + guard Restore

The previous SettingsPane.tsx hid 8 unrelated concerns behind 73 "lines"
of JSX where each line ran 400-2000 chars. Reading the file was
infeasible; editing it meant scrolling horizontally through a single
statement spanning every button, modal, and table row.

Split by concern into `settings-sections/`:

  SettingsPane.tsx           62 LOC  orchestrator
  GeneralSection.tsx         94 LOC  theme / language / auto-update
  ExecutionModeSection.tsx  105 LOC  AI CEO ↔ AI Co-Founder toggle
  ShortcutsSection.tsx       40 LOC  keyboard reference (read-only)
  DataSection.tsx           123 LOC  update check / backup / restore /
                                     secret backend
  SecretsSection.tsx        102 LOC  per-company secret bindings

Every file now reads top-to-bottom with normal indentation.

Behavioral changes bundled (they belong with the restructure, not a
separate pass):

- Preferences save model unified: theme already auto-saved on change;
  language and auto-update checkbox now do the same. The global
  "Save settings" button is gone — there is no longer a state where
  a toggled language sits unpersisted waiting for a second click.
- Execution mode card gained a subheader line that names the current
  company ("Applies to current company: X"). Multi-company users can
  no longer confuse which company they just flipped.
- Auto-updater checkbox gained an inline hint: "Check for application
  updates at launch" — the previous naked checkbox never explained
  what happens when it is off.
- Restore profile moved into DataSection's backup/restore group,
  given `tone="danger"`, and gated behind a `window.confirm` with
  copy warning about running work being lost. Backup stays neutral.
  The previous layout put Restore in the same 2-col grid as Save +
  Check Updates + Backup, making a profile-replacing operation look
  as routine as saving a theme preference.
- Secret backend display promoted from a naked text line to a
  bordered card so it matches the surrounding section rhythm.

Added i18n keys (zh + en):
  settings.updaterHint / settings.modeScope / settings.backupRestore /
  settings.restoreConfirm / settings.restoreHint.

Covers J-S1 / J-S6 / S-S1 / S-S2 / S-S3 / S-S4 from the settings audit.
Remaining work (tab sidebar, Providers IA reordering, copy sweep)
lands in follow-up commits.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 21:06:07 -04:00
1 parent fd6471988a
commit 16e45b6a8d
8 files changed
+530 -67

No files matched your search

@@ -1,73 +1,62 @@
import { useEffect, useState } from "react"
import { ArrowClockwise, CheckCircle, ClockCounterClockwise, Download, Trash } from "../ui/icons"
import type { ProfileSnapshot, SecretRecord } from "@shared/types"
import type { AppLocale } from "@shared/locale"
import { APP_LOCALES, LOCALE_LABELS } from "@shared/locale"
import { useLocale, useT } from "../../i18n"
import { formatTime } from "../../lib/formatters"
import { unwrap } from "../../lib/desktop"
import { isMac } from "../../lib/platform"
import { ActionButton, ConfirmDialog, EmptyState, Eyebrow, InlineNotice, Select } from "../ui"
import { describeActionError } from "./execution-utils"
import { useAppStore } from "../../state/store"
import { useEffect, useState } from "react";
import type { ProfileSnapshot, SecretRecord } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { useLocale } from "../../i18n";
import { GeneralSection } from "./settings-sections/GeneralSection";
import { ExecutionModeSection } from "./settings-sections/ExecutionModeSection";
import { ShortcutsSection } from "./settings-sections/ShortcutsSection";
import { DataSection } from "./settings-sections/DataSection";
import { SecretsSection } from "./settings-sections/SecretsSection";
export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapshot: ProfileSnapshot; secrets: SecretRecord[]; companyId: string; onSaved: () => Promise<void> }) {
const t = useT(); const currentLocale = useLocale(); const addToast = useAppStore((s) => s.addToast)
const [theme, setTheme] = useState(snapshot.theme); const [selectedLocale, setSelectedLocale] = useState<AppLocale>(snapshot.locale ?? currentLocale); const [updatesEnabled, setUpdatesEnabled] = useState(snapshot.updatesEnabled)
const [confirmDeleteSecretId, setConfirmDeleteSecretId] = useState<string | null>(null); const [actionError, setActionError] = useState<string | null>(null); const [actionNotice, setActionNotice] = useState<string | null>(null)
const [deleteSecretError, setDeleteSecretError] = useState<string | null>(null); const [loadingAction, setLoadingAction] = useState<"save"|"check"|"backup"|"restore"|null>(null)
const secretToDelete = secrets.find((s) => s.id === confirmDeleteSecretId) ?? null
interface SettingsPaneProps {
snapshot: ProfileSnapshot;
secrets: SecretRecord[];
companyId: string;
onSaved: () => Promise<void>;
}
useEffect(() => { setTheme(snapshot.theme); setUpdatesEnabled(snapshot.updatesEnabled); if (snapshot.locale) setSelectedLocale(snapshot.locale) }, [snapshot.theme, snapshot.updatesEnabled, snapshot.locale])
async function handleDeleteSecret(id: string) { setDeleteSecretError(null); setActionError(null); setActionNotice(null); try { unwrap(await window.agentCompany.deleteSecret({ id, companyId })); setConfirmDeleteSecretId(null); setActionNotice(t("settings.secretDeleted")); addToast(t("settings.secretDeleted"), "success"); await onSaved() } catch (e) { setDeleteSecretError(describeActionError(e)) } }
async function handleSaveSettings() { setActionError(null); setActionNotice(null); setLoadingAction("save"); try { unwrap(await window.agentCompany.updateSettings({ theme, locale: selectedLocale, updatesEnabled })); await onSaved(); setActionNotice(t("settings.settingsSaved")); addToast(t("settings.settingsSaved"), "success") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleCheckForUpdates() { setActionError(null); setActionNotice(null); setLoadingAction("check"); try { const started = unwrap(await window.agentCompany.checkForUpdates()); setActionNotice(started ? t("settings.updateCheckStarted") : t("settings.updateCheckSkipped")) } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleBackupProfile() { setActionError(null); setActionNotice(null); setLoadingAction("backup"); try { const backupPath = unwrap(await window.agentCompany.backupProfile()); if (backupPath) { setActionNotice(t("settings.profileBackupAt").replace("{path}", backupPath)); addToast(t("settings.profileBackupCreated"), "success") } } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleRestoreProfile() { setActionError(null); setActionNotice(null); setLoadingAction("restore"); try { const restored = unwrap(await window.agentCompany.restoreProfile()); if (!restored) return; await onSaved(); setActionNotice(t("settings.profileRestored")); addToast(t("settings.profileRestored"), "success") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
/**
* Settings page orchestrator. Each concern lives in its own ≤200 LOC
* section under `settings-sections/` so the contents of this file stay
* scannable instead of hiding 2000-char JSX statements inside one
* 73-"line" god file.
*
* Save model: every preference in GeneralSection writes to
* `updateSettings` on change, so there is no global "Save" button.
* DataSection's backup / restore / update-check remain explicit
* actions because they have side effects beyond a single row of app
* state.
*/
export function SettingsPane({ snapshot, secrets, companyId, onSaved }: SettingsPaneProps) {
const currentLocale = useLocale();
const [theme, setTheme] = useState(snapshot.theme);
const [selectedLocale, setSelectedLocale] = useState<AppLocale>(snapshot.locale ?? currentLocale);
const [updatesEnabled, setUpdatesEnabled] = useState(snapshot.updatesEnabled);
useEffect(() => {
setTheme(snapshot.theme);
setUpdatesEnabled(snapshot.updatesEnabled);
if (snapshot.locale) setSelectedLocale(snapshot.locale);
}, [snapshot.theme, snapshot.updatesEnabled, snapshot.locale]);
const company = snapshot.companies.find((entry) => entry.id === companyId) ?? null;
return (
<div className="space-y-6">
<ConfirmDialog open={confirmDeleteSecretId !== null} title={t("settings.deleteSecretTitle")} message={t("settings.deleteSecretConfirm").replace("{name}", secretToDelete?.name ?? "")} errorMessage={deleteSecretError} onConfirm={() => confirmDeleteSecretId && void handleDeleteSecret(confirmDeleteSecretId)} onCancel={() => { setDeleteSecretError(null); setConfirmDeleteSecretId(null) }} />
{actionError ? <InlineNotice message={actionError} /> : null}
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
<div><Eyebrow className="mb-3">{t("exec.profile")}</Eyebrow><div className="border-y border-border py-4"><div className="mb-2 text-[11px] text-muted-foreground">{t("exec.profilePath")}</div><div className="font-mono truncate text-[11px] text-muted-foreground">{snapshot.profilePath}</div></div></div>
<div><Eyebrow className="mb-3">{t("exec.preferences")}</Eyebrow>
<Select label={t("settings.theme")} value={theme} onChange={(v) => { const next = v as ProfileSnapshot["theme"]; setTheme(next); const root = document.documentElement; root.className = next; root.style.colorScheme = next === "system" ? "light dark" : next; void window.agentCompany.updateSettings({ theme: next }).then(() => onSaved()) }} options={[{ value: "system", label: t("settings.system") }, { value: "dark", label: t("settings.dark") }, { value: "light", label: t("settings.light") }]} />
<div className="mt-3"><Select label={t("settings.language")} value={selectedLocale} onChange={(v) => setSelectedLocale(v as AppLocale)} options={APP_LOCALES.map((loc) => ({ value: loc, label: LOCALE_LABELS[loc] }))} /></div>
<div className="border-y border-border py-4 mt-3"><label className="flex items-center justify-between text-[13px] text-muted-foreground"><span>{t("exec.enableUpdater")}</span><input type="checkbox" checked={updatesEnabled} onChange={(e) => setUpdatesEnabled(e.target.checked)} /></label></div>
</div>
{(() => { const company = snapshot.companies.find((c) => c.id === companyId); if (!company) return null; const isCeo = company.executionMode !== "ai_cofounder"; return (
<div><Eyebrow className="mb-3">{t("exec.executionMode")}</Eyebrow>
<div className="space-y-3">
<button type="button" onClick={async () => { if (isCeo) return; setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: true, reviewDeliverables: false, executionMode: "ai_ceo" })); await onSaved(); addToast(t("exec.modeSwitched"), "success") } catch (err) { setActionError(describeActionError(err)) } }} className={`w-full rounded-lg border-2 px-4 py-3 text-left transition ${isCeo ? "border-[color:var(--primary)] bg-accent" : "border-border bg-muted hover:border-[color:var(--muted-foreground)] cursor-pointer"}`}>
<div className="text-[13px] font-semibold text-foreground">AI CEO <span className="ml-1 text-[10px] font-normal text-muted-foreground">{t("exec.modeCeoTag")}</span></div>
<div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{t("exec.modeCeoDesc")}</div>
</button>
<button type="button" onClick={async () => { if (!isCeo) return; setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: false, reviewDeliverables: true, executionMode: "ai_cofounder" })); await onSaved(); addToast(t("exec.modeSwitched"), "success") } catch (err) { setActionError(describeActionError(err)) } }} className={`w-full rounded-lg border-2 px-4 py-3 text-left transition ${!isCeo ? "border-[color:var(--primary)] bg-accent" : "border-border bg-muted hover:border-[color:var(--muted-foreground)] cursor-pointer"}`}>
<div className="text-[13px] font-semibold text-foreground">AI Co-Founder <span className="ml-1 text-[10px] font-normal text-muted-foreground">{t("exec.modeCofounderTag")}</span></div>
<div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{t("exec.modeCofounderDesc")}</div>
</button>
</div></div>) })()}
<div><Eyebrow className="mb-3">{t("settings.keyboardShortcuts")}</Eyebrow>
<div className="space-y-1 border-y border-border py-3">
{([
[isMac() ? "⌘K" : "Ctrl+K", t("settings.shortcut.commandPalette")],
[isMac() ? "⌘Enter" : "Ctrl+Enter", t("settings.shortcut.sendMessage")],
["Escape", t("settings.shortcut.closeModal")],
["↑ / ↓", t("settings.shortcut.navigatePalette")],
["Enter", t("settings.shortcut.selectPaletteResult")],
["Tab", t("settings.shortcut.moveFocus")],
] as const).map(([key, desc]) => (
<div key={key} className="flex items-center justify-between px-3 py-1.5 text-[12px]">
<span className="text-muted-foreground">{desc}</span>
<kbd className="rounded border border-border bg-muted px-2 py-0.5 font-mono text-[11px] text-foreground">{key}</kbd>
</div>
))}
</div>
</div>
<div><Eyebrow className="mb-3">{t("exec.actions")}</Eyebrow><div className="grid gap-3 md:grid-cols-2"><ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} /><ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} /><ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} /><ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} /></div></div>
<div><Eyebrow className="mb-3">{t("exec.secretBackend")}</Eyebrow><div className="border-y border-border py-3 text-[13px] text-muted-foreground">{snapshot.backend.secretBackend}</div></div>
<div><Eyebrow className="mb-3">{t("exec.secrets")} ({secrets.length})</Eyebrow>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-border overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-border"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-border last:border-b-0"><td className="px-4 py-3 font-medium text-foreground">{s.name}</td><td className="px-4 py-3 text-muted-foreground">{s.description || "\u2014"}</td><td className="px-4 py-3 font-mono text-muted-foreground">{s.backend}</td><td className="px-4 py-3 font-mono text-muted-foreground">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label={t("settings.delete")} onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
<div className="space-y-8">
<GeneralSection
snapshot={snapshot}
theme={theme}
setTheme={setTheme}
selectedLocale={selectedLocale}
setSelectedLocale={setSelectedLocale}
updatesEnabled={updatesEnabled}
setUpdatesEnabled={setUpdatesEnabled}
onSaved={onSaved}
/>
{company ? <ExecutionModeSection company={company} onSaved={onSaved} /> : null}
<ShortcutsSection />
<DataSection snapshot={snapshot} onSaved={onSaved} />
<SecretsSection secrets={secrets} companyId={companyId} onSaved={onSaved} />
</div>
)
);
}
@@ -0,0 +1,123 @@
import { useState } from "react";
import { ArrowClockwise, ClockCounterClockwise, Download } from "../../ui/icons";
import type { ProfileSnapshot } from "@shared/types";
import { ActionButton, Eyebrow, InlineNotice } from "../../ui";
import { useT } from "../../../i18n";
import { unwrap } from "../../../lib/desktop";
import { describeActionError } from "../execution-utils";
import { useAppStore } from "../../../state/store";
interface DataSectionProps {
snapshot: ProfileSnapshot;
onSaved: () => Promise<void>;
}
/**
* Data & release operations — profile backup/restore, update check,
* secret backend info. These are the high-blast-radius actions:
* Restore replaces the active profile on disk, so it is visually
* separated from Backup and the ephemeral update check. No single
* "Save" button — granular auto-save happens in GeneralSection.
*/
export function DataSection({ snapshot, onSaved }: DataSectionProps) {
const t = useT();
const addToast = useAppStore((s) => s.addToast);
const [notice, setNotice] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<"check" | "backup" | "restore" | null>(null);
const clearBanners = () => {
setNotice(null);
setError(null);
};
const handleCheckForUpdates = async () => {
clearBanners();
setLoading("check");
try {
const started = unwrap(await window.agentCompany.checkForUpdates());
setNotice(started ? t("settings.updateCheckStarted") : t("settings.updateCheckSkipped"));
} catch (err) {
setError(describeActionError(err));
} finally {
setLoading(null);
}
};
const handleBackup = async () => {
clearBanners();
setLoading("backup");
try {
const backupPath = unwrap(await window.agentCompany.backupProfile());
if (backupPath) {
setNotice(t("settings.profileBackupAt").replace("{path}", backupPath));
addToast(t("settings.profileBackupCreated"), "success");
}
} catch (err) {
setError(describeActionError(err));
} finally {
setLoading(null);
}
};
const handleRestore = async () => {
clearBanners();
if (!window.confirm(t("settings.restoreConfirm"))) return;
setLoading("restore");
try {
const restored = unwrap(await window.agentCompany.restoreProfile());
if (!restored) return;
await onSaved();
setNotice(t("settings.profileRestored"));
addToast(t("settings.profileRestored"), "success");
} catch (err) {
setError(describeActionError(err));
} finally {
setLoading(null);
}
};
return (
<div className="space-y-6">
{error ? <InlineNotice message={error} /> : null}
{!error && notice ? <InlineNotice message={notice} tone="success" /> : null}
<div>
<Eyebrow className="mb-3">{t("settings.updates")}</Eyebrow>
<ActionButton
label={t("exec.checkForUpdates")}
icon={Download}
onClick={() => void handleCheckForUpdates()}
loading={loading === "check"}
/>
</div>
<div>
<Eyebrow className="mb-3">{t("settings.backupRestore")}</Eyebrow>
<div className="grid gap-3 md:grid-cols-2">
<ActionButton
label={t("exec.backupProfile")}
icon={ClockCounterClockwise}
onClick={() => void handleBackup()}
loading={loading === "backup"}
/>
<ActionButton
label={t("exec.restoreProfile")}
tone="danger"
icon={ArrowClockwise}
onClick={() => void handleRestore()}
loading={loading === "restore"}
/>
</div>
<p className="mt-2 text-[11px] text-muted-foreground">{t("settings.restoreHint")}</p>
</div>
<div>
<Eyebrow className="mb-3">{t("exec.secretBackend")}</Eyebrow>
<div className="rounded-lg border border-border bg-card px-3 py-2 text-[13px] text-muted-foreground">
{snapshot.backend.secretBackend}
</div>
</div>
</div>
);
}
@@ -0,0 +1,105 @@
import { useState } from "react";
import type { CompanyRecord } from "@shared/types";
import { Eyebrow } from "../../ui";
import { useT } from "../../../i18n";
import { unwrap } from "../../../lib/desktop";
import { describeActionError } from "../execution-utils";
import { useAppStore } from "../../../state/store";
interface ExecutionModeSectionProps {
company: CompanyRecord;
onSaved: () => Promise<void>;
}
/**
* AI CEO vs AI Co-Founder toggle — company-scoped. The header line
* makes the scope explicit ("当前公司:X") so multi-company users know
* which company they are changing.
*/
export function ExecutionModeSection({ company, onSaved }: ExecutionModeSectionProps) {
const t = useT();
const addToast = useAppStore((s) => s.addToast);
const [error, setError] = useState<string | null>(null);
const isCeo = company.executionMode !== "ai_cofounder";
const applyMode = async (mode: "ai_ceo" | "ai_cofounder") => {
if ((mode === "ai_ceo" && isCeo) || (mode === "ai_cofounder" && !isCeo)) return;
setError(null);
try {
unwrap(
await window.agentCompany.saveCompany({
id: company.id,
name: company.name,
description: company.description,
status: company.status,
autoApproveHires: mode === "ai_ceo",
reviewDeliverables: mode === "ai_cofounder",
executionMode: mode,
}),
);
await onSaved();
addToast(t("exec.modeSwitched"), "success");
} catch (err) {
setError(describeActionError(err));
}
};
return (
<div>
<Eyebrow className="mb-1">{t("exec.executionMode")}</Eyebrow>
<p className="mb-3 text-[11px] text-muted-foreground">
{t("settings.modeScope").replace("{company}", company.name)}
</p>
<div className="space-y-3">
<ModeCard
label="AI CEO"
tag={t("exec.modeCeoTag")}
description={t("exec.modeCeoDesc")}
selected={isCeo}
onClick={() => void applyMode("ai_ceo")}
/>
<ModeCard
label="AI Co-Founder"
tag={t("exec.modeCofounderTag")}
description={t("exec.modeCofounderDesc")}
selected={!isCeo}
onClick={() => void applyMode("ai_cofounder")}
/>
</div>
{error ? <p className="mt-3 text-[12px] text-destructive">{error}</p> : null}
</div>
);
}
function ModeCard({
label,
tag,
description,
selected,
onClick,
}: {
label: string;
tag: string;
description: string;
selected: boolean;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
aria-pressed={selected}
className={`w-full rounded-lg border-2 px-4 py-3 text-left transition ${
selected
? "border-[color:var(--primary)] bg-accent"
: "cursor-pointer border-border bg-muted hover:border-[color:var(--muted-foreground)]"
}`}
>
<div className="text-[13px] font-semibold text-foreground">
{label}
<span className="ml-2 text-[10px] font-normal text-muted-foreground">{tag}</span>
</div>
<div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{description}</div>
</button>
);
}
@@ -0,0 +1,94 @@
import type { ProfileSnapshot } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { APP_LOCALES, LOCALE_LABELS } from "@shared/locale";
import { Eyebrow, Select } from "../../ui";
import { useT } from "../../../i18n";
interface GeneralSectionProps {
snapshot: ProfileSnapshot;
theme: ProfileSnapshot["theme"];
setTheme: (theme: ProfileSnapshot["theme"]) => void;
selectedLocale: AppLocale;
setSelectedLocale: (locale: AppLocale) => void;
updatesEnabled: boolean;
setUpdatesEnabled: (enabled: boolean) => void;
onSaved: () => Promise<void>;
}
/**
* Theme / language / auto-update preferences. Every control writes to
* the store immediately via `updateSettings` — there is no global "Save
* settings" step because the app already persists each field
* individually as soon as the user changes it.
*/
export function GeneralSection({
snapshot,
theme,
setTheme,
selectedLocale,
setSelectedLocale,
updatesEnabled,
setUpdatesEnabled,
onSaved,
}: GeneralSectionProps) {
const t = useT();
const applyTheme = (next: ProfileSnapshot["theme"]) => {
setTheme(next);
const root = document.documentElement;
root.className = next;
root.style.colorScheme = next === "system" ? "light dark" : next;
void window.agentCompany.updateSettings({ theme: next }).then(() => onSaved());
};
const applyLocale = (next: AppLocale) => {
setSelectedLocale(next);
void window.agentCompany.updateSettings({ locale: next }).then(() => onSaved());
};
const applyUpdates = (enabled: boolean) => {
setUpdatesEnabled(enabled);
void window.agentCompany.updateSettings({ updatesEnabled: enabled }).then(() => onSaved());
};
return (
<div>
<Eyebrow className="mb-3">{t("exec.preferences")}</Eyebrow>
<div className="grid gap-3">
<Select
label={t("settings.theme")}
value={theme}
onChange={(value) => applyTheme(value as ProfileSnapshot["theme"])}
options={[
{ value: "system", label: t("settings.system") },
{ value: "dark", label: t("settings.dark") },
{ value: "light", label: t("settings.light") },
]}
/>
<Select
label={t("settings.language")}
value={selectedLocale}
onChange={(value) => applyLocale(value as AppLocale)}
options={APP_LOCALES.map((locale) => ({ value: locale, label: LOCALE_LABELS[locale] }))}
/>
<div className="border-t border-border pt-3">
<label className="flex items-center justify-between text-[13px] text-foreground">
<div className="flex flex-col">
<span className="font-medium">{t("exec.enableUpdater")}</span>
<span className="mt-0.5 text-[11px] text-muted-foreground">{t("settings.updaterHint")}</span>
</div>
<input
type="checkbox"
checked={updatesEnabled}
onChange={(event) => applyUpdates(event.target.checked)}
className="h-4 w-4 accent-[color:var(--primary)]"
/>
</label>
<div className="mt-2 font-mono truncate text-[11px] text-muted-foreground">
{t("exec.profilePath")}: {snapshot.profilePath}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,102 @@
import { useState } from "react";
import { Trash } from "../../ui/icons";
import type { SecretRecord } from "@shared/types";
import { ActionButton, ConfirmDialog, EmptyState, Eyebrow } from "../../ui";
import { formatTime } from "../../../lib/formatters";
import { useT } from "../../../i18n";
import { unwrap } from "../../../lib/desktop";
import { describeActionError } from "../execution-utils";
import { useAppStore } from "../../../state/store";
interface SecretsSectionProps {
secrets: SecretRecord[];
companyId: string;
onSaved: () => Promise<void>;
}
/**
* Per-company secret bindings. Secrets are deleted via a confirm
* dialog because deletions break downstream connectors that still
* reference them; the dialog surfaces that cost in its copy.
*/
export function SecretsSection({ secrets, companyId, onSaved }: SecretsSectionProps) {
const t = useT();
const addToast = useAppStore((s) => s.addToast);
const [confirmId, setConfirmId] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const target = secrets.find((entry) => entry.id === confirmId) ?? null;
const handleDelete = async (id: string) => {
setDeleteError(null);
try {
unwrap(await window.agentCompany.deleteSecret({ id, companyId }));
setConfirmId(null);
addToast(t("settings.secretDeleted"), "success");
await onSaved();
} catch (err) {
setDeleteError(describeActionError(err));
}
};
return (
<div>
<ConfirmDialog
open={confirmId !== null}
title={t("settings.deleteSecretTitle")}
message={t("settings.deleteSecretConfirm").replace("{name}", target?.name ?? "")}
errorMessage={deleteError}
onConfirm={() => confirmId && void handleDelete(confirmId)}
onCancel={() => {
setDeleteError(null);
setConfirmId(null);
}}
/>
<Eyebrow className="mb-3">
{t("exec.secrets")} ({secrets.length})
</Eyebrow>
{secrets.length === 0 ? (
<EmptyState title={t("exec.noSecretsStored")} />
) : (
<div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full text-left text-[13px]">
<thead>
<tr className="border-b border-border">
<HeaderCell>{t("exec.secretName")}</HeaderCell>
<HeaderCell>{t("exec.secretDescription")}</HeaderCell>
<HeaderCell>{t("exec.secretBackendCol")}</HeaderCell>
<HeaderCell>{t("exec.secretUpdated")}</HeaderCell>
<HeaderCell />
</tr>
</thead>
<tbody>
{secrets.map((secret) => (
<tr key={secret.id} className="border-b border-border last:border-b-0">
<td className="px-4 py-3 font-medium text-foreground">{secret.name}</td>
<td className="px-4 py-3 text-muted-foreground">{secret.description || "—"}</td>
<td className="px-4 py-3 font-mono text-muted-foreground">{secret.backend}</td>
<td className="px-4 py-3 font-mono text-muted-foreground">{formatTime(secret.updatedAt)}</td>
<td className="px-4 py-3">
<ActionButton
label={t("settings.delete")}
tone="danger"
icon={Trash}
onClick={() => setConfirmId(secret.id)}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
function HeaderCell({ children }: { children?: React.ReactNode }) {
return (
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{children}
</th>
);
}
@@ -0,0 +1,40 @@
import { Eyebrow } from "../../ui";
import { useT } from "../../../i18n";
import { isMac } from "../../../lib/platform";
/**
* Read-only keyboard shortcut reference. Listed in the settings page
* because this is where users look when they want to remember how to
* invoke a hotkey; it is NOT a configurable surface — so the visuals
* are lighter than the other settings sections (no active state, no
* action buttons).
*/
export function ShortcutsSection() {
const t = useT();
const mac = isMac();
const rows: Array<[string, string]> = [
[mac ? "⌘K" : "Ctrl+K", t("settings.shortcut.commandPalette")],
[mac ? "⌘Enter" : "Ctrl+Enter", t("settings.shortcut.sendMessage")],
["Escape", t("settings.shortcut.closeModal")],
["↑ / ↓", t("settings.shortcut.navigatePalette")],
["Enter", t("settings.shortcut.selectPaletteResult")],
["Tab", t("settings.shortcut.moveFocus")],
];
return (
<div>
<Eyebrow className="mb-3">{t("settings.keyboardShortcuts")}</Eyebrow>
<div className="divide-y divide-border rounded-lg border border-border">
{rows.map(([key, description]) => (
<div key={key} className="flex items-center justify-between px-3 py-2 text-[12px]">
<span className="text-muted-foreground">{description}</span>
<kbd className="rounded border border-border bg-muted px-2 py-0.5 font-mono text-[11px] text-foreground">
{key}
</kbd>
</div>
))}
</div>
</div>
);
}