This commit is contained in:
ZacharyZhang-NY committed 2026-03-19 16:04:35 -04:00
1 parent da1fec8f2a
commit b28ed38b8d
116 files changed
+273040 -723

No files matched your search

+171 -95
View File
@@ -5,6 +5,7 @@ import {
CheckCircle,
Cpu,
FolderOpen,
GlobeSimple,
Lightning,
Rocket,
Target,
@@ -13,23 +14,33 @@ import {
} from "@phosphor-icons/react";
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
import type { OnboardingBootstrapResult, ProfileSnapshot } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { APP_LOCALES, LOCALE_LABELS } from "@shared/locale";
import { useT } from "../i18n";
import { unwrap } from "../lib/desktop";
import { ActionButton, Input, Select, TextArea } from "./ui";
type Step = "company" | "mission" | "launch";
type Step = "language" | "company" | "mission" | "launch";
export function CompanyOnboarding({
connectors,
onDone,
canCancel = false,
onCancel,
currentLocale,
onLocaleChange,
isFirstLaunch = false,
}: {
connectors: ProfileSnapshot["connectors"];
onDone: (summary: OnboardingBootstrapResult) => Promise<void>;
canCancel?: boolean;
onCancel?: () => void;
currentLocale: AppLocale;
onLocaleChange: (locale: AppLocale) => void;
isFirstLaunch?: boolean;
}) {
const [step, setStep] = useState<Step>("company");
const t = useT();
const [step, setStep] = useState<Step>(isFirstLaunch ? "language" : "company");
const [companyName, setCompanyName] = useState("");
const [companyDescription, setCompanyDescription] = useState("");
const [workspacePath, setWorkspacePath] = useState("");
@@ -60,11 +71,11 @@ export function CompanyOnboarding({
}, [executionReadyConnectors, selectedConnectorId]);
const stepLabels = [
{ label: "Company", icon: Buildings },
{ label: "Mission", icon: Target },
{ label: "Launch", icon: Rocket },
{ label: t("onboarding.stepLabel1"), icon: Buildings },
{ label: t("onboarding.stepLabel2"), icon: Target },
{ label: t("onboarding.stepLabel3"), icon: Rocket },
] as const;
const stepIndex = step === "company" ? 0 : step === "mission" ? 1 : 2;
const stepIndex = step === "language" ? -1 : step === "company" ? 0 : step === "mission" ? 1 : 2;
const pickDirectory = useCallback(async () => {
setError(null);
@@ -103,21 +114,21 @@ export function CompanyOnboarding({
const continueToMission = useCallback(async () => {
if (loading) return;
if (!companyName.trim()) {
setError("Enter a company name.");
setError(t("onboarding.enterCompanyName"));
return;
}
if (!selectedConnector) {
setError("At least one execution-ready connector is required before you can continue.");
setError(t("onboarding.connectorRequired"));
return;
}
setError(null);
setStep("mission");
}, [companyName, loading, selectedConnector]);
}, [companyName, loading, selectedConnector, t]);
const launchCompany = useCallback(async () => {
if (loading) return;
if (!goalTitle.trim()) {
setError("Describe the first real company goal.");
setError(t("onboarding.describeFirstGoal"));
return;
}
setLoading(true);
@@ -125,11 +136,11 @@ export function CompanyOnboarding({
try {
await bootstrap();
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to bootstrap the company.");
setError(submitError instanceof Error ? submitError.message : t("onboarding.unableToBootstrap"));
} finally {
setLoading(false);
}
}, [bootstrap, goalTitle, loading]);
}, [bootstrap, goalTitle, loading, t]);
const finishLaunch = useCallback(async () => {
if (!launchSummary) return;
@@ -138,11 +149,11 @@ export function CompanyOnboarding({
try {
await onDone(launchSummary);
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to open the control center.");
setError(submitError instanceof Error ? submitError.message : t("onboarding.unableToOpenControlCenter"));
} finally {
setLoading(false);
}
}, [launchSummary, onDone]);
}, [launchSummary, onDone, t]);
return (
<div className="grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
@@ -154,52 +165,56 @@ export function CompanyOnboarding({
className="mx-auto w-full max-w-[460px]"
>
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
AgentCompany Desktop
{t("onboarding.subtitle")}
</div>
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
Start a real local-first
<br />
agent company.
{t("onboarding.title").split("\n").map((line, i) => (
<span key={i}>{line}{i === 0 ? <br /> : null}</span>
))}
</h1>
<p className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]">
This flow creates one company, binds one execution-ready connector, links an optional workspace, writes
the first goal, creates the operating project, and queues the first execution task.
{t("onboarding.description")}
</p>
<div className="mt-10 flex items-center gap-3">
{stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2">
{index > 0 ? (
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
) : null}
<div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
index < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: index === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}
>
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
{entry.label}
{step !== "language" ? (
<div className="mt-10 flex items-center gap-3">
{stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2">
{index > 0 ? (
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
) : null}
<div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
index < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: index === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}
>
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
{entry.label}
</div>
</div>
</div>
))}
</div>
))}
</div>
) : null}
<div className="mt-10">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Connector readiness
{t("onboarding.connectorReadiness")}
</div>
<div className="space-y-2.5">
{connectors.map((connector) => {
const issue = getConnectorExecutionReadinessIssue(connector);
const isRunnable = !issue;
const isChecking = connector.status === "detected";
const isReady = connector.status === "ready";
const isMissing = connector.status === "not_installed";
const issue = !isChecking ? getConnectorExecutionReadinessIssue(connector) : null;
return (
<div key={connector.id} className="flex items-start gap-2.5 text-[13px]">
{isRunnable ? (
{isChecking ? (
<span className="mt-0.5 h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-[color:var(--muted)] border-t-[color:var(--accent)]" />
) : isReady ? (
<CheckCircle size={16} weight="fill" className="shrink-0 text-[color:var(--success)]" />
) : isMissing ? (
<XCircle size={16} weight="fill" className="shrink-0 text-[color:var(--muted)]" />
@@ -208,11 +223,11 @@ export function CompanyOnboarding({
)}
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={isRunnable ? "font-medium text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
<span className={isReady ? "font-medium text-[color:var(--text)]" : isChecking ? "text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
{connector.label}
</span>
<span className="text-[11px] text-[color:var(--muted)]">
{connector.status.replaceAll("_", " ")}
{isChecking ? (currentLocale === "zh" ? "检测中..." : "checking...") : connector.status.replaceAll("_", " ")}
</span>
</div>
{issue ? (
@@ -227,7 +242,7 @@ export function CompanyOnboarding({
</div>
{!hasRunnableConnector ? (
<div className="mt-4 rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
Install, authenticate, and fully validate at least one supported CLI runtime before creating the company.
{t("onboarding.connectorWarning")}
</div>
) : null}
</div>
@@ -242,11 +257,68 @@ export function CompanyOnboarding({
onClick={onCancel}
className="absolute right-6 top-6 text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
Cancel
{t("onboarding.cancel")}
</button>
) : null}
<AnimatePresence mode="wait">
{step === "language" ? (
<motion.div
key="language"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<GlobeSimple size={12} />
{t("onboarding.languageSelection")} / Language
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
{t("onboarding.selectLanguage")} / Select Language
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-2.5">
{APP_LOCALES.map((locale) => (
<button
key={locale}
type="button"
onClick={() => onLocaleChange(locale)}
className={`flex w-full items-center gap-3 rounded-[10px] border px-4 py-3.5 text-left transition ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]"
: "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"
}`}
>
<div className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent)]"
: "border-[color:var(--line)]"
}`}>
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-white" /> : null}
</div>
<span className={`text-[14px] font-medium ${
currentLocale === locale ? "text-[color:var(--text)]" : "text-[color:var(--muted-strong)]"
}`}>
{LOCALE_LABELS[locale]}
</span>
</button>
))}
</div>
<div className="mt-8 flex items-center justify-end">
<ActionButton
label={t("onboarding.continue")}
onClick={() => setStep("company")}
icon={GlobeSimple}
tone="accent"
/>
</div>
</motion.div>
) : null}
{step === "company" ? (
<motion.div
key="company"
@@ -257,29 +329,29 @@ export function CompanyOnboarding({
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Step 1 of 3
{t("onboarding.stepLabel1")} 1 / 3
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
Create the operating boundary
{t("onboarding.step1Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input
label="Company name"
label={t("onboarding.companyName")}
value={companyName}
onChange={setCompanyName}
placeholder="e.g. Northline Systems"
placeholder={t("onboarding.companyNamePlaceholder")}
/>
<TextArea
label="Company description"
label={t("onboarding.companyDescription")}
value={companyDescription}
onChange={setCompanyDescription}
placeholder="Optional context for the company operating model."
placeholder={t("onboarding.companyDescriptionPlaceholder")}
rows={3}
/>
<Select
label="Primary connector"
label={t("onboarding.primaryConnector")}
value={selectedConnector?.id ?? ""}
onChange={setSelectedConnectorId}
options={executionReadyConnectors.map((connector) => ({
@@ -288,28 +360,27 @@ export function CompanyOnboarding({
}))}
/>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
Only connectors in `ready` state with session resume and structured transcripts are offered here.
Re-run connector health checks from Settings if the list is empty.
{t("onboarding.connectorInfo")}
</div>
<div>
<label
htmlFor="onboarding-workspace-path"
className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]"
>
Workspace folder <span className="font-normal text-[color:var(--muted)]">(optional)</span>
{t("onboarding.workspaceFolder")} <span className="font-normal text-[color:var(--muted)]">{t("onboarding.workspaceFolderOptional")}</span>
</label>
<div className="flex gap-3">
<Input
id="onboarding-workspace-path"
ariaLabel="Workspace folder"
ariaLabel={t("onboarding.workspaceFolder")}
value={workspacePath}
onChange={setWorkspacePath}
placeholder="/Users/you/project"
placeholder={t("onboarding.workspaceFolderPlaceholder")}
/>
<ActionButton label="Choose" onClick={() => void pickDirectory()} icon={FolderOpen} />
<ActionButton label={currentLocale === "zh" ? "选择" : "Choose"} onClick={() => void pickDirectory()} icon={FolderOpen} />
</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
If set, the first execution task will be bound to this path and the path will be registered as the primary workspace.
{t("onboarding.workspaceHelp")}
</div>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
@@ -321,9 +392,9 @@ export function CompanyOnboarding({
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
/>
<div>
<div className="text-[13px] font-medium text-[color:var(--text)]">Auto-approve hire and strategy approvals</div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{t("onboarding.autoApproveLabel")}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
Useful for single-operator flows. Leave it off if you want the board to gate organization changes explicitly.
{t("onboarding.autoApproveHelp")}
</div>
</div>
</label>
@@ -336,9 +407,18 @@ export function CompanyOnboarding({
</div>
) : null}
<div className="mt-8 flex items-center justify-end">
<div className="mt-8 flex items-center justify-between">
{isFirstLaunch ? (
<button
type="button"
onClick={() => setStep("language")}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
{t("onboarding.back")}
</button>
) : <div />}
<ActionButton
label="Continue"
label={t("onboarding.continue")}
onClick={() => void continueToMission()}
icon={Buildings}
tone="accent"
@@ -358,39 +438,36 @@ export function CompanyOnboarding({
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Step 2 of 3
{t("onboarding.stepLabel2")} 2 / 3
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
Define the first real goal
</div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">
The first goal seeds the operating project and the first task. This should be specific enough that the CEO agent can decompose it into concrete work.
{t("onboarding.step2Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input
label="Goal"
label={t("onboarding.goal")}
value={goalTitle}
onChange={setGoalTitle}
placeholder="e.g. Ship the first customer-ready desktop demo"
placeholder={t("onboarding.goalPlaceholder")}
/>
<TextArea
label="Acceptance criteria"
label={t("onboarding.acceptanceCriteria")}
value={goalDescription}
onChange={setGoalDescription}
placeholder="Describe what successful execution looks like, what cannot be skipped, and any delivery constraints."
placeholder={t("onboarding.acceptanceCriteriaPlaceholder")}
rows={5}
/>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] text-[color:var(--muted)]">
<div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">
<Cpu size={12} />
Execution handoff
{t("onboarding.executionHandoff")}
</div>
<div className="space-y-2">
<div>The selected connector becomes the CEO agent runtime for the first launch.</div>
<div>The app will create the company, the primary workspace, the CEO agent, the kickoff project, and the first task.</div>
<div>If the worker is already ready, the first run will be queued immediately; otherwise the scheduler will pick it up once the runtime is available.</div>
<div>{t("onboarding.executionHandoffLine1")}</div>
<div>{t("onboarding.executionHandoffLine2")}</div>
<div>{t("onboarding.executionHandoffLine3")}</div>
</div>
</div>
</div>
@@ -407,10 +484,10 @@ export function CompanyOnboarding({
onClick={() => setStep("company")}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
Back
{t("onboarding.back")}
</button>
<ActionButton
label={loading ? "Launching..." : "Launch Company"}
label={loading ? t("onboarding.launching") : t("onboarding.launchCompany")}
onClick={() => void launchCompany()}
icon={Lightning}
tone="accent"
@@ -433,42 +510,41 @@ export function CompanyOnboarding({
<CheckCircle size={24} weight="fill" />
</div>
<h2 className="mt-5 text-[28px] tracking-[-0.03em] text-[color:var(--text)]">
{launchSummary.companyName} is live.
{launchSummary.companyName} {t("onboarding.isLive")}
</h2>
<p className="mt-3 text-[14px] leading-relaxed text-[color:var(--muted)]">
The company shell is created. The first project and task are registered, and the kickoff execution is
{launchSummary.runId ? " queued." : " prepared for deferred execution once the runtime is ready."}
{launchSummary.runId ? t("onboarding.launchDescription") : t("onboarding.launchDescriptionDeferred")}
</p>
<div className="mt-6 grid gap-3 md:grid-cols-2">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Connector</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.connector")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{selectedConnector?.label ?? "Unavailable"}
{selectedConnector?.label ?? t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Lead agent</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.leadAgent")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.leadAgentId ? "CEO agent created" : "Lead agent unavailable"}
{launchSummary.leadAgentId ? t("onboarding.ceoAgentCreated") : t("onboarding.leadAgentUnavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Primary workspace</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.primaryWorkspace")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.workspaceId ? "Registered" : "Not bound"}
{launchSummary.workspaceId ? t("onboarding.registered") : t("onboarding.notBound")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Kickoff project</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.kickoffProject")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.projectId ? "Created" : "Unavailable"}
{launchSummary.projectId ? t("onboarding.created") : t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Initial run</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.initialRun")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.runId ? "Queued in orchestration" : "Deferred until runtime is ready"}
{launchSummary.runId ? t("onboarding.queuedInOrchestration") : t("onboarding.deferredUntilReady")}
</div>
</div>
</div>
@@ -481,7 +557,7 @@ export function CompanyOnboarding({
<div className="mt-8 flex items-center justify-end">
<ActionButton
label={loading ? "Opening..." : "Open Control Center"}
label={loading ? t("onboarding.opening") : t("onboarding.openControlCenter")}
onClick={() => void finishLaunch()}
icon={Rocket}
tone="accent"
+3 -1
View File
@@ -29,6 +29,7 @@ import type {
import { timeAgo } from "../lib/formatters"
import { DEPARTMENT_OPTIONS } from "@shared/constants"
import { ActionButton, Select } from "./ui"
import { useT } from "../i18n"
const ACCENT = "var(--accent)"
const LINE = "var(--line)"
@@ -1611,6 +1612,7 @@ export function AutomationPanel({
companyId,
onRefresh,
}: AutomationPanelProps) {
const t = useT()
const [activeSection, setActiveSection] = useState<"rules" | "workflows">("rules")
return (
@@ -1630,7 +1632,7 @@ export function AutomationPanel({
<Robot size={20} weight="fill" />
</div>
<div>
<div className="text-[13px] font-semibold text-[color:var(--text)]">Automation Engine</div>
<div className="text-[13px] font-semibold text-[color:var(--text)]">{t("auto.automationEngine")}</div>
<div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">
Rules trigger actions based on company events. Workflows chain multiple steps into pipelines.
Together, they let your AI company run fully autonomously -- responding to changes, routing work,
+47 -43
View File
@@ -34,6 +34,7 @@ import { getAgentInitials, formatMessageTime } from "../lib/formatters"
import { DEPARTMENT_COLORS } from "@shared/constants"
import { unwrap } from "../lib/desktop"
import { InlineNotice, Modal, Select, Input, TextArea } from "./ui"
import { useT } from "../i18n"
const TERRACOTTA = "#d97745"
const TERRACOTTA_SOFT = "#d9774518"
@@ -815,6 +816,7 @@ interface ComposeModalProps {
}
function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: ComposeModalProps) {
const t = useT()
const [fromAgentId, setFromAgentId] = useState(agents[0]?.id ?? "")
const [toAgentId, setToAgentId] = useState("")
const [channel, setChannel] = useState<MessageChannel>("company")
@@ -893,11 +895,11 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
}, [agents, body, channel, channelTargetId, companyId, fromAgentId, onClose, onSent, priority, subject, toAgentId])
return (
<Modal open={open} title="Compose Message" onClose={onClose} width="560px">
<Modal open={open} title={t("comm.composeMessage")} onClose={onClose} width="560px">
<div className="space-y-4">
{sendError ? <InlineNotice message={sendError} /> : null}
<Select
label="From Agent"
label={t("comm.fromAgent")}
value={fromAgentId}
onChange={setFromAgentId}
options={agents.map((a) => ({
@@ -907,15 +909,15 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
/>
<Select
label="Channel"
label={t("comm.channel")}
value={channel}
onChange={(v) => setChannel(v as MessageChannel)}
options={[
{ value: "company", label: "Company-wide broadcast" },
{ value: "department", label: "Department channel" },
{ value: "project", label: "Project channel" },
{ value: "incident", label: "Incident channel" },
{ value: "direct", label: "Direct message" },
{ value: "company", label: t("comm.companyBroadcast") },
{ value: "department", label: t("comm.departmentChannel") },
{ value: "project", label: t("comm.projectChannel") },
{ value: "incident", label: t("comm.incidentChannel") },
{ value: "direct", label: t("comm.directMessage") },
]}
/>
@@ -963,17 +965,17 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
) : null}
<Input
label="Subject"
label={t("comm.subject")}
value={subject}
onChange={setSubject}
placeholder="Message subject"
placeholder={t("comm.messageSubject")}
/>
<TextArea
label="Message"
label={t("comm.message")}
value={body}
onChange={setBody}
placeholder="Write your message..."
placeholder={t("comm.writeMessage")}
rows={5}
/>
@@ -994,7 +996,7 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
onClick={onClose}
className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]"
>
Cancel
{t("common.cancel")}
</button>
<button
type="button"
@@ -1018,7 +1020,7 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
) : (
<PaperPlaneTilt size={14} weight="fill" />
)}
Send Message
{t("comm.sendMessage")}
</button>
</div>
</div>
@@ -1040,6 +1042,7 @@ function FilterBar({
filters: FilterState
onChangeFilters: (next: FilterState) => void
}) {
const t = useT()
const [expanded, setExpanded] = useState(false)
return (
@@ -1057,7 +1060,7 @@ function FilterBar({
onChange={(e) =>
onChangeFilters({ ...filters, search: e.target.value })
}
placeholder="Search messages..."
placeholder={t("comm.searchMessages")}
className="w-full rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] py-1.5 pl-8 pr-3 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-1"
style={{ focusRingColor: TERRACOTTA } as React.CSSProperties}
/>
@@ -1079,7 +1082,7 @@ function FilterBar({
}
>
<Funnel size={12} />
Filters
{t("comm.filters")}
</button>
</div>
@@ -1102,12 +1105,12 @@ function FilterBar({
}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
>
<option value="all">All channels</option>
<option value="direct">Direct</option>
<option value="department">Department</option>
<option value="company">Company</option>
<option value="project">Project</option>
<option value="incident">Incident</option>
<option value="all">{t("comm.allChannels")}</option>
<option value="direct">{t("comm.direct")}</option>
<option value="department">{t("comm.department")}</option>
<option value="company">{t("comm.company")}</option>
<option value="project">{t("nav.projects")}</option>
<option value="incident">{t("comm.incident")}</option>
</select>
<select
@@ -1120,10 +1123,10 @@ function FilterBar({
}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
>
<option value="all">All priorities</option>
<option value="urgent">Urgent</option>
<option value="normal">Normal</option>
<option value="low">Low</option>
<option value="all">{t("comm.allPriorities")}</option>
<option value="urgent">{t("comm.urgent")}</option>
<option value="normal">{t("comm.normal")}</option>
<option value="low">{t("comm.low")}</option>
</select>
<button
@@ -1138,7 +1141,7 @@ function FilterBar({
}`}
style={filters.unreadOnly ? { backgroundColor: TERRACOTTA } : undefined}
>
Unread only
{t("comm.unreadOnly")}
</button>
{(filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly) ? (
@@ -1154,7 +1157,7 @@ function FilterBar({
}
className="text-[10px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
>
Clear filters
{t("comm.clearFilters")}
</button>
) : null}
</motion.div>
@@ -1189,6 +1192,7 @@ export function CommunicationHub({
onNavigateToTask,
onRefresh,
}: CommunicationHubProps) {
const t = useT()
const [selectedChannelKey, setSelectedChannelKey] = useState<string | null>(null)
const [showSidebar, setShowSidebar] = useState(true)
const [showCompose, setShowCompose] = useState(false)
@@ -1447,7 +1451,7 @@ export function CommunicationHub({
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">
Communication Hub
{t("comm.communicationHub")}
</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{messages.length} message{messages.length !== 1 ? "s" : ""}
@@ -1464,7 +1468,7 @@ export function CommunicationHub({
style={{ backgroundColor: TERRACOTTA }}
>
<Plus size={13} weight="bold" />
New message
{t("comm.newMessage")}
</button>
</div>
@@ -1486,7 +1490,7 @@ export function CommunicationHub({
{/* Sidebar header */}
<div className="flex items-center justify-between border-b border-[color:var(--line)] px-4 py-3">
<span className="text-[13px] font-semibold text-[color:var(--text)]">
Channels
{t("comm.channels")}
</span>
<button
type="button"
@@ -1527,7 +1531,7 @@ export function CommunicationHub({
{taskDiscussionChannels.length > 0 ? (
<div className="mb-3">
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
Task Discussions
{t("comm.taskDiscussions")}
</div>
{taskDiscussionChannels.map((discussion) => (
<button
@@ -1551,7 +1555,7 @@ export function CommunicationHub({
{channels.length === 0 ? (
<div className="px-3 py-6 text-center text-[11px] text-[color:var(--muted)]">
No active departments yet.
{t("comm.noActiveDepartments")}
</div>
) : null}
</div>
@@ -1622,7 +1626,7 @@ export function CommunicationHub({
</>
) : (
<span className="text-[13px] text-[color:var(--muted)]">
Select a channel
{t("comm.selectChannel")}
</span>
)}
@@ -1641,10 +1645,10 @@ export function CommunicationHub({
className="mb-3 text-[color:var(--muted)]"
/>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
Select a channel to view messages
{t("comm.selectChannelView")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
Monitor internal agent communications across departments
{t("comm.monitorComms")}
</div>
</div>
) : activeChannel.kind === "task" ? (
@@ -1652,10 +1656,10 @@ export function CommunicationHub({
<div className="flex h-full flex-col items-center justify-center text-center">
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
No discussion yet
{t("comm.noDiscussionYet")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
Comments for this task will appear here as agents and the board coordinate.
{t("comm.commentsAppearHere")}
</div>
</div>
) : (
@@ -1675,13 +1679,13 @@ export function CommunicationHub({
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
{filters.search || filters.unreadOnly || filters.priority !== "all"
? "No messages match your filters"
: "No messages in this channel yet"}
? t("comm.noMatchFilters")
: t("comm.noMessagesChannel")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
{filters.search || filters.unreadOnly || filters.priority !== "all"
? "Try adjusting your search or filters"
: "Messages will appear here as agents work and communicate"}
? t("comm.adjustFilters")
: t("comm.messagesAppear")}
</div>
</div>
) : (
@@ -1746,7 +1750,7 @@ export function CommunicationHub({
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-2.5">
<div className="flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
<ChatCircle size={14} />
<span>Select a channel to read and send messages.</span>
<span>{t("comm.selectChannelSend")}</span>
</div>
</div>
)}
@@ -19,6 +19,7 @@ import type {
SprintRecord,
} from "@shared/types";
import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, Modal, Select, StatusPill, TextArea } from "./ui";
import { useT } from "../i18n";
const documentTypeOptions = [
"prd",
@@ -450,13 +451,14 @@ export function DocumentCenter({
goals: GoalRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeDocument = useMemo(() => documents.find((document) => document.id === editingId) ?? null, [documents, editingId]);
return (
<div>
<SectionHeader title="Document Center" count={documents.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.documentCenter")} count={documents.length} action={() => setEditingId("__new__")} />
<ListFrame>
{documents.map((document) => (
<ListRow key={document.id}>
@@ -521,13 +523,14 @@ export function KnowledgeBaseManager({
entries: KnowledgeEntryRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeEntry = useMemo(() => entries.find((entry) => entry.id === editingId) ?? null, [entries, editingId]);
return (
<div>
<SectionHeader title="Knowledge Base" count={entries.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.knowledgeBase")} count={entries.length} action={() => setEditingId("__new__")} />
<ListFrame>
{entries.map((entry) => (
<ListRow key={entry.id}>
@@ -591,13 +594,14 @@ export function MeetingsManager({
agents: AgentRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeMeeting = useMemo(() => meetings.find((meeting) => meeting.id === editingId) ?? null, [meetings, editingId]);
return (
<div>
<SectionHeader title="Meetings" count={meetings.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.meetings")} count={meetings.length} action={() => setEditingId("__new__")} />
<ListFrame>
{meetings.map((meeting) => (
<ListRow key={meeting.id}>
@@ -660,13 +664,14 @@ export function SprintManager({
sprints: SprintRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeSprint = useMemo(() => sprints.find((sprint) => sprint.id === editingId) ?? null, [sprints, editingId]);
return (
<div>
<SectionHeader title="Sprint Board" count={sprints.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.sprintBoard")} count={sprints.length} action={() => setEditingId("__new__")} />
<ListFrame>
{sprints.map((sprint) => (
<ListRow key={sprint.id}>
+5 -3
View File
@@ -3,6 +3,7 @@ import { CheckCircle, SignIn } from "@phosphor-icons/react";
import type { ConnectorRecord } from "@shared/types";
import { unwrap } from "../lib/desktop";
import { ActionButton, InlineNotice, Input, Select, StatusPill, TextArea } from "./ui";
import { useT } from "../i18n";
function getAuthActionCopy(connector: ConnectorRecord) {
switch (connector.id) {
@@ -40,6 +41,7 @@ function getAuthActionCopy(connector: ConnectorRecord) {
}
export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRecord; onSaved: () => Promise<void> }) {
const t = useT();
const [model, setModel] = useState(connector.model ?? "");
const [envBindingText, setEnvBindingText] = useState(connector.envBindingText);
const [notes, setNotes] = useState(connector.notes);
@@ -173,14 +175,14 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
) : null}
<div className="grid gap-4 md:grid-cols-2">
<div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">Status</div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("conn.status")}</div>
<div className="flex items-center gap-3">
<StatusPill status={connector.status} />
<span className="text-[12px] text-[color:var(--muted)]">{connector.version ?? "version unknown"}</span>
</div>
</div>
<div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">Capability matrix</div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("conn.capabilityMatrix")}</div>
<div className="grid grid-cols-2 gap-2">
{Object.entries(connector.capabilityMatrix).map(([key, value]) => (
<div key={key} className={`rounded-[6px] border px-3 py-2 text-[12px] ${value ? "border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] text-[color:var(--accent)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)] text-[color:var(--muted)]"}`}>
@@ -233,7 +235,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
) : null}
<div className="flex gap-3">
<ActionButton
label="Save connector"
label={t("conn.saveConnector")}
tone="accent"
onClick={() => void (async () => {
setSaving(true);
+25 -23
View File
@@ -42,6 +42,7 @@ import type {
import { isConnectorExecutionReady } from "@shared/connector-policy";
import { formatMoney, getAgentInitials, timeAgo } from "../lib/formatters";
import { StatusPill } from "./ui";
import { useT } from "../i18n";
interface DashboardProps {
agents: AgentRecord[];
@@ -589,6 +590,7 @@ function GoalProgress({ goals, tasks, onNavigate }: { goals: GoalRecord[]; tasks
}
export function Dashboard(props: DashboardProps) {
const t = useT();
const { agents, tasks, runs, approvals, goals, projects, activity, costs, connectors, messages = [], browserActions = [], workflows = [], sprints = [], onNavigate } = props;
const healthScore = useMemo(
@@ -638,51 +640,51 @@ export function Dashboard(props: DashboardProps) {
{/* Metric Cards */}
<div className="grid flex-1 grid-cols-2 gap-2 lg:grid-cols-3">
<MetricCard
label="Live Runs"
label={t("dash.liveRuns")}
value={liveRuns.length}
icon={Heartbeat}
tone={liveRuns.length > 0 ? "success" : "default"}
onClick={() => onNavigate("runs")}
subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} running` : "All quiet"}
subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} ${t("dash.running")}` : t("dash.allQuiet")}
/>
<MetricCard
label="Active Tasks"
label={t("dash.activeTasks")}
value={activeTasks.length}
icon={Briefcase}
tone={activeTasks.length > 0 ? "accent" : "default"}
onClick={() => onNavigate("tasks")}
subtitle={`${tasks.filter((t) => t.status === "todo").length} queued`}
subtitle={`${tasks.filter((t) => t.status === "todo").length} ${t("dash.queued")}`}
/>
<MetricCard
label="Approvals"
label={t("dash.approvals")}
value={pendingApprovals.length}
icon={ShieldCheck}
tone={pendingApprovals.length > 0 ? "warn" : "default"}
onClick={() => onNavigate("approvals")}
subtitle={pendingApprovals.length > 0 ? "Needs your attention" : "All clear"}
subtitle={pendingApprovals.length > 0 ? t("dash.needsAttention") : t("dash.allClear")}
/>
<MetricCard
label="Failed Runs"
label={t("dash.failedRuns")}
value={failedRuns.length}
icon={Warning}
tone={failedRuns.length > 0 ? "danger" : "default"}
onClick={() => onNavigate("runs")}
/>
<MetricCard
label="Total Spend"
label={t("dash.totalSpend")}
value={formatMoney(totalSpend)}
icon={CurrencyCircleDollar}
onClick={() => onNavigate("costs")}
/>
<MetricCard
label="Projects"
label={t("dash.projects")}
value={projects.filter((p) => p.status === "in_progress").length}
icon={Target}
onClick={() => onNavigate("projects")}
subtitle={`${projects.length} total`}
subtitle={`${projects.length} ${t("dash.total")}`}
/>
<MetricCard
label="Unread Messages"
label={t("dash.unreadMessages")}
value={unreadMessages.length}
icon={Users}
tone={urgentMessages.length > 0 ? "warn" : unreadMessages.length > 0 ? "accent" : "default"}
@@ -690,7 +692,7 @@ export function Dashboard(props: DashboardProps) {
subtitle={urgentMessages.length > 0 ? `${urgentMessages.length} urgent` : unreadMessages.length > 0 ? "Needs triage" : "All read"}
/>
<MetricCard
label="Browser Ops"
label={t("dash.browserOps")}
value={blockedBrowserActions.length}
icon={Cpu}
tone={blockedBrowserActions.length > 0 ? "warn" : "default"}
@@ -698,7 +700,7 @@ export function Dashboard(props: DashboardProps) {
subtitle={blockedBrowserActions.length > 0 ? "Blocked or failed" : "Nominal"}
/>
<MetricCard
label="Workflow Stalls"
label={t("dash.workflowStalls")}
value={stalledWorkflows.length}
icon={Lightning}
tone={stalledWorkflows.length > 0 ? "warn" : "default"}
@@ -711,13 +713,13 @@ export function Dashboard(props: DashboardProps) {
{/* Department Overview */}
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Departments</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.departments")}</div>
<button
type="button"
onClick={() => onNavigate("orgchart")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
Org chart <TreeStructure size={12} />
{t("dash.orgChart")} <TreeStructure size={12} />
</button>
</div>
<DepartmentOverview agents={agents} tasks={tasks} runs={runs} onNavigate={onNavigate} />
@@ -728,7 +730,7 @@ export function Dashboard(props: DashboardProps) {
{activeSprint ? (
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Current Sprint</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.currentSprint")}</div>
<button
type="button"
onClick={() => onNavigate("sprints")}
@@ -743,7 +745,7 @@ export function Dashboard(props: DashboardProps) {
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Goal Progress</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.goalProgress")}</div>
<button
type="button"
onClick={() => onNavigate("goals")}
@@ -759,7 +761,7 @@ export function Dashboard(props: DashboardProps) {
{/* Task Pipeline */}
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Task Pipeline</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.taskPipeline")}</div>
<button
type="button"
onClick={() => onNavigate("tasks")}
@@ -778,13 +780,13 @@ export function Dashboard(props: DashboardProps) {
{/* Workforce */}
<section className="lg:col-span-3">
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Workforce</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.workforce")}</div>
<button
type="button"
onClick={() => onNavigate("orgchart")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
Org chart <TreeStructure size={12} />
{t("dash.orgChart")} <TreeStructure size={12} />
</button>
</div>
<WorkforceGrid agents={agents} onSelectAgent={(id) => onNavigate("agents", id)} />
@@ -793,7 +795,7 @@ export function Dashboard(props: DashboardProps) {
{/* Activity Feed */}
<section className="lg:col-span-2">
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Recent Activity</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.recentActivity")}</div>
<button
type="button"
onClick={() => onNavigate("activity")}
@@ -810,7 +812,7 @@ export function Dashboard(props: DashboardProps) {
{/* Connector Health */}
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Infrastructure</div>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.infrastructure")}</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
{connectors.map((connector) => {
const isReady = isConnectorExecutionReady(connector);
@@ -842,7 +844,7 @@ export function Dashboard(props: DashboardProps) {
{/* Needs Attention */}
{(pendingApprovals.length > 0 || failedRuns.length > 0 || urgentMessages.length > 0 || blockedBrowserActions.length > 0 || stalledWorkflows.length > 0) ? (
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Needs Attention</div>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">{t("dash.needsAttentionSection")}</div>
<div className="space-y-1.5">
{pendingApprovals.slice(0, 5).map((approval) => {
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
+123 -100
View File
@@ -1,4 +1,4 @@
import { useEffect, useState, useMemo } from "react";
import { useCallback, useEffect, useState, useMemo } from "react";
import {
ArrowClockwise,
ArrowSquareOut,
@@ -39,6 +39,9 @@ import type {
TaskStatus,
WorkspaceRecord,
} from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { APP_LOCALES, LOCALE_LABELS } from "@shared/locale";
import { useLocale, useT } from "../i18n";
import { formatMoney, formatTime } from "../lib/formatters";
import { unwrap } from "../lib/desktop";
import { ActionButton, ConfirmDialog, EmptyState, Field, InlineNotice, Input, ListFrame, ListRow, Select, StatusPill, TextArea } from "./ui";
@@ -120,8 +123,9 @@ export function TaskQueue({
selectedTaskId: string | null;
onSelect: (taskId: string) => void;
}) {
const t = useT();
if (tasks.length === 0) {
return <EmptyState title="Task queue is empty" />;
return <EmptyState title={t("exec.taskQueueEmpty")} />;
}
return (
<ListFrame>
@@ -188,6 +192,7 @@ export function TaskInspector({
onRefresh: () => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const t = useT();
const [editing, setEditing] = useState(false);
const [editTitle, setEditTitle] = useState(task.title);
const [editDescription, setEditDescription] = useState(task.description);
@@ -313,8 +318,8 @@ export function TaskInspector({
<div className="space-y-5">
<ConfirmDialog
open={confirmDelete}
title="Delete task"
message={`Permanently delete "${task.title}"? This cannot be undone.`}
title={t("exec.deleteTask")}
message={`${t("exec.deleteTaskConfirm").replace("{0}", task.title)}`}
errorMessage={deleteError}
onConfirm={() => void handleDelete()}
onCancel={() => {
@@ -342,20 +347,20 @@ export function TaskInspector({
{editing ? (
<div className="grid gap-3 border-y border-[color:var(--line)] py-4 md:grid-cols-2">
<div className="md:col-span-2">
<Input label="Title" value={editTitle} onChange={setEditTitle} placeholder="Task title" />
<Input label={t("exec.title")} value={editTitle} onChange={setEditTitle} placeholder={t("exec.taskTitle")} />
</div>
<div className="md:col-span-2">
<TextArea label="Description" value={editDescription} onChange={setEditDescription} placeholder="Description" rows={4} />
<TextArea label={t("exec.description")} value={editDescription} onChange={setEditDescription} placeholder={t("exec.description")} rows={4} />
</div>
<Select label="Priority" value={editPriority} onChange={v => setEditPriority(v as TaskRecord["priority"])} options={["critical", "high", "medium", "low"].map(v => ({ value: v, label: v }))} />
<Select label="Status" value={editStatus} onChange={v => setEditStatus(v as TaskStatus)} options={["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"].map(v => ({ value: v, label: v.replaceAll("_", " ") }))} />
<Select label="Assignee" value={editAssignee} onChange={setEditAssignee} options={[{ value: "", label: "Unassigned" }, ...agents.map(a => ({ value: a.id, label: a.name }))]} />
<Select label="Workspace" value={editWorkspace} onChange={setEditWorkspace} options={[{ value: "", label: "No workspace" }, ...workspaces.map(w => ({ value: w.id, label: w.name }))]} />
<Select label="Project" value={editProject} onChange={setEditProject} options={[{ value: "", label: "No project" }, ...projects.map(p => ({ value: p.id, label: p.name }))]} />
<Select label="Goal" value={editGoal} onChange={setEditGoal} options={[{ value: "", label: "No goal" }, ...goals.map(g => ({ value: g.id, label: g.title }))]} />
<Select label={t("exec.priority")} value={editPriority} onChange={v => setEditPriority(v as TaskRecord["priority"])} options={["critical", "high", "medium", "low"].map(v => ({ value: v, label: v }))} />
<Select label={t("exec.status")} value={editStatus} onChange={v => setEditStatus(v as TaskStatus)} options={["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"].map(v => ({ value: v, label: v.replaceAll("_", " ") }))} />
<Select label={t("exec.assignee")} value={editAssignee} onChange={setEditAssignee} options={[{ value: "", label: t("exec.unassigned") }, ...agents.map(a => ({ value: a.id, label: a.name }))]} />
<Select label={t("exec.workspace")} value={editWorkspace} onChange={setEditWorkspace} options={[{ value: "", label: t("exec.noWorkspace") }, ...workspaces.map(w => ({ value: w.id, label: w.name }))]} />
<Select label={t("exec.project")} value={editProject} onChange={setEditProject} options={[{ value: "", label: t("exec.noProject") }, ...projects.map(p => ({ value: p.id, label: p.name }))]} />
<Select label={t("exec.goal")} value={editGoal} onChange={setEditGoal} options={[{ value: "", label: t("exec.noGoal") }, ...goals.map(g => ({ value: g.id, label: g.title }))]} />
<div className="md:col-span-2 flex gap-3">
<ActionButton label="Save" tone="accent" onClick={() => void saveChanges()} icon={CheckCircle} loading={savingChanges} />
<ActionButton label="Cancel" onClick={() => setEditing(false)} icon={XCircle} />
<ActionButton label={t("exec.save")} tone="accent" onClick={() => void saveChanges()} icon={CheckCircle} loading={savingChanges} />
<ActionButton label={t("exec.cancel")} onClick={() => setEditing(false)} icon={XCircle} />
</div>
</div>
) : (
@@ -389,16 +394,16 @@ export function TaskInspector({
<div className="rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3">
<div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--warn)]">
<ShieldCheck size={16} weight="fill" />
{task.status === "blocked" ? "Governance action required" : "Task is waiting on review"}
{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}
</div>
<div className="mt-1 text-[13px] leading-relaxed text-[color:var(--muted-strong)]">
{task.status === "blocked"
? "Create an approval so leadership can resolve the blocker, provide credentials, or authorize the next privileged action."
: "Create an approval entry if this task needs an explicit operator decision before it can move forward."}
? t("exec.blockedGovernance")
: t("exec.reviewGovernance")}
</div>
<div className="mt-3">
<ActionButton
label={task.status === "blocked" ? "Create unblock approval" : "Create review approval"}
label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")}
onClick={() => void handleCreateApproval()}
icon={ShieldCheck}
loading={creatingApproval}
@@ -410,23 +415,23 @@ export function TaskInspector({
{/* Contextual actions — only show what makes sense for current state */}
<div className="flex items-center gap-2 pt-1">
{activeRun ? (
<ActionButton label="Cancel run" onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" />
<ActionButton label={t("exec.cancelRun")} onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" />
) : (
<>
{["todo", "in_progress", "blocked"].includes(task.status) && (
<ActionButton label="Run" onClick={() => void onRun(task.id)} icon={Play} tone="accent" />
<ActionButton label={t("exec.run")} onClick={() => void onRun(task.id)} icon={Play} tone="accent" />
)}
{task.status === "in_review" && (
<ActionButton label="Done" onClick={() => void quickStatus("done")} icon={CheckCircle} tone="accent" loading={updatingStatus === "done"} />
<ActionButton label={t("exec.done")} onClick={() => void quickStatus("done")} icon={CheckCircle} tone="accent" loading={updatingStatus === "done"} />
)}
{task.status === "blocked" && (
<ActionButton label="Unblock" onClick={() => void quickStatus("in_progress")} icon={Play} loading={updatingStatus === "in_progress"} />
<ActionButton label={t("exec.unblock")} onClick={() => void quickStatus("in_progress")} icon={Play} loading={updatingStatus === "in_progress"} />
)}
</>
)}
<ActionButton label="Discussion" onClick={() => onOpenDiscussion(task.id)} icon={ArrowRight} />
<ActionButton label="Edit" onClick={startEdit} icon={Pencil} />
<ActionButton label="Delete" onClick={() => setConfirmDelete(true)} icon={Trash} tone="danger" />
<ActionButton label={t("exec.discussion")} onClick={() => onOpenDiscussion(task.id)} icon={ArrowRight} />
<ActionButton label={t("exec.edit")} onClick={startEdit} icon={Pencil} />
<ActionButton label={t("exec.delete")} onClick={() => setConfirmDelete(true)} icon={Trash} tone="danger" />
</div>
</>
)}
@@ -439,7 +444,7 @@ export function TaskInspector({
<div className="space-y-2">
<div className="flex items-center gap-2">
<ListChecks size={14} className="text-[color:var(--muted)]" />
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Subtasks ({subtasks.length})</span>
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.subtasks")} ({subtasks.length})</span>
<span className="text-[10px] text-[color:var(--success)]">
{subtasks.filter((t) => t.status === "done").length}/{subtasks.length} done
</span>
@@ -490,7 +495,7 @@ export function TaskInspector({
{/* Run history — only if there are runs */}
{runs.length > 0 && (
<div className="space-y-2">
<div className="text-[11px] font-medium uppercase tracking-[0.08em] text-[color:var(--muted)]">Run history</div>
<div className="text-[11px] font-medium uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runHistory")}</div>
<div className="space-y-2">
{runs.slice(0, 8).map((run) => {
const runAgent = agents.find((a) => a.id === run.agentId);
@@ -549,6 +554,7 @@ function TaskComments({
comments: CommentRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [newComment, setNewComment] = useState("");
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
@@ -586,11 +592,11 @@ function TaskComments({
<div className="space-y-3">
<div className="flex items-center gap-2">
<Robot size={16} className="text-[color:var(--muted)]" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">Communication</span>
<span className="text-[13px] font-semibold text-[color:var(--text)]">{t("exec.communication")}</span>
<span className="text-[11px] text-[color:var(--muted)]">({comments.length})</span>
</div>
{comments.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">No messages yet. Agents will post updates here during runs.</div>
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noMessagesYet")}</div>
) : (
<div className="max-h-[240px] space-y-2 overflow-y-auto rounded-lg border border-[color:var(--line)] p-3">
{comments.map((comment) => {
@@ -615,12 +621,12 @@ function TaskComments({
<div className="flex gap-2">
<input
className="flex-1 rounded-md border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-[color:var(--accent)] focus:outline-none"
placeholder="Send a message to the team..."
placeholder={t("exec.sendMessage")}
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); void handleSubmit(); } }}
/>
<ActionButton label="Send" tone="accent" onClick={() => void handleSubmit()} icon={ArrowRight} loading={submitting} />
<ActionButton label={t("exec.send")} tone="accent" onClick={() => void handleSubmit()} icon={ArrowRight} loading={submitting} />
</div>
</div>
);
@@ -642,6 +648,7 @@ export function TaskCreator({
workspaces: WorkspaceRecord[];
onSaved: () => Promise<void>;
}) {
const t = useT();
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [goalId, setGoalId] = useState(goals[0]?.id ?? "");
@@ -679,17 +686,17 @@ export function TaskCreator({
return (
<div className="grid gap-4 md:grid-cols-2">
<Input label="Title" value={title} onChange={setTitle} placeholder="Task title" />
<Select label="Priority" value={priority} onChange={setPriority} options={["critical", "high", "medium", "low"].map((value) => ({ value, label: value }))} />
<Input label={t("exec.title")} value={title} onChange={setTitle} placeholder={t("exec.taskTitle")} />
<Select label={t("exec.priority")} value={priority} onChange={setPriority} options={["critical", "high", "medium", "low"].map((value) => ({ value, label: value }))} />
<div className="md:col-span-2"><TextArea label="Description" value={description} onChange={setDescription} placeholder="What should the agent do, and what result should come back into the run summary?" rows={5} /></div>
<Select label="Goal" value={goalId} onChange={setGoalId} options={[{ value: "", label: "No goal" }, ...goals.map((goal) => ({ value: goal.id, label: goal.title }))]} />
<Select label="Project" value={projectId} onChange={setProjectId} options={[{ value: "", label: "No project" }, ...projects.map((project) => ({ value: project.id, label: project.name }))]} />
<Select label="Assignee" value={assigneeAgentId} onChange={setAssigneeAgentId} options={[{ value: "", label: "Unassigned" }, ...agents.map((agent) => ({ value: agent.id, label: agent.name }))]} />
<Select label="Workspace" value={workspaceId} onChange={setWorkspaceId} options={[{ value: "", label: "No workspace" }, ...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name }))]} />
<Select label={t("exec.goal")} value={goalId} onChange={setGoalId} options={[{ value: "", label: t("exec.noGoal") }, ...goals.map((goal) => ({ value: goal.id, label: goal.title }))]} />
<Select label={t("exec.project")} value={projectId} onChange={setProjectId} options={[{ value: "", label: t("exec.noProject") }, ...projects.map((project) => ({ value: project.id, label: project.name }))]} />
<Select label={t("exec.assignee")} value={assigneeAgentId} onChange={setAssigneeAgentId} options={[{ value: "", label: t("exec.unassigned") }, ...agents.map((agent) => ({ value: agent.id, label: agent.name }))]} />
<Select label={t("exec.workspace")} value={workspaceId} onChange={setWorkspaceId} options={[{ value: "", label: t("exec.noWorkspace") }, ...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name }))]} />
{submitError ? <div className="md:col-span-2"><InlineNotice message={submitError} /></div> : null}
<div className="md:col-span-2">
<ActionButton
label="Create task"
label={t("exec.createTask")}
tone="accent"
onClick={() => void handleCreateTask()}
icon={ListChecks}
@@ -702,9 +709,10 @@ export function TaskCreator({
export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agents: AgentRecord[] }) {
const t = useT();
const reviewTasks = tasks.filter((t) => t.status === "in_review");
if (reviewTasks.length === 0) {
return <EmptyState title="No agent reviews in progress" />;
return <EmptyState title={t("exec.noReviewsInProgress")} />;
}
return (
<ListFrame>
@@ -730,8 +738,9 @@ export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agen
export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { approvals: ApprovalRecord[]; selectedApprovalId: string | null; onSelect: (id: string) => void }) {
const t = useT();
if (approvals.length === 0) {
return <EmptyState title="No approvals" />;
return <EmptyState title={t("exec.noApprovals")} />;
}
return (
<ListFrame>
@@ -774,6 +783,7 @@ export function ApprovalInspector({
onDecide: (state: "approved" | "rejected" | "revision_requested", note?: string) => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const t = useT();
const [decisionNote, setDecisionNote] = useState("");
const [confirmDelete, setConfirmDelete] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
@@ -813,8 +823,8 @@ export function ApprovalInspector({
<div className="space-y-5">
<ConfirmDialog
open={confirmDelete}
title="Delete approval"
message={`Permanently delete this approval? This cannot be undone.`}
title={t("exec.deleteApproval")}
message={t("exec.deleteApprovalConfirm")}
errorMessage={actionError}
onConfirm={() => void handleDelete()}
onCancel={() => {
@@ -928,9 +938,9 @@ export function ApprovalInspector({
<div className="flex flex-wrap gap-3">
{!isDecided ? (
<>
<ActionButton label="Approve" tone="accent" onClick={() => void handleDecision("approved")} icon={CheckCircle} loading={acting === "approved"} />
<ActionButton label="Request revision" onClick={() => void handleDecision("revision_requested")} icon={ClockCounterClockwise} loading={acting === "revision_requested"} />
<ActionButton label="Reject" tone="danger" onClick={() => void handleDecision("rejected")} icon={XCircle} loading={acting === "rejected"} />
<ActionButton label={t("exec.approve")} tone="accent" onClick={() => void handleDecision("approved")} icon={CheckCircle} loading={acting === "approved"} />
<ActionButton label={t("exec.requestRevision")} onClick={() => void handleDecision("revision_requested")} icon={ClockCounterClockwise} loading={acting === "revision_requested"} />
<ActionButton label={t("exec.reject")} tone="danger" onClick={() => void handleDecision("rejected")} icon={XCircle} loading={acting === "rejected"} />
</>
) : null}
{isDecided ? (
@@ -949,6 +959,7 @@ export function ApprovalInspector({
export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["costs"]; agents: ProfileSnapshot["agents"]; runs: ProfileSnapshot["runs"] }) {
const t = useT();
const [connectorFilter, setConnectorFilter] = useState("all");
const connectorIds = useMemo(() => {
@@ -964,7 +975,7 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
const total = useMemo(() => filtered.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0), [filtered]);
if (costs.length === 0) {
return <EmptyState title="No cost data yet" />;
return <EmptyState title={t("exec.noCostData")} />;
}
return (
@@ -973,7 +984,7 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
<Select
value={connectorFilter}
onChange={setConnectorFilter}
options={[{ value: "all", label: "All connectors" }, ...connectorIds.map((id) => ({ value: id, label: id }))]}
options={[{ value: "all", label: t("exec.allConnectors") }, ...connectorIds.map((id) => ({ value: id, label: id }))]}
/>
</div>
<ListFrame>
@@ -1066,6 +1077,7 @@ export function BudgetSummary({ agents, costs }: { agents: ProfileSnapshot["agen
export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
const t = useT();
const [search, setSearch] = useState("");
const [entityFilter, setEntityFilter] = useState<string>("all");
const [visibleCount, setVisibleCount] = useState(100);
@@ -1090,7 +1102,7 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
const visible = filtered.slice(0, visibleCount);
if (activity.length === 0) {
return <EmptyState title="No activity yet" />;
return <EmptyState title={t("exec.noActivityYet")} />;
}
return (
<div className="space-y-3">
@@ -1103,18 +1115,18 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2.5 pl-8 pr-3.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search activity..."
placeholder={t("exec.searchActivity")}
/>
</div>
<Select
value={entityFilter}
onChange={setEntityFilter}
options={ENTITY_TYPE_OPTIONS.map((v) => ({ value: v, label: v === "all" ? "All types" : v }))}
options={ENTITY_TYPE_OPTIONS.map((v) => ({ value: v, label: v === "all" ? t("exec.allTypes") : v }))}
/>
</div>
{visible.length === 0 ? (
<EmptyState title="No matching activity" />
<EmptyState title={t("exec.noMatchingActivity")} />
) : (
<>
<ListFrame>
@@ -1167,6 +1179,7 @@ export function WorkspaceList({
onOpen: (path: string) => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const t = useT();
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const toDelete = workspaces.find((w) => w.id === confirmDeleteId) ?? null;
@@ -1183,14 +1196,14 @@ export function WorkspaceList({
}
if (workspaces.length === 0) {
return <EmptyState title="No workspaces yet" />;
return <EmptyState title={t("exec.noWorkspacesYet")} />;
}
return (
<>
<ConfirmDialog
open={confirmDeleteId !== null}
title="Delete workspace"
message={`Delete workspace "${toDelete?.name ?? ""}"? This only removes it from the company registry; files on disk are not affected.`}
title={t("exec.deleteWorkspace")}
message={t("exec.deleteWorkspaceConfirm").replace("{0}", toDelete?.name ?? "")}
errorMessage={deleteError}
onConfirm={() => confirmDeleteId && void handleDelete(confirmDeleteId)}
onCancel={() => {
@@ -1214,9 +1227,9 @@ export function WorkspaceList({
</div>
<div className="flex items-center gap-2">
{workspace.isPrimary ? <StatusPill status="ready" /> : null}
<ActionButton label="Inspect" onClick={() => onSelect(workspace.id)} icon={FolderOpen} />
<ActionButton label="Reveal" onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} />
<ActionButton label="Delete" onClick={() => setConfirmDeleteId(workspace.id)} icon={Trash} tone="danger" />
<ActionButton label={t("exec.inspect")} onClick={() => onSelect(workspace.id)} icon={FolderOpen} />
<ActionButton label={t("exec.reveal")} onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} />
<ActionButton label={t("exec.delete")} onClick={() => setConfirmDeleteId(workspace.id)} icon={Trash} tone="danger" />
</div>
</div>
<div className="text-[12px] text-[color:var(--muted)]">
@@ -1249,8 +1262,9 @@ export function WorkspaceInspector({
project: ProjectRecord | null;
onOpen: (path: string) => Promise<void>;
}) {
const t = useT();
if (!workspace) {
return <EmptyState title="Select a workspace" detail="Choose a workspace to inspect the agents, tasks, runs, and documents bound to it." />;
return <EmptyState title={t("exec.selectWorkspace")} detail={t("exec.selectWorkspaceDetail")} />;
}
const workspaceAgents = agents.filter((agent) => agent.workspaceId === workspace.id);
@@ -1274,34 +1288,34 @@ export function WorkspaceInspector({
{workspace.repoRef ? <span> · Ref: <span className="text-[color:var(--text)]">{workspace.repoRef}</span></span> : null}
</div>
</div>
<ActionButton label="Open folder" onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} />
<ActionButton label={t("exec.openFolder")} onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} />
</div>
</div>
<div className="grid gap-3 md:grid-cols-4">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">Agents</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.agents")}</div>
<div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{workspaceAgents.length}</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">Tasks</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.tasks")}</div>
<div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{workspaceTasks.length}</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">Runs</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runs")}</div>
<div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{workspaceRuns.length}</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">Docs</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.docs")}</div>
<div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{workspaceDocuments.length}</div>
</div>
</div>
<div className="grid gap-4 xl:grid-cols-3">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">Assigned Agents</div>
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.assignedAgents")}</div>
{workspaceAgents.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">No agents are currently bound to this workspace.</div>
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noAgentsBound")}</div>
) : (
<div className="space-y-2">
{workspaceAgents.map((agent) => (
@@ -1318,9 +1332,9 @@ export function WorkspaceInspector({
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">Open Tasks</div>
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.openTasks")}</div>
{workspaceTasks.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">No tasks target this workspace yet.</div>
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noTasksTarget")}</div>
) : (
<div className="space-y-2">
{workspaceTasks.slice(0, 6).map((task) => (
@@ -1337,9 +1351,9 @@ export function WorkspaceInspector({
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">Recent Runs</div>
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.recentRuns")}</div>
{workspaceRuns.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">No runs have executed in this workspace.</div>
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noRunsExecuted")}</div>
) : (
<div className="space-y-2">
{workspaceRuns.slice(0, 6).map((run) => (
@@ -1361,6 +1375,7 @@ export function WorkspaceInspector({
export function WorkspaceCreator({ companyId, projects, onSaved }: { companyId: string; projects: ProjectRecord[]; onSaved: () => Promise<void> }) {
const t = useT();
const [name, setName] = useState("");
const [localPath, setLocalPath] = useState("");
const [repoUrl, setRepoUrl] = useState("");
@@ -1400,23 +1415,23 @@ export function WorkspaceCreator({ companyId, projects, onSaved }: { companyId:
return (
<div className="grid gap-4">
<Input label="Workspace name" value={name} onChange={setName} placeholder="Workspace name" />
<Field label="Local path">
<Input label={t("exec.workspaceName")} value={name} onChange={setName} placeholder={t("exec.workspaceName")} />
<Field label={t("exec.localPath")}>
<div className="flex gap-3">
<Input value={localPath} onChange={setLocalPath} placeholder="/Users/you/project" />
<ActionButton
label="Choose"
label={t("exec.choose")}
onClick={() => void handleChooseDirectory()}
icon={FolderOpen}
/>
</div>
</Field>
<Input label="Repository URL" value={repoUrl} onChange={setRepoUrl} placeholder="https://github.com/org/repo" />
<Input label="Branch / ref" value={repoRef} onChange={setRepoRef} placeholder="main" />
<Select label="Project" value={projectId} onChange={setProjectId} options={[{ value: "", label: "No project binding" }, ...projects.map((project) => ({ value: project.id, label: project.name }))]} />
<Input label={t("exec.repoUrl")} value={repoUrl} onChange={setRepoUrl} placeholder="https://github.com/org/repo" />
<Input label={t("exec.branchRef")} value={repoRef} onChange={setRepoRef} placeholder="main" />
<Select label={t("exec.project")} value={projectId} onChange={setProjectId} options={[{ value: "", label: t("exec.noProjectBinding") }, ...projects.map((project) => ({ value: project.id, label: project.name }))]} />
{submitError ? <InlineNotice message={submitError} /> : null}
<ActionButton
label="Add workspace"
label={t("exec.addWorkspace")}
tone="accent"
onClick={() => void handleSaveWorkspace()}
icon={FolderOpen}
@@ -1438,7 +1453,10 @@ export function SettingsPane({
companyId: string;
onSaved: () => Promise<void>;
}) {
const t = useT();
const currentLocale = useLocale();
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);
@@ -1450,7 +1468,8 @@ export function SettingsPane({
useEffect(() => {
setTheme(snapshot.theme);
setUpdatesEnabled(snapshot.updatesEnabled);
}, [snapshot.theme, snapshot.updatesEnabled]);
if (snapshot.locale) setSelectedLocale(snapshot.locale);
}, [snapshot.theme, snapshot.updatesEnabled, snapshot.locale]);
async function handleDeleteSecret(id: string) {
setDeleteSecretError(null);
@@ -1471,7 +1490,7 @@ export function SettingsPane({
setActionNotice(null);
setLoadingAction("save");
try {
unwrap(await window.agentCompany.updateSettings({ theme, updatesEnabled }));
unwrap(await window.agentCompany.updateSettings({ theme, locale: selectedLocale, updatesEnabled }));
await onSaved();
setActionNotice("Settings saved.");
} catch (error) {
@@ -1548,20 +1567,23 @@ export function SettingsPane({
{/* Profile info */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Profile</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.profile")}</div>
<div className="border-y border-[color:var(--line)] py-4">
<div className="mb-2 text-[12px] text-[color:var(--muted)]">Profile path</div>
<div className="mb-2 text-[12px] text-[color:var(--muted)]">{t("exec.profilePath")}</div>
<div className="mono text-[12px] text-[color:var(--muted-strong)]">{snapshot.profilePath}</div>
</div>
</div>
{/* Theme & updates */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Preferences</div>
<Select value={theme} onChange={(value) => setTheme(value as ProfileSnapshot["theme"])} options={[{ value: "system", label: "System" }, { value: "dark", label: "Dark" }, { value: "light", label: "Light" }]} />
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.preferences")}</div>
<Select label={t("settings.theme")} value={theme} onChange={(value) => setTheme(value as ProfileSnapshot["theme"])} 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={(value) => setSelectedLocale(value as AppLocale)} options={APP_LOCALES.map((loc) => ({ value: loc, label: LOCALE_LABELS[loc] }))} />
</div>
<div className="border-y border-[color:var(--line)] py-4 mt-3">
<label className="flex items-center justify-between text-[13px] text-[color:var(--muted-strong)]">
<span>Enable updater checks</span>
<span>{t("exec.enableUpdater")}</span>
<input type="checkbox" checked={updatesEnabled} onChange={(event) => setUpdatesEnabled(event.target.checked)} />
</label>
</div>
@@ -1573,7 +1595,7 @@ export function SettingsPane({
if (!company) return null;
return (
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Company autonomy</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.companyAutonomy")}</div>
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3">
<label className="flex items-center gap-3">
<input
@@ -1597,9 +1619,9 @@ export function SettingsPane({
className="h-4 w-4 rounded accent-[color:var(--accent)]"
/>
<div>
<div className="text-[13px] font-medium text-[color:var(--text)]">Auto-approve hire and strategy approvals</div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.autoApproveLabel")}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
Automatically approve agent hiring requests and CEO strategy approvals so the company can continue operating without waiting on the board for those specific approval types.
{t("exec.autoApproveDesc")}
</div>
</div>
</label>
@@ -1610,18 +1632,18 @@ export function SettingsPane({
{/* Actions */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Actions</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.actions")}</div>
<div className="grid gap-3 md:grid-cols-2">
<ActionButton label="Save settings" tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} />
<ActionButton label="Check for updates" onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} />
<ActionButton label="Backup profile" onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} />
<ActionButton label="Restore profile" onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} />
<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>
{/* Secret backend info */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Secret backend</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretBackend")}</div>
<div className="border-y border-[color:var(--line)] py-3 text-[13px] text-[color:var(--muted-strong)]">
{snapshot.backend.secretBackend}
</div>
@@ -1629,18 +1651,18 @@ export function SettingsPane({
{/* Secrets table */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Secrets ({secrets.length})</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secrets")} ({secrets.length})</div>
{secrets.length === 0 ? (
<EmptyState title="No secrets stored" />
<EmptyState title={t("exec.noSecretsStored")} />
) : (
<div className="border-y border-[color:var(--line)] overflow-x-auto">
<table className="w-full text-left text-[13px]">
<thead>
<tr className="border-b border-[color:var(--line)]">
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Name</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Description</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Backend</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Updated</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretName")}</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretDescription")}</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretBackendCol")}</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretUpdated")}</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]" />
</tr>
</thead>
@@ -1667,6 +1689,7 @@ export function SettingsPane({
export function SecretCreator({ companyId, onSaved }: { companyId: string; onSaved: () => Promise<void> }) {
const t = useT();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [value, setValue] = useState("");
@@ -1686,7 +1709,7 @@ export function SecretCreator({ companyId, onSaved }: { companyId: string; onSav
return (
<div className="grid gap-4">
<div>
<Input label="Secret name" value={name} onChange={(v) => { setName(v); if (errors.name) setErrors((e) => ({ ...e, name: undefined })); }} placeholder="e.g. ANTHROPIC_API_KEY" />
<Input label={t("exec.secretNameLabel")} value={name} onChange={(v) => { setName(v); if (errors.name) setErrors((e) => ({ ...e, name: undefined })); }} placeholder="e.g. ANTHROPIC_API_KEY" />
{errors.name ? <div className="mt-1 text-[12px] text-[color:var(--danger)]" role="alert">{errors.name}</div> : null}
</div>
<Input label="Description" value={description} onChange={setDescription} placeholder="What this secret is used for" />
@@ -1696,7 +1719,7 @@ export function SecretCreator({ companyId, onSaved }: { companyId: string; onSav
</div>
{submitError ? <InlineNotice message={submitError} /> : null}
<ActionButton
label="Store secret"
label={t("exec.storeSecret")}
tone="accent"
onClick={() => void (async () => {
if (!validate()) return;
+32 -46
View File
@@ -6,6 +6,7 @@ import {
X,
} from "@phosphor-icons/react";
import type { AgentRecord, ApprovalRecord } from "@shared/types";
import { useT } from "../i18n";
import { getAgentInitials } from "../lib/formatters";
import { unwrap } from "../lib/desktop";
import { ActionButton, EmptyState, Panel, StatusPill } from "./ui";
@@ -39,17 +40,6 @@ function getHiringStage(agent: AgentRecord, approval: ApprovalRecord | null): Hi
return "active";
}
function stageLabel(stage: HiringStage): string {
switch (stage) {
case "requisition": return "Requisition";
case "pending_approval": return "Pending Approval";
case "approved": return "Approved";
case "onboarding": return "Onboarding";
case "active": return "Active";
case "rejected": return "Rejected";
}
}
function stageColor(stage: HiringStage): string {
switch (stage) {
case "requisition": return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]";
@@ -64,8 +54,18 @@ function stageColor(stage: HiringStage): string {
const STAGE_ORDER: HiringStage[] = ["requisition", "pending_approval", "approved", "onboarding", "active", "rejected"];
export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent }: HiringPipelineProps) {
const t = useT();
const [filterStage, setFilterStage] = useState<HiringStage | "all">("all");
const stageLabelMap: Record<HiringStage, string> = {
requisition: t("hiring.requisition"),
pending_approval: t("hiring.pendingApproval"),
approved: t("hiring.approved"),
onboarding: t("hiring.onboarding"),
active: t("hiring.active"),
rejected: t("status.rejected"),
};
const hireApprovals = approvals.filter(a => a.type === "hire_agent");
const candidates: HiringCandidate[] = agents.map(agent => {
@@ -108,7 +108,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
return (
<div className="space-y-4">
<Panel title="Hiring Pipeline" eyebrow={`${activeHires} active hiring processes`}>
<Panel title={t("hiring.title")} eyebrow={`${activeHires} ${t("hiring.activeProcesses")}`}>
<div className="flex items-center gap-1 px-4 py-3 overflow-x-auto">
{STAGE_ORDER.filter(s => s !== "rejected").map((stage, idx) => (
<div key={stage} className="flex items-center gap-1">
@@ -118,7 +118,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
filterStage === stage ? "ring-1 ring-[color:var(--accent)]/40 bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]"
} ${stageColor(stage)}`}
>
{stageLabel(stage)}
{stageLabelMap[stage]}
<span className="ml-1.5 inline-flex items-center justify-center h-5 w-5 rounded-full bg-[color:var(--line)] text-[10px]">
{stageCounts[stage]}
</span>
@@ -135,7 +135,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
filterStage === "rejected" ? "ring-1 ring-[color:var(--danger)]/40 bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]"
} ${stageColor("rejected")}`}
>
Rejected
{stageLabelMap.rejected}
<span className="ml-1.5 inline-flex items-center justify-center h-5 w-5 rounded-full bg-[color:var(--line)] text-[10px]">
{stageCounts.rejected}
</span>
@@ -145,7 +145,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
</Panel>
{pendingCount > 0 && (
<Panel title="Awaiting Board Decision" eyebrow={`${pendingCount} hire requests need your review`}>
<Panel title={t("hiring.pendingApproval")}>
<div className="divide-y divide-[color:var(--line)]">
{allCandidates
.filter(c => c.stage === "requisition" || c.stage === "pending_approval")
@@ -159,25 +159,14 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-[color:var(--text)]">{ag?.name ?? "Proposed Agent"}</span>
<span className="font-medium text-sm text-[color:var(--text)]">{ag?.name ?? t("hiring.pendingHires")}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded ${stageColor(candidate.stage)}`}>
{stageLabel(candidate.stage)}
{stageLabelMap[candidate.stage]}
</span>
</div>
<div className="text-xs text-[color:var(--muted)] mt-0.5">
{ag ? `${ag.role} — ${ag.title}` : a?.payloadSummary}
</div>
{ag?.department && (
<div className="text-xs text-[color:var(--muted)] mt-0.5">
Dept: {ag.department} | Budget: ${ag.budgetMonthlyUsd}/mo | Connector: {ag.connectorId}
</div>
)}
{a?.payloadSummary && (
<div className="text-xs text-[color:var(--muted)] mt-1">{a.payloadSummary}</div>
)}
{a?.impactSummary && (
<div className="text-xs text-[color:var(--muted)]">{a.impactSummary}</div>
)}
{ag?.capabilities && (
<div className="text-xs text-[color:var(--muted)] mt-1 line-clamp-2">{ag.capabilities}</div>
)}
@@ -186,7 +175,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
{a && a.state === "pending" && (
<>
<ActionButton
label="Approve"
label={t("hiring.approved")}
icon={Check}
tone="accent"
onClick={async () => {
@@ -200,7 +189,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
}}
/>
<ActionButton
label="Reject"
label={t("status.rejected")}
icon={X}
tone="danger"
onClick={async () => {
@@ -224,18 +213,15 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
)}
<Panel
title={filterStage === "all" ? "All Agents" : `${stageLabel(filterStage)} (${filteredCandidates.length})`}
title={filterStage === "all" ? t("hiring.allAgents") : `${stageLabelMap[filterStage]} (${filteredCandidates.length})`}
action={
filterStage !== "all" ? (
<ActionButton label="Show all" icon={X} onClick={() => setFilterStage("all")} />
<ActionButton label={t("hiring.allAgents")} icon={X} onClick={() => setFilterStage("all")} />
) : undefined
}
>
{filteredCandidates.length === 0 ? (
<EmptyState
title="No hiring activity"
detail="When agents propose new hires via the API, they will appear here for your review."
/>
<EmptyState title={t("panel.noData")} />
) : (
<div className="divide-y divide-[color:var(--line)]">
{filteredCandidates.map((candidate) => {
@@ -258,7 +244,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-[color:var(--text)] truncate">{ag.name}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded ${stageColor(candidate.stage)}`}>
{stageLabel(candidate.stage)}
{stageLabelMap[candidate.stage]}
</span>
<StatusPill status={ag.status} />
</div>
@@ -268,10 +254,10 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
</div>
<div className="flex-shrink-0 text-right">
<div className="text-xs text-[color:var(--muted)]">
{manager ? `Reports to ${manager.name}` : "Top level"}
{manager ? `${t("hiring.reportsTo")} ${manager.name}` : t("hiring.topLevel")}
</div>
{requestedBy && (
<div className="text-[10px] text-[color:var(--muted)]">Proposed by {requestedBy.name}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("hiring.proposedBy")} {requestedBy.name}</div>
)}
<div className="text-xs text-[color:var(--muted)]">${ag.budgetMonthlyUsd}/mo</div>
</div>
@@ -284,28 +270,28 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<div className="grid grid-cols-4 gap-3">
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">Total Agents</div>
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.totalAgents")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">{agents.filter(a => a.status !== "terminated").length}</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">Pending Hires</div>
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.pendingHires")}</div>
<div className="text-2xl font-bold text-[color:var(--warn)] mt-1">{agents.filter(a => a.status === "pending_approval").length}</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">Departments</div>
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.departments")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">
{new Set(agents.filter(a => a.department && a.status !== "terminated").map(a => a.department)).size}
</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">Monthly Payroll</div>
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.monthlyPayroll")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">
${agents.filter(a => a.status !== "terminated" && a.status !== "pending_approval").reduce((s, a) => s + a.budgetMonthlyUsd, 0).toLocaleString()}
</div>
</div>
</div>
<Panel title="Department Headcount">
<Panel title={t("hiring.departmentHeadcount")}>
<div className="grid grid-cols-3 gap-2 p-4">
{(["executive", "engineering", "product", "design", "marketing", "sales", "hr", "finance", "legal", "customer_support", "operations", "research"] as const).map(dept => {
const deptAgents = agents.filter(a => a.department === dept && a.status !== "terminated");
@@ -317,11 +303,11 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<div className="text-xs font-medium text-[color:var(--text)] capitalize">{dept.replace("_", " ")}</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-sm font-bold text-[color:var(--text)]">{active}</span>
<span className="text-[10px] text-[color:var(--muted)]">active</span>
<span className="text-[10px] text-[color:var(--muted)]">{t("hiring.active")}</span>
{pending > 0 && (
<>
<span className="text-sm font-bold text-[color:var(--warn)]">+{pending}</span>
<span className="text-[10px] text-[color:var(--warn)]">pending</span>
<span className="text-[10px] text-[color:var(--warn)]">{t("hiring.pendingApproval")}</span>
</>
)}
</div>
+23 -19
View File
@@ -18,6 +18,7 @@ import {
} from "@phosphor-icons/react";
import type { InboxItem, InboxItemType, SectionId, StandupReport, CompanyMetrics } from "@shared/types";
import { formatMoney, timeAgo, getAgentInitials, formatDuration } from "../lib/formatters";
import { useT } from "../i18n";
const typeConfig: Record<InboxItemType, { icon: React.ElementType; color: string; bg: string; label: string }> = {
pending_approval: { icon: ShieldCheck, color: "var(--warn)", bg: "var(--warn-soft)", label: "Approval" },
@@ -41,6 +42,7 @@ interface InboxProps {
}
export function Inbox({ items, onNavigate }: InboxProps) {
const t = useT();
const [filter, setFilter] = useState<InboxItemType | "all">("all");
const filteredItems = useMemo(() => {
@@ -88,7 +90,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
<Bell size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">Inbox</h2>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("inbox.title")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention
</p>
@@ -123,8 +125,8 @@ export function Inbox({ items, onNavigate }: InboxProps) {
{filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Tray size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">All clear!</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">No items need your attention right now</div>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("inbox.allClear")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("inbox.noItemsNow")}</div>
</div>
) : (
<div className="space-y-1.5">
@@ -189,12 +191,13 @@ interface StandupProps {
}
export function StandupView({ report, onNavigate }: StandupProps) {
const t = useT();
if (!report) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Clock size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No standup data</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Company needs active agents to generate standup reports</div>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("standup.noData")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("standup.noDataDesc")}</div>
</div>
);
}
@@ -207,7 +210,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
<Heartbeat size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">Daily Standup</h2>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("standup.dailyStandup")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
Generated {new Date(report.generatedAt).toLocaleString()}
</p>
@@ -217,31 +220,31 @@ export function StandupView({ report, onNavigate }: StandupProps) {
<div className="mt-3 flex gap-4">
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Productive</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.productive")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--danger)]">{report.blockers.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Blockers</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.blockers")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--warn)]">{report.pendingApprovals}</div>
<div className="text-[10px] text-[color:var(--muted)]">Approvals</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.pendingApprovals")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className={`text-[18px] font-bold ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
{report.budgetUtilization}%
</div>
<div className="text-[10px] text-[color:var(--muted)]">Budget Used</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.budgetUsed")}</div>
</div>
</div>
</div>
{report.companyHighlights.length > 0 ? (
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Highlights</div>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.highlights")}</div>
<div className="space-y-1.5">
{report.companyHighlights.map((h, i) => (
<div key={i} className="flex items-center gap-2 rounded-[6px] bg-[color:var(--success-soft)] px-3 py-2 text-[12px] text-[color:var(--success)]">
@@ -268,7 +271,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
) : null}
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agent Status</div>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.agentStatus")}</div>
<div className="space-y-2">
{report.agentReports.map((ar) => (
<button
@@ -323,12 +326,13 @@ interface PerformanceProps {
}
export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
const t = useT();
if (!metrics) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Target size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No metrics yet</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Metrics will appear after agents start working</div>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("perf.noMetrics")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("perf.noMetricsDesc")}</div>
</div>
);
}
@@ -337,24 +341,24 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
<div className="space-y-6">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
<CompactMetric
label="Success Rate"
label={t("perf.successRate")}
value={`${metrics.overallSuccessRate}%`}
tone={metrics.overallSuccessRate >= 80 ? "success" : metrics.overallSuccessRate >= 50 ? "warn" : "danger"}
/>
<CompactMetric
label="Tasks Done"
label={t("perf.tasksDone")}
value={metrics.completedTasks}
tone="success"
subtitle={`of ${metrics.totalTasks} total`}
/>
<CompactMetric
label="Throughput"
label={t("perf.throughput")}
value={`${metrics.throughputTasksPerDay.toFixed(1)}`}
tone="accent"
subtitle="tasks/day"
subtitle={t("perf.tasksPerDay")}
/>
<CompactMetric
label="Avg Cost/Task"
label={t("perf.avgCostTask")}
value={formatMoney(metrics.avgCostPerTask)}
tone="default"
/>
+9 -4
View File
@@ -30,6 +30,7 @@ import {
StatusPill,
TextArea,
} from "./ui";
import { useT } from "../i18n";
function BudgetBar({ spent, budget }: { spent: number; budget: number }) {
const pct = budget > 0 ? Math.min((spent / budget) * 100, 100) : 0;
@@ -80,8 +81,9 @@ export function AgentRoster({
selectedAgentId: string | null;
onSelect: (agentId: string) => void;
}) {
const t = useT();
if (agents.length === 0) {
return <EmptyState title="No agents yet" />;
return <EmptyState title={t("org.noAgentsYet")} />;
}
return (
<ListFrame>
@@ -617,8 +619,9 @@ export function GoalList({
selectedGoalId: string | null;
onSelect: (goalId: string) => void;
}) {
const t = useT();
if (goals.length === 0) {
return <EmptyState title="No goals yet" />;
return <EmptyState title={t("org.noGoalsYet")} />;
}
return (
<ListFrame>
@@ -847,8 +850,9 @@ export function ProjectList({
selectedProjectId: string | null;
onSelect: (projectId: string) => void;
}) {
const t = useT();
if (projects.length === 0) {
return <EmptyState title="No projects yet" />;
return <EmptyState title={t("org.noProjectsYet")} />;
}
return (
<ListFrame>
@@ -1183,6 +1187,7 @@ export function OrgChart({
agents: AgentRecord[];
onSelectAgent?: (agentId: string) => void;
}) {
const t = useT();
const containerRef = useRef<HTMLDivElement>(null);
const [pan, setPan] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
@@ -1257,7 +1262,7 @@ export function OrgChart({
}, [zoom, pan]);
if (agents.length === 0) {
return <EmptyState title="No agents to display" />;
return <EmptyState title={t("org.noAgentsToDisplay")} />;
}
return (
+22 -20
View File
@@ -10,6 +10,7 @@ import {
import type { AgentRecord, RunRecord, TaskRecord, WorkspaceRecord } from "@shared/types";
import { formatMoney, formatTime, timeAgo } from "../lib/formatters";
import { ActionButton, EmptyState, Input, ListFrame, ListRow, Panel, Select, StatusPill } from "./ui";
import { useT } from "../i18n";
const LOG_CHUNK_BYTES = 65_536;
@@ -64,6 +65,7 @@ export function RunCenter({
onOpenLogPath,
onOpenConsole,
}: RunCenterProps) {
const t = useT();
const [statusFilter, setStatusFilter] = useState<"all" | RunRecord["status"]>("all");
const [search, setSearch] = useState("");
const [logState, setLogState] = useState<RunLogState>(initialLogState);
@@ -265,8 +267,8 @@ export function RunCenter({
if (runs.length === 0) {
return (
<EmptyState
title="No runs yet"
detail="Start a task run from the task queue or onboarding handoff. This surface becomes the operator console for queue state, failures, and live log tails."
title={t("runs.noRunsYet")}
detail={t("runs.noRunsDetail")}
/>
);
}
@@ -275,22 +277,22 @@ export function RunCenter({
<div className="space-y-10">
<div className="grid gap-3 md:grid-cols-3">
<RunSummaryCard
label="Active"
label={t("runs.activeLabel")}
value={groupedCounts.active}
detail="Queued or running right now"
detail={t("runs.queuedOrRunning")}
icon={PlayCircle}
/>
<RunSummaryCard
label="Needs attention"
label={t("runs.needsAttention")}
value={groupedCounts.attention}
detail="Failed, timed out, or interrupted"
detail={t("runs.failedTimedInterrupted")}
icon={WarningCircle}
tone="danger"
/>
<RunSummaryCard
label="Completed"
label={t("runs.completedLabel")}
value={groupedCounts.completed}
detail="Finished without current intervention"
detail={t("runs.finishedWithout")}
icon={ListChecks}
/>
</div>
@@ -298,7 +300,7 @@ export function RunCenter({
<div className="grid gap-8 xl:grid-cols-[minmax(0,420px)_minmax(0,1fr)]">
<div className="space-y-5">
<Panel
title="Run queue"
title={t("runs.runQueue")}
action={(
<Select
compact
@@ -322,7 +324,7 @@ export function RunCenter({
ariaLabel="Search runs"
value={search}
onChange={setSearch}
placeholder="Search by task, agent, connector, workspace, or error"
placeholder={t("runs.searchRuns")}
/>
</div>
<ListFrame>
@@ -359,8 +361,8 @@ export function RunCenter({
</ListFrame>
{filteredRuns.length === 0 ? (
<EmptyState
title="No runs match this filter"
detail="Widen the status filter or clear the search query to inspect the full run history."
title={t("runs.noRunsMatch")}
detail={t("runs.noRunsMatchDetail")}
/>
) : null}
</Panel>
@@ -370,17 +372,17 @@ export function RunCenter({
{selectedRun ? (
<>
<Panel
title="Run detail"
title={t("runs.runDetail")}
action={(
<div className="flex items-center gap-2">
<ActionButton
label="Open console"
label={t("runs.openConsole")}
icon={Lightning}
onClick={() => onOpenConsole(selectedRun.id)}
/>
{isActiveStatus(selectedRun.status) ? (
<ActionButton
label="Cancel run"
label={t("runs.cancelRun")}
tone="danger"
icon={WarningCircle}
onClick={() => void onCancelRun(selectedRun.id)}
@@ -409,7 +411,7 @@ export function RunCenter({
{selectedRun.logPath ? (
<div className="mt-5">
<ActionButton
label="Open log file"
label={t("runs.openLogFile")}
icon={FolderOpen}
onClick={() => void onOpenLogPath(selectedRun.logPath!)}
/>
@@ -418,18 +420,18 @@ export function RunCenter({
</Panel>
<Panel
title="Log tail"
eyebrow="Incremental reader"
title={t("runs.logTail")}
eyebrow={t("runs.incrementalReader")}
action={(
<div className="flex items-center gap-2">
<ActionButton
label="Reload tail"
label={t("runs.reloadTail")}
icon={ArrowClockwise}
onClick={() => setLogReloadKey((current) => current + 1)}
/>
{logState.offset > 0 ? (
<ActionButton
label="Load older"
label={t("runs.loadOlder")}
icon={ListChecks}
onClick={() => void loadOlderLogs()}
disabled={logState.loading}
+5 -3
View File
@@ -31,6 +31,7 @@ import {
import type { BrowserActionRecord, SocialAccountRecord, SocialPlatform } from "@shared/types";
import { timeAgo } from "../lib/formatters";
import { ActionButton, Input } from "./ui";
import { useT } from "../i18n";
const platformConfig: Record<SocialPlatform, { label: string; color: string; bgColor: string; icon: ElementType }> = {
twitter: { label: "Twitter / X", color: "#1D9BF0", bgColor: "#1D9BF014", icon: XLogo },
@@ -418,6 +419,7 @@ export function SocialAccountsPanel({
onCancelAction,
onNavigate,
}: SocialPanelProps) {
const t = useT();
const [showAddDialog, setShowAddDialog] = useState(false);
const [activeTab, setActiveTab] = useState<"accounts" | "history">("accounts");
@@ -434,7 +436,7 @@ export function SocialAccountsPanel({
<Megaphone size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">Social Media</h2>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("social.socialMedia")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{accounts.length} account{accounts.length !== 1 ? "s" : ""} connected
</p>
@@ -491,9 +493,9 @@ export function SocialAccountsPanel({
accounts.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Megaphone size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No social accounts</div>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("social.noAccounts")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
Add your first social media account to enable marketing automation
{t("social.noAccountsDesc")}
</div>
<div className="mt-4">
<ActionButton