This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 14:03:56 -04:00
1 parent 806200b3d1
commit 5547c29491
216 files changed
+5043 -3352

No files matched your search

@@ -1,12 +1,13 @@
import { CaretDown, CaretRight } from "../ui/icons";
import type { ProfileSnapshot } from "@shared/types";
import { Input } from "../ui";
import { useT } from "../../i18n";
/* ------------------------------------------------------------------ */
/* Shared */
/* ------------------------------------------------------------------ */
export const INPUT_CLS =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring w-full rounded-lg border border-border bg-card px-3 py-2.5 text-[13px] text-foreground placeholder:text-muted-foreground";
export { Input };
export interface ModelEntry {
id: string;
@@ -25,12 +26,13 @@ export function parseEnvValue(text: string, key: string): string {
/* StatusBadge */
/* ------------------------------------------------------------------ */
export function StatusBadge({ status, zh }: { status: ConnectorStatus; zh: boolean }) {
export function StatusBadge({ status }: { status: ConnectorStatus }) {
const t = useT();
if (status === "ready") {
return (
<span className="inline-flex items-center gap-1.5 rounded-full bg-success-muted px-2 py-0.5 text-[11px] font-medium text-success">
<span className="h-1.5 w-1.5 rounded-full bg-success" />
{zh ? "就绪" : "Ready"}
{t("models.ready")}
</span>
);
}
@@ -38,7 +40,7 @@ export function StatusBadge({ status, zh }: { status: ConnectorStatus; zh: boole
return (
<span className="inline-flex items-center gap-1.5 rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground" />
{zh ? "未安装" : "Not installed"}
{t("models.notInstalled")}
</span>
);
}
@@ -46,7 +48,7 @@ export function StatusBadge({ status, zh }: { status: ConnectorStatus; zh: boole
return (
<span className="inline-flex items-center gap-1.5 rounded-full bg-warning-muted px-2 py-0.5 text-[11px] font-medium text-warning">
<span className="h-1.5 w-1.5 rounded-full bg-warning" />
{zh ? "需登录" : "Auth required"}
{t("models.authRequired")}
</span>
);
}
@@ -54,7 +56,7 @@ export function StatusBadge({ status, zh }: { status: ConnectorStatus; zh: boole
return (
<span className="inline-flex items-center gap-1.5 rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
<span className="h-3 w-3 animate-spin rounded-full border-[1.5px] border-muted-foreground border-t-primary" />
{zh ? "检测中" : "Checking"}
{t("models.checking")}
</span>
);
}
@@ -137,18 +139,17 @@ export function ModelToggleList({
onToggle,
onRemove,
canRemove,
zh,
}: {
models: ModelEntry[];
onToggle: (id: string) => void;
onRemove?: (id: string) => void;
canRemove: boolean;
zh: boolean;
}) {
const t = useT();
if (models.length === 0) {
return (
<div className="text-[12px] text-muted-foreground">
{zh ? "暂无模型" : "No models available"}
{t("models.noModels")}
</div>
);
}
@@ -156,7 +157,7 @@ export function ModelToggleList({
return (
<div>
<div className="mb-1.5 text-[12px] font-medium text-muted-foreground">
{zh ? "模型" : "Models"} ({enabledCount}/{models.length} {zh ? "已启用" : "enabled"})
{t("models.label")} ({t("models.enabledCount").replace("{enabled}", String(enabledCount)).replace("{total}", String(models.length))})
</div>
<div className="space-y-1">
{models.map((m) => (
@@ -168,7 +169,7 @@ export function ModelToggleList({
<ToggleSwitch
checked={m.enabled}
onChange={() => onToggle(m.id)}
label={`${m.enabled ? "Disable" : "Enable"} ${m.label}`}
label={(m.enabled ? t("models.disableToggle") : t("models.enableToggle")).replace("{name}", m.label)}
/>
<span
className={`text-[12px] ${m.enabled ? "text-foreground" : "text-muted-foreground line-through"}`}
@@ -181,7 +182,7 @@ export function ModelToggleList({
type="button"
onClick={() => onRemove(m.id)}
className="text-muted-foreground transition hover:text-destructive"
aria-label={`Remove ${m.label}`}
aria-label={t("models.removeModel").replace("{name}", m.label)}
>
<XIcon size={12} />
</button>
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import type { BudgetPolicyRecord } from "@shared/types";
import { ActionButton, Input, Modal, Select } from "../ui";
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input";
import { useT } from "../../i18n";
interface PolicyFormModalProps {
open: boolean;
@@ -14,6 +15,7 @@ interface PolicyFormModalProps {
}
export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPolicy, agents, projects }: PolicyFormModalProps) {
const t = useT();
const [scopeType, setScopeType] = useState<"company" | "agent" | "project">("company");
const [scopeId, setScopeId] = useState(companyId);
const [amountDollars, setAmountDollars] = useState("100");
@@ -79,23 +81,23 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
<Modal
open={open}
onClose={onClose}
title={editingPolicy ? "Edit Budget Policy" : "Add Budget Policy"}
title={editingPolicy ? t("policy.editTitle") : t("policy.addTitle")}
>
<div className="grid gap-4">
<Select
label="Scope"
label={t("policy.scope")}
value={scopeType}
onChange={(v) => setScopeType(v as "company" | "agent" | "project")}
options={[
{ value: "company", label: "Company" },
{ value: "agent", label: "Agent" },
{ value: "project", label: "Project" },
{ value: "company", label: t("policy.scopeCompany") },
{ value: "agent", label: t("policy.scopeAgent") },
{ value: "project", label: t("policy.scopeProject") },
]}
/>
{scopeType === "agent" ? (
<Select
label="Agent"
label={t("policy.scopeAgent")}
value={scopeId}
onChange={setScopeId}
options={agents.map((a) => ({ value: a.id, label: a.name }))}
@@ -104,7 +106,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
{scopeType === "project" ? (
<Select
label="Project"
label={t("policy.scopeProject")}
value={scopeId}
onChange={setScopeId}
options={projects.map((p) => ({ value: p.id, label: p.name }))}
@@ -112,7 +114,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
) : null}
<Input
label="Monthly Budget (USD)"
label={t("policy.monthlyBudget")}
value={amountDollars}
onChange={setAmountDollars}
type="number"
@@ -120,7 +122,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
/>
<Input
label="Warning Threshold (%)"
label={t("policy.warningThreshold")}
value={warnPercent}
onChange={setWarnPercent}
type="number"
@@ -134,7 +136,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
onChange={(e) => setHardStopEnabled(e.target.checked)}
className="h-4 w-4 rounded border-border accent-[color:var(--primary)]"
/>
<span className="text-[13px] text-foreground">Enable hard stop (block new runs at 100%)</span>
<span className="text-[13px] text-foreground">{t("policy.hardStop")}</span>
</label>
<label className="flex items-center gap-2.5">
@@ -144,19 +146,13 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
onChange={(e) => setNotifyEnabled(e.target.checked)}
className="h-4 w-4 rounded border-border accent-[color:var(--primary)]"
/>
<span className="text-[13px] text-foreground">Send notifications</span>
<span className="text-[13px] text-foreground">{t("policy.sendNotifications")}</span>
</label>
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
className="rounded-lg px-4 py-2 text-[13px] font-medium text-muted-foreground transition hover:text-foreground"
onClick={onClose}
>
Cancel
</button>
<ActionButton label={t("policy.cancel")} onClick={onClose} />
<ActionButton
label={saving ? "Saving..." : "Save Policy"}
label={saving ? t("policy.saving") : t("policy.savePolicy")}
tone="accent"
disabled={saving}
onClick={() => void handleSave()}
@@ -7,7 +7,7 @@ import { useLocale, useT } from "../../i18n"
import { formatTime } from "../../lib/formatters"
import { unwrap } from "../../lib/desktop"
import { isMac } from "../../lib/platform"
import { ActionButton, ConfirmDialog, EmptyState, InlineNotice, Select } from "../ui"
import { ActionButton, ConfirmDialog, EmptyState, Eyebrow, InlineNotice, Select } from "../ui"
import { describeActionError } from "./execution-utils"
import { useAppStore } from "../../state/store"
@@ -19,25 +19,25 @@ export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapsh
const secretToDelete = secrets.find((s) => s.id === confirmDeleteSecretId) ?? null
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("Secret deleted."); addToast("Secret deleted", "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("Settings saved."); addToast("Settings saved", "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 ? "Update check started." : "Update check skipped because no release update feed is configured.") } 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(`Profile backup created at ${backupPath}.`); addToast("Profile backup created", "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("Profile restored."); addToast("Profile restored", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
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) } }
return (
<div className="space-y-6">
<ConfirmDialog open={confirmDeleteSecretId !== null} title="Delete secret" message={`Permanently delete secret "${secretToDelete?.name ?? ""}"? Any connectors referencing it will lose access.`} errorMessage={deleteSecretError} onConfirm={() => confirmDeleteSecretId && void handleDeleteSecret(confirmDeleteSecretId)} onCancel={() => { setDeleteSecretError(null); setConfirmDeleteSecretId(null) }} />
<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><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.profile")}</div><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><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.preferences")}</div>
<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><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.executionMode")}</div>
<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>
@@ -48,15 +48,15 @@ export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapsh
<div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{t("exec.modeCofounderDesc")}</div>
</button>
</div></div>) })()}
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Keyboard Shortcuts</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", "Open command palette"],
[isMac() ? "⌘Enter" : "Ctrl+Enter", "Send message"],
["Escape", "Close modal / palette"],
["↑ / ↓", "Navigate palette results"],
["Enter", "Select palette result"],
["Tab", "Move focus between elements"],
[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>
@@ -65,9 +65,9 @@ export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapsh
))}
</div>
</div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.actions")}</div><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><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretBackend")}</div><div className="border-y border-border py-3 text-[13px] text-muted-foreground">{snapshot.backend.secretBackend}</div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secrets")} ({secrets.length})</div>{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="Delete" onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></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>
)
}
@@ -4,7 +4,7 @@ import type { CommentRecord, GoalRecord, ProfileSnapshot, ProjectRecord, RunReco
import { useT } from "../../i18n"
import { formatMoney, formatTime } from "../../lib/formatters"
import { unwrap } from "../../lib/desktop"
import { ActionButton, ConfirmDialog, InlineNotice, Input, Select, StatusPill, TextArea } from "../ui"
import { ActionButton, ConfirmDialog, Eyebrow, InlineNotice, Input, Select, StatusPill, TextArea } from "../ui"
import { describeActionError } from "./execution-utils"
import { TaskComments } from "./TaskComments"
import { useAppStore } from "../../state/store"
@@ -80,7 +80,7 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
)}
{(() => { const subtasks = tasks.filter((t) => t.parentId === task.id); if (subtasks.length === 0) return null; return (
<div className="space-y-2">
<div className="flex items-center gap-2"><ListChecks size={14} className="text-muted-foreground" /><span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.subtasks")} ({subtasks.length})</span><span className="text-[10px] text-success">{t("exec.doneCount").replace("{0}", String(subtasks.filter((st) => st.status === "done").length)).replace("{1}", String(subtasks.length))}</span></div>
<div className="flex items-center gap-2"><ListChecks size={14} className="text-muted-foreground" /><Eyebrow>{t("exec.subtasks")} ({subtasks.length})</Eyebrow><span className="text-[10px] text-success">{t("exec.doneCount").replace("{0}", String(subtasks.filter((st) => st.status === "done").length)).replace("{1}", String(subtasks.length))}</span></div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted"><div className="h-full rounded-full bg-success transition-all duration-300" style={{ width: `${subtasks.length > 0 ? (subtasks.filter((t) => t.status === "done").length / subtasks.length) * 100 : 0}%` }} /></div>
<div className="space-y-1">{subtasks.map((sub) => { const subAssignee = agentMap.get(sub.assigneeAgentId ?? ""); return (
<div key={sub.id} className="flex items-center gap-2 rounded-lg px-3 py-2 text-[12px] bg-muted">
@@ -89,7 +89,7 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
{subAssignee ? <span className="shrink-0 text-[10px] text-muted-foreground">{subAssignee.name}</span> : null}<StatusPill status={sub.status} />
</div>) })}</div></div>) })()}
<TaskComments taskId={task.id} companyId={task.companyId} agents={agents} comments={comments} onRefresh={onRefresh} />
{runs.length > 0 && <div className="space-y-2"><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.runHistory")}</div><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agentMap.get(run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
{runs.length > 0 && <div className="space-y-2"><Eyebrow>{t("exec.runHistory")}</Eyebrow><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agentMap.get(run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
<div key={run.id} className={`rounded-lg border px-3 py-2.5 ${isActive ? "border-[color:var(--primary)] bg-success-muted" : run.status === "failed" || run.status === "timed_out" ? "border-[color:var(--destructive)] border-opacity-30 bg-destructive-muted" : "border-border bg-muted"}`}>
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-success" /> : null}<span className="truncate text-[13px] font-medium text-foreground">{run.summary || (isActive ? t("exec.runningEllipsis") : t("exec.completedRun"))}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-muted-foreground">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="font-mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="font-mono tabular-nums">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
{run.errorMessage ? <div className="mt-1.5 rounded bg-destructive-muted px-2 py-1 text-[11px] text-destructive break-words">{run.errorMessage}</div> : null}
@@ -4,7 +4,8 @@ import type { ApprovalRecord, DocumentRecord } from "@shared/types"
import { extractApprovalDocumentReferences, type ApprovalDocumentReference } from "@shared/approval-documents"
import { unwrap } from "../../../lib/desktop"
import { useAppStore } from "../../../state/store"
import { ActionButton, InlineNotice, Input, TextArea } from "../../ui"
import { ActionButton, Card, Eyebrow, InlineNotice, Input, TextArea } from "../../ui"
import { useT } from "../../../i18n"
import { describeActionError } from "../execution-utils"
function ApprovalDocumentEditor({
@@ -16,6 +17,7 @@ function ApprovalDocumentEditor({
document: DocumentRecord
fallback?: ApprovalDocumentReference
}) {
const t = useT()
const addToast = useAppStore((s) => s.addToast)
const [title, setTitle] = useState(document.title)
const [content, setContent] = useState(document.content ?? "")
@@ -24,7 +26,7 @@ function ApprovalDocumentEditor({
async function handleSave() {
if (!title.trim()) {
setSaveError("Document title is required.")
setSaveError(t("approval.documentTitleRequired"))
return
}
setSaving(true)
@@ -47,7 +49,7 @@ function ApprovalDocumentEditor({
tagsJson: document.tagsJson,
}),
)
addToast(`Saved document: ${title.trim()}`, "success")
addToast(t("approval.documentSavedToast").replace("{title}", title.trim()), "success")
} catch (error) {
setSaveError(describeActionError(error))
} finally {
@@ -56,7 +58,7 @@ function ApprovalDocumentEditor({
}
return (
<div className="space-y-3 rounded-lg border border-border px-4 py-3">
<Card padding="sm" className="space-y-3">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-[13px] font-medium text-foreground">{fallback?.title ?? document.title}</div>
@@ -65,7 +67,7 @@ function ApprovalDocumentEditor({
</div>
</div>
<ActionButton
label={saving ? "Saving..." : "Save changes"}
label={saving ? t("approval.saving") : t("approval.saveChanges")}
icon={CheckCircle}
tone="accent"
onClick={() => void handleSave()}
@@ -73,9 +75,9 @@ function ApprovalDocumentEditor({
/>
</div>
{saveError ? <InlineNotice message={saveError} /> : null}
<Input label="Title" value={title} onChange={setTitle} placeholder="Document title" />
<TextArea label="Content" value={content} onChange={setContent} rows={12} placeholder="Document content" />
</div>
<Input label={t("approval.docTitle")} value={title} onChange={setTitle} placeholder={t("approval.docTitlePlaceholder")} />
<TextArea label={t("approval.docContent")} value={content} onChange={setContent} rows={12} placeholder={t("approval.docContentPlaceholder")} />
</Card>
)
}
@@ -88,6 +90,7 @@ export function ApprovalDocumentsPanel({
documents?: DocumentRecord[]
companyId: string
}) {
const t = useT()
const references = useMemo(
() => extractApprovalDocumentReferences(approval.payloadJson),
[approval.payloadJson],
@@ -109,7 +112,7 @@ export function ApprovalDocumentsPanel({
return (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Linked Documents</div>
<Eyebrow tone="primary">{t("approval.linkedDocuments")}</Eyebrow>
{fullDocs.length > 0
? fullDocs.map(({ reference, document }) => (
<ApprovalDocumentEditor
@@ -120,15 +123,15 @@ export function ApprovalDocumentsPanel({
/>
))
: references.map((reference) => (
<div key={reference.id} className="rounded-lg border border-border bg-muted px-4 py-3">
<Card key={reference.id} padding="sm" className="bg-muted">
<div className="text-[13px] font-medium text-foreground">{reference.title ?? reference.id}</div>
<div className="text-[11px] text-muted-foreground">
{reference.type?.replaceAll("_", " ") ?? "document"} · {reference.id}
</div>
<div className="mt-2 text-[12px] text-muted-foreground">
The linked document is not loaded in the current snapshot yet. Reopen the approval after the next refresh.
{t("approval.documentNotLoaded")}
</div>
</div>
</Card>
))}
{approval.impactSummary ? (
<div className="text-[11px] text-muted-foreground">{approval.impactSummary}</div>
@@ -3,7 +3,7 @@ import type { AgentRecord, ApprovalRecord, DocumentRecord, TaskRecord } from "@s
import { extractApprovalDocumentReferences } from "@shared/approval-documents"
import { useT } from "../../../i18n"
import { unwrap } from "../../../lib/desktop"
import { ConfirmDialog, InlineNotice } from "../../ui"
import { Card, ConfirmDialog, Eyebrow, InlineNotice } from "../../ui"
import { useAppStore } from "../../../state/store"
import { describeActionError } from "../execution-utils"
import { SocialDraftPreview } from "../SocialDraftPreview"
@@ -69,12 +69,12 @@ export function ApprovalInspector({
if (isSocialPost && state === "approved" && scheduleMode === "scheduled") {
if (!scheduledAt) {
setScheduleError("Please select a date and time for scheduling.")
setScheduleError(t("approval.scheduleSelectTime"))
return
}
const chosenTime = new Date(scheduledAt).getTime()
if (chosenTime <= Date.now()) {
setScheduleError("Scheduled time must be in the future.")
setScheduleError(t("approval.scheduleFuture"))
return
}
}
@@ -87,9 +87,9 @@ export function ApprovalInspector({
noteForDecision = noteForDecision ? `${noteForDecision} ${scheduleInfo}` : scheduleInfo
}
await onDecide(state, noteForDecision)
if (state === "approved") addToast("Approval granted — agent is moving forward", "success")
else if (state === "rejected") addToast("Approval rejected", "warn")
else if (state === "revision_requested") addToast("Revision requested — agent will revise and resubmit", "warn")
if (state === "approved") addToast(t("approval.granted"), "success")
else if (state === "rejected") addToast(t("approval.rejectedToast"), "warn")
else if (state === "revision_requested") addToast(t("approval.revisionRequested"), "warn")
} catch (error) {
setActionError(describeActionError(error))
} finally {
@@ -115,14 +115,17 @@ export function ApprovalInspector({
<ApprovalSummary approval={approval} requestingAgent={requestingAgent} relatedTask={relatedTask} />
{approval.type === "hire_agent" ? <ProposedHireCard approval={approval} agents={agents} /> : null}
{approval.type === "approve_ceo_strategy" ? (
<div className="rounded-lg border border-[color:var(--destructive)] border-opacity-30 bg-warning-muted px-4 py-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-warning">
Organization Strategy
</div>
<Card
padding="sm"
className="border-[color:var(--destructive)] border-opacity-30 bg-warning-muted"
>
<Eyebrow tone="warning" className="mb-2">
{t("approval.orgStrategy")}
</Eyebrow>
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">
{approval.payloadSummary}
</div>
</div>
</Card>
) : null}
<ApprovalDocumentsPanel approval={approval} documents={documents} companyId={companyId} />
{approval.type === "deliverable_review" && !hasDocumentReferences ? (
@@ -160,7 +163,7 @@ export function ApprovalInspector({
{approval.decisionNote ? (
<div className="border-t border-border pt-4 text-[13px] text-muted-foreground">
Last note: {approval.decisionNote}
{t("approval.lastNote")} {approval.decisionNote}
</div>
) : null}
</div>
@@ -2,6 +2,7 @@ import { Briefcase, Robot } from "../../ui/icons"
import type { AgentRecord, ApprovalRecord, TaskRecord } from "@shared/types"
import { formatTime } from "../../../lib/formatters"
import { StatusPill } from "../../ui"
import { useT } from "../../../i18n"
import { TypeBadge } from "./TypeBadge"
export function ApprovalSummary({
@@ -13,6 +14,7 @@ export function ApprovalSummary({
requestingAgent: AgentRecord | null
relatedTask: TaskRecord | null
}) {
const t = useT()
return (
<>
<div className="flex items-center gap-3">
@@ -26,8 +28,8 @@ export function ApprovalSummary({
<div className="grid gap-2 text-[13px] md:grid-cols-2">
<div className="flex items-center gap-2 text-muted-foreground">
<Robot size={14} />
Requested by:{" "}
<span className="font-medium text-foreground">{requestingAgent?.name ?? "Unknown"}</span>
{t("approval.requestedBy")}{" "}
<span className="font-medium text-foreground">{requestingAgent?.name ?? t("approval.unknownRequester")}</span>
{requestingAgent?.role ? (
<span className="text-[10px] text-muted-foreground">({requestingAgent.role})</span>
) : null}
@@ -35,15 +37,15 @@ export function ApprovalSummary({
{relatedTask ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Briefcase size={14} />
Task: <span className="font-medium text-foreground">{relatedTask.title}</span>
{t("approval.task")} <span className="font-medium text-foreground">{relatedTask.title}</span>
</div>
) : null}
<div className="text-muted-foreground">
Created: <span className="font-mono text-foreground">{formatTime(approval.createdAt)}</span>
{t("approval.created")} <span className="font-mono text-foreground">{formatTime(approval.createdAt)}</span>
</div>
{approval.updatedAt !== approval.createdAt ? (
<div className="text-muted-foreground">
Updated: <span className="font-mono text-foreground">{formatTime(approval.updatedAt)}</span>
{t("approval.updated")} <span className="font-mono text-foreground">{formatTime(approval.updatedAt)}</span>
</div>
) : null}
</div>
@@ -1,5 +1,5 @@
import { CheckCircle, ClockCounterClockwise, Trash, XCircle } from "../../ui/icons"
import { ActionButton, TextArea } from "../../ui"
import { ActionButton, Eyebrow, TextArea } from "../../ui"
import { useT } from "../../../i18n"
export type DecisionState = "approved" | "rejected" | "revision_requested"
@@ -28,13 +28,13 @@ export function DecisionForm({
<>
{!isDecided ? (
<div>
<label className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
Decision note
</label>
<Eyebrow className="mb-1">
{t("approval.decisionNote")}
</Eyebrow>
<TextArea
value={decisionNote}
onChange={onDecisionNoteChange}
placeholder="Add a note (optional)"
placeholder={t("approval.decisionNotePlaceholder")}
rows={3}
/>
</div>
@@ -43,7 +43,7 @@ export function DecisionForm({
{!isDecided ? (
<>
<ActionButton
label={isSocialPost ? "Approve & Post" : t("exec.approve")}
label={isSocialPost ? t("approval.approvePost") : t("exec.approve")}
tone="accent"
onClick={() => onDecide("approved")}
icon={CheckCircle}
@@ -66,7 +66,7 @@ export function DecisionForm({
) : null}
{isDecided ? (
<ActionButton
label="Delete"
label={t("approval.delete")}
onClick={onRequestDelete}
icon={Trash}
tone="danger"
@@ -1,6 +1,8 @@
import { useMemo, useState } from "react"
import { CaretDown, CaretUp } from "../../ui/icons"
import type { AgentRecord, ApprovalRecord, DocumentRecord } from "@shared/types"
import { Card, Eyebrow } from "../../ui"
import { useT } from "../../../i18n"
import { useApprovalPayload } from "./useApprovalPayload"
interface DocRef {
@@ -36,6 +38,7 @@ export function DeliverableReviewPanel({
documents?: DocumentRecord[]
agents: AgentRecord[]
}) {
const t = useT()
const payload = useApprovalPayload(approval)
const docRefs = useMemo(() => toDocRefs(payload?.documents), [payload])
const docIds = useMemo(() => toDocIds(payload?.documentIds, docRefs), [payload, docRefs])
@@ -52,13 +55,13 @@ export function DeliverableReviewPanel({
return (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Deliverable Review</div>
<Eyebrow tone="primary">{t("approval.deliverableReview")}</Eyebrow>
{fullDocs.length > 0 ? (
fullDocs.map((doc) => {
const isExpanded = expandedDocId === doc.id
const author = doc.authorAgentId ? agents.find((a) => a.id === doc.authorAgentId) : null
return (
<div key={doc.id} className="rounded-lg border border-border overflow-hidden">
<Card key={doc.id} padding="none" className="overflow-hidden">
<button
type="button"
onClick={() => setExpandedDocId(isExpanded ? null : doc.id)}
@@ -78,28 +81,29 @@ export function DeliverableReviewPanel({
<div className="border-t border-border px-4 py-3">
{author ? (
<div className="mb-2 text-[11px] text-muted-foreground">
Author: <span className="font-medium text-foreground">{author.name}</span> ({author.role})
{t("approval.hireAuthor")} <span className="font-medium text-foreground">{author.name}</span> ({author.role})
</div>
) : null}
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">
{doc.content || "No content."}
{doc.content || t("approval.docEmpty")}
</div>
</div>
) : null}
</div>
</Card>
)
})
) : docRefs.length > 0 ? (
docRefs.map((doc) => (
<div
<Card
key={doc.id}
className="flex items-center gap-2 rounded-lg border border-border bg-muted px-4 py-3 text-[12px]"
padding="sm"
className="flex items-center gap-2 bg-muted text-[12px]"
>
<span className="rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">
{doc.type.replaceAll("_", " ")}
</span>
<span className="font-medium text-foreground">{doc.title}</span>
</div>
</Card>
))
) : (
<div className="text-[12px] text-muted-foreground">{approval.payloadSummary}</div>
@@ -1,6 +1,8 @@
import { useMemo } from "react"
import type { AgentRecord, ApprovalRecord } from "@shared/types"
import { formatMoney } from "../../../lib/formatters"
import { Card, Eyebrow } from "../../ui"
import { useT } from "../../../i18n"
import { useApprovalPayload } from "./useApprovalPayload"
interface HireSnapshot {
@@ -25,6 +27,7 @@ export function ProposedHireCard({
approval: ApprovalRecord
agents: AgentRecord[]
}) {
const t = useT()
const payload = useApprovalPayload(approval)
const { proposedAgent, reqSnap, mgr } = useMemo(() => {
@@ -42,52 +45,52 @@ export function ProposedHireCard({
if (!proposedAgent && !reqSnap) return null
const name = proposedAgent?.name ?? reqSnap?.name ?? "Unspecified"
const role = proposedAgent?.role ?? reqSnap?.role ?? "Unspecified"
const name = proposedAgent?.name ?? reqSnap?.name ?? t("approval.proposedUnspecified")
const role = proposedAgent?.role ?? reqSnap?.role ?? t("approval.proposedUnspecified")
const title = proposedAgent?.title || reqSnap?.title
const capabilities = proposedAgent?.capabilities || reqSnap?.capabilities
const connectorId = proposedAgent?.connectorId ?? reqSnap?.connectorId ?? "Unknown"
const connectorId = proposedAgent?.connectorId ?? reqSnap?.connectorId ?? t("approval.proposedUnknown")
const budget = proposedAgent?.budgetMonthlyUsd ?? reqSnap?.budgetMonthlyUsd ?? 0
return (
<div className="rounded-lg border border-[color:var(--primary)] border-opacity-30 bg-accent px-4 py-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Proposed Hire</div>
<Card padding="sm" className="border-[color:var(--primary)] border-opacity-30 bg-accent">
<Eyebrow tone="primary" className="mb-2">{t("approval.proposedHire")}</Eyebrow>
<div className="grid gap-1.5 text-[12px]">
<div>
<span className="text-muted-foreground">Name:</span>{" "}
<span className="text-muted-foreground">{t("approval.proposedName")}</span>{" "}
<span className="font-medium text-foreground">{name}</span>
</div>
<div>
<span className="text-muted-foreground">Role:</span>{" "}
<span className="text-muted-foreground">{t("approval.proposedRole")}</span>{" "}
<span className="text-foreground">{role}</span>
</div>
{title ? (
<div>
<span className="text-muted-foreground">Title:</span>{" "}
<span className="text-muted-foreground">{t("approval.proposedTitle")}</span>{" "}
<span className="text-foreground">{title}</span>
</div>
) : null}
{capabilities ? (
<div>
<span className="text-muted-foreground">Capabilities:</span>{" "}
<span className="text-muted-foreground">{t("approval.proposedCapabilities")}</span>{" "}
<span className="text-foreground">{capabilities}</span>
</div>
) : null}
<div>
<span className="text-muted-foreground">Connector:</span>{" "}
<span className="text-muted-foreground">{t("approval.proposedConnector")}</span>{" "}
<span className="font-mono text-foreground">{connectorId}</span>
</div>
<div>
<span className="text-muted-foreground">Budget:</span>{" "}
<span className="font-mono text-foreground">{formatMoney(budget)}/mo</span>
<span className="text-muted-foreground">{t("approval.proposedBudget")}</span>{" "}
<span className="font-mono text-foreground">{formatMoney(budget)}{t("approval.budgetSuffix")}</span>
</div>
{mgr ? (
<div>
<span className="text-muted-foreground">Reports to:</span>{" "}
<span className="text-muted-foreground">{t("approval.proposedReportsTo")}</span>{" "}
<span className="text-foreground">{mgr.name}</span>
</div>
) : null}
</div>
</div>
</Card>
)
}
@@ -1,6 +1,7 @@
import { useMemo } from "react"
import { Clock } from "../../ui/icons"
import { InlineNotice } from "../../ui"
import { useT } from "../../../i18n"
export type ScheduleMode = "immediate" | "scheduled"
@@ -25,11 +26,12 @@ export function SchedulePicker({
onScheduledAtChange: (value: string) => void
onClearError: () => void
}) {
const t = useT()
const minDatetime = useMemo(() => computeMinDatetime(), [])
return (
<div className="space-y-3">
<div role="radiogroup" aria-label="Post timing" className="space-y-2">
<div role="radiogroup" aria-label={t("approval.scheduleTimingAria")} className="space-y-2">
<label className="flex cursor-pointer items-center gap-2">
<input
type="radio"
@@ -42,7 +44,7 @@ export function SchedulePicker({
}}
className="h-4 w-4 shrink-0 cursor-pointer accent-[color:var(--primary)]"
/>
<span className="text-[13px] font-medium text-foreground">Post immediately</span>
<span className="text-[13px] font-medium text-foreground">{t("approval.schedulePostNow")}</span>
</label>
<label className="flex cursor-pointer items-center gap-2">
<input
@@ -53,7 +55,7 @@ export function SchedulePicker({
onChange={() => onModeChange("scheduled")}
className="h-4 w-4 shrink-0 cursor-pointer accent-[color:var(--primary)]"
/>
<span className="text-[13px] font-medium text-foreground">Schedule for later</span>
<span className="text-[13px] font-medium text-foreground">{t("approval.scheduleLater")}</span>
</label>
</div>
{mode === "scheduled" ? (