update
This commit is contained in:
1 parent
da1fec8f2a
commit
b28ed38b8d
116 files changed
+273040
-723
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user