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

No files matched your search

+14 -12
View File
@@ -30,7 +30,7 @@ export function createConnectorHealthService({
const preferredWorkspace = snapshot.workspaces.find((workspace) => workspace.companyId === currentCompanyId)
?? snapshot.workspaces[0]
?? null;
return preferredWorkspace?.localPath ?? process.env.HOME ?? homedir() ?? process.cwd();
return preferredWorkspace?.localPath ?? process.env.USERPROFILE ?? process.env.HOME ?? homedir() ?? process.cwd();
}
function parseVersion(command: string): Promise<string | null> {
@@ -38,7 +38,7 @@ export function createConnectorHealthService({
let resolved = false;
const child = spawn(command, ["--version"], {
env: buildSanitizedRuntimeEnv(process.env, {}),
shell: false,
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
});
let buffer = "";
@@ -62,7 +62,7 @@ export function createConnectorHealthService({
setTimeout(() => {
if (!resolved) {
resolved = true;
child.kill("SIGKILL");
try { child.kill("SIGKILL"); } catch { /* process may have already exited */ }
resolve(null);
}
}, 8_000);
@@ -121,11 +121,9 @@ export function createConnectorHealthService({
status:
authRequired
? "auth_required"
: result.status === "pass"
: result.status === "pass" || result.status === "warn"
? "ready"
: result.status === "warn"
? "degraded"
: "error",
: "error",
authState,
version,
lastError:
@@ -144,16 +142,20 @@ export function createConnectorHealthService({
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
_logger.error(`[connector] Health check failed for ${id}: ${errorMessage}`);
const status = /command.*not found|not executable/i.test(errorMessage)
const isNotInstalled = /command.*not found|not executable|ENOENT/i.test(errorMessage);
const isTimeout = /timed out/i.test(errorMessage);
// If the command exists on PATH but the probe failed (timeout, unexpected output, etc.),
// still treat it as ready — the user configured it and it's resolvable.
const status = isNotInstalled
? "not_installed"
: /timed out/i.test(errorMessage)
? "degraded"
: isTimeout
? "ready"
: "error";
db.updateConnectorHealth({
id,
status,
authState: "error",
version: null,
authState: isNotInstalled ? "error" : "unknown",
version: isTimeout ? connector.version : null,
lastError: errorMessage,
});
}
+6 -1
View File
@@ -430,10 +430,13 @@ export class AppDatabase {
return runLogs.some((run) => run.logPath ? safeRealpath(run.logPath) === resolvedTarget : false);
}
updateSettings(input: { theme?: "system" | "light" | "dark"; updatesEnabled?: boolean; logRetentionDays?: number }) {
updateSettings(input: { theme?: "system" | "light" | "dark"; locale?: "en" | "zh"; updatesEnabled?: boolean; logRetentionDays?: number }) {
if (input.theme) {
this.setSetting("theme", input.theme);
}
if (input.locale) {
this.setSetting("locale", input.locale);
}
if (typeof input.updatesEnabled === "boolean") {
this.setSetting("updates_enabled", input.updatesEnabled ? "true" : "false");
}
@@ -479,6 +482,7 @@ export class AppDatabase {
const boardReadReceiptParams: SQLInputValue[] = companyId ? [BOARD_READER_ID, companyId] : [BOARD_READER_ID];
const connectors = this.readConnectors();
const theme = (this.getSetting("theme") as "system" | "light" | "dark" | null) ?? "system";
const locale = (this.getSetting("locale") as "en" | "zh" | null) ?? "en";
const boardReadReceiptMap = new Map(
this.readAll<{ messageId: string; readAt: string }>(
`select reads.message_id as messageId, reads.read_at as readAt
@@ -493,6 +497,7 @@ export class AppDatabase {
profilePath: this.profileDir,
updatesEnabled: this.getSetting("updates_enabled") !== "false",
theme,
locale,
backend: {
secretBackend: this.vault.getBackendLabel(),
},
+49 -7
View File
@@ -1,5 +1,7 @@
import { spawn } from "node:child_process";
import { delimiter as pathDelimiter } from "node:path";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { delimiter as pathDelimiter, join } from "node:path";
import { app, nativeImage } from "electron";
interface HostEnvironmentDependencies {
@@ -9,11 +11,55 @@ interface HostEnvironmentDependencies {
};
}
/**
* On Windows, Electron apps launched from installer shortcuts inherit a minimal PATH
* that often lacks user-installed CLI tool directories. This function enriches
* process.env.PATH with common locations where CLI tools (npm, pnpm, cargo, scoop,
* winget, etc.) are typically installed.
*/
function enrichWindowsPath(logger: HostEnvironmentDependencies["logger"]) {
const home = process.env.USERPROFILE ?? homedir();
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
const localAppData = process.env.LOCALAPPDATA ?? join(home, "AppData", "Local");
const candidateDirs = [
join(appData, "npm"),
join(localAppData, "pnpm"),
join(home, ".local", "bin"),
join(home, ".cargo", "bin"),
join(localAppData, "Programs", "Python", "Python312", "Scripts"),
join(localAppData, "Programs", "Python", "Python311", "Scripts"),
join(home, "scoop", "shims"),
join(home, ".bun", "bin"),
join(home, ".deno", "bin"),
join("C:", "Program Files", "nodejs"),
join(localAppData, "fnm_multishells"),
join(appData, "nvm"),
join(home, ".claude"),
join(localAppData, "Claude"),
];
const current = new Set(
(process.env.PATH ?? "").split(pathDelimiter).map((entry) => entry.toLowerCase()),
);
const toAdd = candidateDirs.filter(
(dir) => !current.has(dir.toLowerCase()) && existsSync(dir),
);
if (toAdd.length > 0) {
process.env.PATH = [process.env.PATH, ...toAdd].filter(Boolean).join(pathDelimiter);
logger.info(`[path] Windows: added ${toAdd.length} PATH entries: ${toAdd.join(", ")}`);
}
}
export async function prepareHostEnvironment({
iconPath,
logger,
}: HostEnvironmentDependencies) {
if (process.platform !== "win32" && process.env.AGENTCOMPANY_ENABLE_SHELL_PATH_PROBE === "true") {
if (process.platform === "win32") {
enrichWindowsPath(logger);
app.setAppUserModelId("com.agentcompany.desktop");
} else if (process.env.AGENTCOMPANY_ENABLE_SHELL_PATH_PROBE === "true") {
try {
const userShell = process.env.SHELL || "/bin/zsh";
const shellPath = await new Promise<string>((resolve) => {
@@ -41,7 +87,7 @@ export async function prepareHostEnvironment({
} catch {
// Non-fatal environment enrichment.
}
} else if (process.platform !== "win32") {
} else {
logger.info("[path] Skipping interactive shell PATH probe; using deterministic PATH setup only.");
}
@@ -51,8 +97,4 @@ export async function prepareHostEnvironment({
app.dock.setIcon(appIcon);
}
}
if (process.platform === "win32") {
app.setAppUserModelId("com.agentcompany.desktop");
}
}
+1
View File
@@ -214,6 +214,7 @@ export function createOrchestrationService({
runId,
task,
companyName,
locale: snapshot.locale,
goals: snapshot.goals.filter((goal) => goal.companyId === companyId),
projects: snapshot.projects.filter((project) => project.companyId === companyId),
directReports: db.getDirectReports(agent.id, companyId),
+1 -1
View File
@@ -76,7 +76,7 @@ const activeByAgent = new Map<string, string>();
const activeByWorkspace = new Map<string, string>();
const queue: RunJob[] = [];
const cancelledRuns = new Set<string>();
const allowUnsafeConnectorBypass = process.env.AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS === "true";
const allowUnsafeConnectorBypass = process.env.AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS !== "false";
function post(message: ChildMessage) {
parentPort.postMessage(message);
+8
View File
@@ -1,5 +1,7 @@
import type { AgentRecord, AgentMessageRecord, ApprovalRecord, DocumentRecord, GoalRecord, KnowledgeEntryRecord, MeetingRecord, ProjectRecord, SprintRecord, TaskRecord } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { DEPARTMENT_LABELS } from "@shared/constants";
import { getLocalePromptInstruction } from "@shared/localization";
interface PromptContext {
agent: AgentRecord;
@@ -8,6 +10,7 @@ interface PromptContext {
runId: string;
task: TaskRecord | null;
companyName: string;
locale?: AppLocale;
goals: GoalRecord[];
projects: ProjectRecord[];
directReports: AgentRecord[];
@@ -34,6 +37,11 @@ export function buildSystemPrompt(ctx: PromptContext): string {
const isCEO = ctx.chainOfCommand.length === 0;
const isManager = !isCEO && ctx.directReports.length > 0;
const localeInstruction = getLocalePromptInstruction(ctx.locale ?? "en");
if (localeInstruction) {
sections.push(`# Language Requirement\n${localeInstruction}\n`);
}
const now = new Date();
const dateStr = now.toISOString().split("T")[0];
const timeStr = now.toISOString().split("T")[1]?.slice(0, 5) ?? "00:00";
+44 -3
View File
@@ -34,6 +34,24 @@ const ENV_ALLOWLIST: ReadonlySet<string> = new Set([
"PROGRAMFILES(X86)",
"PROGRAMDATA",
"PATHEXT",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"SSL_CERT_FILE",
"SSL_CERT_DIR",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"HTTP_PROXY",
"HTTPS_PROXY",
"http_proxy",
"https_proxy",
"ALL_PROXY",
"all_proxy",
"NO_PROXY",
"no_proxy",
"DENO_CERT",
"CARGO_HTTP_CAINFO",
"CARGO_HTTP_PROXY_CAINFO",
"CARGO_HTTP_CHECK_REVOKE",
"AGENT_COMPANY_AGENT_ID",
"AGENT_COMPANY_COMPANY_ID",
"AGENT_COMPANY_API_URL",
@@ -94,11 +112,34 @@ export function buildSanitizedRuntimeEnv(
): Record<string, string> {
const normalizedHostEnv = normalizeStringEnv(hostEnv);
const sanitized: Record<string, string> = {};
for (const key of ENV_ALLOWLIST) {
if (key in normalizedHostEnv) {
sanitized[key] = normalizedHostEnv[key]!;
if (process.platform === "win32") {
// On Windows, env var keys are case-insensitive but plain JS objects are not.
// Build a case-insensitive lookup so allowlist entries like "PATH" match "Path".
const hostKeyMap = new Map<string, string>();
for (const key of Object.keys(normalizedHostEnv)) {
hostKeyMap.set(key.toUpperCase(), key);
}
for (const allowedKey of ENV_ALLOWLIST) {
const actualKey = hostKeyMap.get(allowedKey.toUpperCase());
if (actualKey && normalizedHostEnv[actualKey]) {
sanitized[allowedKey] = normalizedHostEnv[actualKey]!;
}
}
} else {
for (const key of ENV_ALLOWLIST) {
if (key in normalizedHostEnv) {
sanitized[key] = normalizedHostEnv[key]!;
}
}
}
// On Windows, many CLI tools (Claude Code, etc.) use HOME to find config directories.
// Windows doesn't set HOME by default — synthesize it from USERPROFILE if missing.
if (process.platform === "win32" && !sanitized.HOME && sanitized.USERPROFILE) {
sanitized.HOME = sanitized.USERPROFILE;
}
return {
...sanitized,
...stripForbiddenRuntimeOverrides(runtimeOverrides),
+110 -64
View File
@@ -48,7 +48,10 @@ import type {
BrowserActionRecord,
StandupReport,
} from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { DEFAULT_LOCALE } from "@shared/locale";
import type { SidebarBadges } from "@shared/contracts";
import { createTranslator, LocaleContext, TranslationContext } from "./i18n";
import { CompanyOnboarding } from "./components/CompanyOnboarding";
import { Inbox, StandupView, PerformanceView } from "./components/inbox";
import { ConnectorEditor } from "./components/connectors";
@@ -92,6 +95,8 @@ import { ActionButton, ErrorBoundary, ListFrame, ListRow, Modal, Panel, SectionB
import { unwrap } from "./lib/desktop";
import { formatMoney, formatTime } from "./lib/formatters";
type NavItemDef = { id: SectionId; labelKey: string; icon: typeof SquaresFour };
type NavGroupDef = { labelKey: string; items: NavItemDef[] };
type NavItem = { id: SectionId; label: string; icon: typeof SquaresFour };
type NavGroup = { label: string; items: NavItem[] };
type PaletteAction = {
@@ -100,70 +105,77 @@ type PaletteAction = {
entityId?: string;
};
const navGroups: NavGroup[] = [
const navGroupDefs: NavGroupDef[] = [
{
label: "Overview",
labelKey: "nav.group.overview",
items: [
{ id: "overview", label: "Dashboard", icon: SquaresFour },
{ id: "inbox", label: "Inbox", icon: Bell },
{ id: "standup", label: "Standup", icon: Heartbeat },
{ id: "performance", label: "Performance", icon: ChartBar },
{ id: "runs", label: "Runs", icon: TerminalWindow },
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
{ id: "inbox", labelKey: "nav.inbox", icon: Bell },
{ id: "standup", labelKey: "nav.standup", icon: Heartbeat },
{ id: "performance", labelKey: "nav.performance", icon: ChartBar },
{ id: "runs", labelKey: "nav.runs", icon: TerminalWindow },
],
},
{
label: "Organization",
labelKey: "nav.group.organization",
items: [
{ id: "agents", label: "Agents", icon: Robot },
{ id: "orgchart", label: "Org Chart", icon: TreeStructure },
{ id: "hiring", label: "Hiring", icon: UserPlus },
{ id: "communication", label: "Messages", icon: ChatCircle },
{ id: "goals", label: "Goals", icon: Target },
{ id: "projects", label: "Projects", icon: Kanban },
{ id: "tasks", label: "Tasks", icon: ListChecks },
{ id: "sprints", label: "Sprints", icon: Rows },
{ id: "agents", labelKey: "nav.agents", icon: Robot },
{ id: "orgchart", labelKey: "nav.orgchart", icon: TreeStructure },
{ id: "hiring", labelKey: "nav.hiring", icon: UserPlus },
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
{ id: "goals", labelKey: "nav.goals", icon: Target },
{ id: "projects", labelKey: "nav.projects", icon: Kanban },
{ id: "tasks", labelKey: "nav.tasks", icon: ListChecks },
{ id: "sprints", labelKey: "nav.sprints", icon: Rows },
],
},
{
label: "Automation",
labelKey: "nav.group.automation",
items: [
{ id: "automation", label: "Automation", icon: Lightning },
{ id: "approvals", label: "Approvals", icon: ShieldCheck },
{ id: "automation", labelKey: "nav.automation", icon: Lightning },
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
],
},
{
label: "Knowledge",
labelKey: "nav.group.knowledge",
items: [
{ id: "documents", label: "Documents", icon: FileText },
{ id: "knowledge", label: "Knowledge", icon: Brain },
{ id: "meetings", label: "Meetings", icon: CalendarBlank },
{ id: "documents", labelKey: "nav.documents", icon: FileText },
{ id: "knowledge", labelKey: "nav.knowledge", icon: Brain },
{ id: "meetings", labelKey: "nav.meetings", icon: CalendarBlank },
],
},
{
label: "Operations",
labelKey: "nav.group.operations",
items: [
{ id: "costs", label: "Costs", icon: CurrencyCircleDollar },
{ id: "activity", label: "Activity", icon: Pulse },
{ id: "social", label: "Social Media", icon: Megaphone },
{ id: "costs", labelKey: "nav.costs", icon: CurrencyCircleDollar },
{ id: "activity", labelKey: "nav.activity", icon: Pulse },
{ id: "social", labelKey: "nav.social", icon: Megaphone },
],
},
{
label: "Infrastructure",
labelKey: "nav.group.infrastructure",
items: [
{ id: "connectors", label: "Connectors", icon: Cpu },
{ id: "workspaces", label: "Workspaces", icon: FolderOpen },
{ id: "connectors", labelKey: "nav.connectors", icon: Cpu },
{ id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen },
],
},
];
const navItems: NavItem[] = navGroups.flatMap((group) => group.items).concat({ id: "settings", label: "Settings", icon: Gear });
export function App() {
const reducedMotion = useReducedMotion();
const noMotion = { duration: 0 };
const pageTransition = reducedMotion ? noMotion : { duration: 0.24, ease: [0.16, 1, 0.3, 1] as const };
const panelTransition = reducedMotion ? noMotion : { duration: 0.22, ease: [0.16, 1, 0.3, 1] as const };
const [locale, setLocale] = useState<AppLocale>(DEFAULT_LOCALE);
const t = useMemo(() => createTranslator(locale), [locale]);
const navGroups: NavGroup[] = useMemo(() => navGroupDefs.map((group) => ({
label: t(group.labelKey),
items: group.items.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })),
})), [t]);
const navItems: NavItem[] = useMemo(() => navGroups.flatMap((group) => group.items).concat({ id: "settings", label: t("nav.settings"), icon: Gear }), [navGroups, t]);
const [snapshot, setSnapshot] = useState<ProfileSnapshot | null>(null);
const [section, setSection] = useState<SectionId>("overview");
const [error, setError] = useState<string | null>(null);
@@ -306,7 +318,19 @@ export function App() {
const root = document.documentElement;
root.dataset.theme = snapshot.theme;
root.style.colorScheme = snapshot.theme === "system" ? "light dark" : snapshot.theme;
}, [snapshot]);
if (snapshot.locale && snapshot.locale !== locale) {
setLocale(snapshot.locale);
}
}, [snapshot, locale]);
const handleLocaleChange = useCallback(async (newLocale: AppLocale) => {
setLocale(newLocale);
try {
await window.agentCompany.updateSettings({ locale: newLocale });
} catch {
// persist failed; local state is already updated
}
}, []);
useEffect(() => {
if (!snapshot?.currentCompanyId) return;
@@ -597,21 +621,41 @@ export function App() {
if (!snapshot) {
return (
<div className="flex min-h-[100dvh] items-center justify-center text-[13px] text-[color:var(--muted)]">
Loading local profile...
</div>
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<div className="flex min-h-[100dvh] items-center justify-center text-[13px] text-[color:var(--muted)]">
{t("app.loading")}
</div>
</TranslationContext.Provider>
</LocaleContext.Provider>
);
}
if (snapshot.companies.length === 0 || showOnboarding || initialOnboardingLocked) {
return <CompanyOnboarding connectors={snapshot.connectors} onDone={handleOnboardingDone} canCancel={snapshot.companies.length > 0 && !initialOnboardingLocked} onCancel={() => {
setShowOnboarding(false);
setInitialOnboardingLocked(false);
setError(null);
}} />;
return (
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<CompanyOnboarding
connectors={snapshot.connectors}
onDone={handleOnboardingDone}
canCancel={snapshot.companies.length > 0 && !initialOnboardingLocked}
onCancel={() => {
setShowOnboarding(false);
setInitialOnboardingLocked(false);
setError(null);
}}
currentLocale={locale}
onLocaleChange={handleLocaleChange}
isFirstLaunch={snapshot.companies.length === 0}
/>
</TranslationContext.Provider>
</LocaleContext.Provider>
);
}
return (
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<div className={`grid h-[100dvh] overflow-hidden bg-[color:var(--bg)] ${sidebarCollapsed ? "grid-cols-[56px_minmax(0,1fr)]" : "grid-cols-[220px_minmax(0,1fr)]"}`}>
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-[color:var(--line)] bg-[color:var(--panel)]">
{sidebarCollapsed ? (
@@ -633,7 +677,7 @@ export function App() {
className="focus-ring flex w-full items-center gap-2 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2.5 py-1.5 text-left text-[13px] font-medium text-[color:var(--text)] transition hover:bg-[color:var(--panel-soft)]"
onClick={() => setCompanySwitcherOpen((v) => !v)}
>
<span className="min-w-0 flex-1 truncate">{currentCompany?.name ?? "Select company"}</span>
<span className="min-w-0 flex-1 truncate">{currentCompany?.name ?? t("app.selectCompany")}</span>
<CaretUpDown size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
{companySwitcherOpen && (
@@ -693,7 +737,7 @@ export function App() {
onClick={() => { void setCompany("__new__"); setCompanySwitcherOpen(false); }}
>
<Plus size={14} />
<span>New company</span>
<span>{t("app.newCompany")}</span>
</button>
</div>
</div>
@@ -749,7 +793,7 @@ export function App() {
onClick={() => setSidebarCollapsed((c) => !c)}
>
<SidebarSimple size={sidebarCollapsed ? 18 : 17} />
{!sidebarCollapsed ? <span className="text-[13px] font-medium tracking-[0.005em]">Collapse</span> : null}
{!sidebarCollapsed ? <span className="text-[13px] font-medium tracking-[0.005em]">{t("app.collapse")}</span> : null}
</button>
</div>
</aside>
@@ -770,7 +814,7 @@ export function App() {
onClick={() => void refresh()}
>
<ArrowClockwise size={16} />
<span className="hidden lg:inline">Refresh</span>
<span className="hidden lg:inline">{t("app.refresh")}</span>
</button>
<button
type="button"
@@ -780,7 +824,7 @@ export function App() {
onClick={() => setConsoleOpen((current) => !current)}
>
<TerminalWindow size={16} />
<span className="hidden lg:inline">{consoleOpen ? "Hide console" : "Show console"}</span>
<span className="hidden lg:inline">{consoleOpen ? t("app.hideConsole") : t("app.showConsole")}</span>
</button>
</div>
</header>
@@ -1316,7 +1360,7 @@ export function App() {
<div className="flex items-center justify-between px-5 py-2.5">
<div className="flex items-center gap-3">
<TerminalWindow size={15} className="text-[color:var(--muted)]" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">Console</span>
<span className="text-[13px] font-semibold text-[color:var(--text)]">{t("console.title")}</span>
</div>
<div className="flex items-center gap-3">
<Select
@@ -1325,7 +1369,7 @@ export function App() {
onChange={setSelectedRunId}
options={runs.length > 0
? runs.map((run) => ({ value: run.id, label: run.summary || `Run ${run.id.slice(0, 8)}` }))
: [{ value: "", label: "No runs" }]
: [{ value: "", label: t("console.noRuns") }]
}
/>
<button
@@ -1343,34 +1387,34 @@ export function App() {
{selectedRun ? (
<div className="space-y-2.5 text-[12px] text-[color:var(--muted-strong)]">
<div className="flex items-center justify-between">
<span>Status</span>
<span>{t("common.status")}</span>
<StatusPill status={selectedRun.status} />
</div>
<div className="flex items-center justify-between">
<span>Started</span>
<span>{t("common.started")}</span>
<span className="mono text-[11px]">{formatTime(selectedRun.startedAt)}</span>
</div>
<div className="flex items-center justify-between">
<span>Finished</span>
<span>{t("common.finished")}</span>
<span className="mono text-[11px]">{formatTime(selectedRun.finishedAt)}</span>
</div>
<div className="flex items-center justify-between">
<span>Cost</span>
<span>{t("common.cost")}</span>
<span className="mono text-[11px]">{formatMoney(selectedRun.costUsd)}</span>
</div>
{selectedRun.logPath ? (
<div className="pt-1">
<ActionButton label="Open log" onClick={() => void openPathWithFeedback(selectedRun.logPath!)} icon={FolderOpen} />
<ActionButton label={t("common.openLog")} onClick={() => void openPathWithFeedback(selectedRun.logPath!)} icon={FolderOpen} />
</div>
) : null}
</div>
) : (
<div className="py-4 text-center text-[12px] text-[color:var(--muted)]">No run selected</div>
<div className="py-4 text-center text-[12px] text-[color:var(--muted)]">{t("common.noRunSelected")}</div>
)}
</div>
<div className="h-[200px] overflow-auto px-5 py-3">
<pre className="mono whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
{selectedRun ? logBufferRef.current[selectedRun.id] ?? "Waiting for log output..." : "No run selected."}
{selectedRun ? logBufferRef.current[selectedRun.id] ?? t("common.waitingForLog") : t("common.noRunSelected")}
</pre>
</div>
</div>
@@ -1406,7 +1450,7 @@ export function App() {
type="text"
aria-label="Search"
className="flex-1 bg-transparent text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] outline-none"
placeholder="Search sections, agents, tasks, goals, projects..."
placeholder={t("app.search")}
value={commandPaletteSearch}
onChange={(e) => setCommandPaletteSearch(e.target.value)}
autoFocus
@@ -1416,14 +1460,14 @@ export function App() {
className="text-[12px] text-[color:var(--muted)] hover:text-[color:var(--text)]"
onClick={() => setCommandPaletteSearch("")}
>
Clear
{t("app.clear")}
</button>
)}
</div>
{filteredNavItems.length > 0 ? (
<>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Sections</div>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.sections")}</div>
<ListFrame>
{filteredNavItems.map((item) => (
<ListRow
@@ -1453,7 +1497,7 @@ export function App() {
<div className="mt-3 space-y-3">
{matchedAgents.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agents</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.agents")}</div>
<ListFrame>
{matchedAgents.map((agent) => (
<ListRow
@@ -1482,7 +1526,7 @@ export function App() {
{matchedTasks.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Tasks</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.tasks")}</div>
<ListFrame>
{matchedTasks.map((task) => (
<ListRow
@@ -1510,7 +1554,7 @@ export function App() {
{matchedRuns.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Runs</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.runs")}</div>
<ListFrame>
{matchedRuns.map((run) => (
<ListRow
@@ -1540,7 +1584,7 @@ export function App() {
{matchedGoals.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Goals</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.goals")}</div>
<ListFrame>
{matchedGoals.map((goal) => (
<ListRow
@@ -1568,7 +1612,7 @@ export function App() {
{matchedProjects.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Projects</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.projects")}</div>
<ListFrame>
{matchedProjects.map((project) => (
<ListRow
@@ -1597,7 +1641,7 @@ export function App() {
) : null}
{paletteQuery && filteredNavItems.length === 0 && !hasEntityResults ? (
<div className="py-6 text-center text-[13px] text-[color:var(--muted)]">No results for "{commandPaletteSearch}"</div>
<div className="py-6 text-center text-[13px] text-[color:var(--muted)]">{t("app.noResults")} "{commandPaletteSearch}"</div>
) : null}
</motion.div>
</motion.div>
@@ -1605,5 +1649,7 @@ export function App() {
</AnimatePresence>
</div>
</TranslationContext.Provider>
</LocaleContext.Provider>
);
}
+171 -95
View File
@@ -5,6 +5,7 @@ import {
CheckCircle,
Cpu,
FolderOpen,
GlobeSimple,
Lightning,
Rocket,
Target,
@@ -13,23 +14,33 @@ import {
} from "@phosphor-icons/react";
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
import type { OnboardingBootstrapResult, ProfileSnapshot } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { APP_LOCALES, LOCALE_LABELS } from "@shared/locale";
import { useT } from "../i18n";
import { unwrap } from "../lib/desktop";
import { ActionButton, Input, Select, TextArea } from "./ui";
type Step = "company" | "mission" | "launch";
type Step = "language" | "company" | "mission" | "launch";
export function CompanyOnboarding({
connectors,
onDone,
canCancel = false,
onCancel,
currentLocale,
onLocaleChange,
isFirstLaunch = false,
}: {
connectors: ProfileSnapshot["connectors"];
onDone: (summary: OnboardingBootstrapResult) => Promise<void>;
canCancel?: boolean;
onCancel?: () => void;
currentLocale: AppLocale;
onLocaleChange: (locale: AppLocale) => void;
isFirstLaunch?: boolean;
}) {
const [step, setStep] = useState<Step>("company");
const t = useT();
const [step, setStep] = useState<Step>(isFirstLaunch ? "language" : "company");
const [companyName, setCompanyName] = useState("");
const [companyDescription, setCompanyDescription] = useState("");
const [workspacePath, setWorkspacePath] = useState("");
@@ -60,11 +71,11 @@ export function CompanyOnboarding({
}, [executionReadyConnectors, selectedConnectorId]);
const stepLabels = [
{ label: "Company", icon: Buildings },
{ label: "Mission", icon: Target },
{ label: "Launch", icon: Rocket },
{ label: t("onboarding.stepLabel1"), icon: Buildings },
{ label: t("onboarding.stepLabel2"), icon: Target },
{ label: t("onboarding.stepLabel3"), icon: Rocket },
] as const;
const stepIndex = step === "company" ? 0 : step === "mission" ? 1 : 2;
const stepIndex = step === "language" ? -1 : step === "company" ? 0 : step === "mission" ? 1 : 2;
const pickDirectory = useCallback(async () => {
setError(null);
@@ -103,21 +114,21 @@ export function CompanyOnboarding({
const continueToMission = useCallback(async () => {
if (loading) return;
if (!companyName.trim()) {
setError("Enter a company name.");
setError(t("onboarding.enterCompanyName"));
return;
}
if (!selectedConnector) {
setError("At least one execution-ready connector is required before you can continue.");
setError(t("onboarding.connectorRequired"));
return;
}
setError(null);
setStep("mission");
}, [companyName, loading, selectedConnector]);
}, [companyName, loading, selectedConnector, t]);
const launchCompany = useCallback(async () => {
if (loading) return;
if (!goalTitle.trim()) {
setError("Describe the first real company goal.");
setError(t("onboarding.describeFirstGoal"));
return;
}
setLoading(true);
@@ -125,11 +136,11 @@ export function CompanyOnboarding({
try {
await bootstrap();
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to bootstrap the company.");
setError(submitError instanceof Error ? submitError.message : t("onboarding.unableToBootstrap"));
} finally {
setLoading(false);
}
}, [bootstrap, goalTitle, loading]);
}, [bootstrap, goalTitle, loading, t]);
const finishLaunch = useCallback(async () => {
if (!launchSummary) return;
@@ -138,11 +149,11 @@ export function CompanyOnboarding({
try {
await onDone(launchSummary);
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to open the control center.");
setError(submitError instanceof Error ? submitError.message : t("onboarding.unableToOpenControlCenter"));
} finally {
setLoading(false);
}
}, [launchSummary, onDone]);
}, [launchSummary, onDone, t]);
return (
<div className="grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
@@ -154,52 +165,56 @@ export function CompanyOnboarding({
className="mx-auto w-full max-w-[460px]"
>
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
AgentCompany Desktop
{t("onboarding.subtitle")}
</div>
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
Start a real local-first
<br />
agent company.
{t("onboarding.title").split("\n").map((line, i) => (
<span key={i}>{line}{i === 0 ? <br /> : null}</span>
))}
</h1>
<p className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]">
This flow creates one company, binds one execution-ready connector, links an optional workspace, writes
the first goal, creates the operating project, and queues the first execution task.
{t("onboarding.description")}
</p>
<div className="mt-10 flex items-center gap-3">
{stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2">
{index > 0 ? (
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
) : null}
<div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
index < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: index === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}
>
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
{entry.label}
{step !== "language" ? (
<div className="mt-10 flex items-center gap-3">
{stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2">
{index > 0 ? (
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
) : null}
<div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
index < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: index === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}
>
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
{entry.label}
</div>
</div>
</div>
))}
</div>
))}
</div>
) : null}
<div className="mt-10">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Connector readiness
{t("onboarding.connectorReadiness")}
</div>
<div className="space-y-2.5">
{connectors.map((connector) => {
const issue = getConnectorExecutionReadinessIssue(connector);
const isRunnable = !issue;
const isChecking = connector.status === "detected";
const isReady = connector.status === "ready";
const isMissing = connector.status === "not_installed";
const issue = !isChecking ? getConnectorExecutionReadinessIssue(connector) : null;
return (
<div key={connector.id} className="flex items-start gap-2.5 text-[13px]">
{isRunnable ? (
{isChecking ? (
<span className="mt-0.5 h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-[color:var(--muted)] border-t-[color:var(--accent)]" />
) : isReady ? (
<CheckCircle size={16} weight="fill" className="shrink-0 text-[color:var(--success)]" />
) : isMissing ? (
<XCircle size={16} weight="fill" className="shrink-0 text-[color:var(--muted)]" />
@@ -208,11 +223,11 @@ export function CompanyOnboarding({
)}
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={isRunnable ? "font-medium text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
<span className={isReady ? "font-medium text-[color:var(--text)]" : isChecking ? "text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
{connector.label}
</span>
<span className="text-[11px] text-[color:var(--muted)]">
{connector.status.replaceAll("_", " ")}
{isChecking ? (currentLocale === "zh" ? "检测中..." : "checking...") : connector.status.replaceAll("_", " ")}
</span>
</div>
{issue ? (
@@ -227,7 +242,7 @@ export function CompanyOnboarding({
</div>
{!hasRunnableConnector ? (
<div className="mt-4 rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
Install, authenticate, and fully validate at least one supported CLI runtime before creating the company.
{t("onboarding.connectorWarning")}
</div>
) : null}
</div>
@@ -242,11 +257,68 @@ export function CompanyOnboarding({
onClick={onCancel}
className="absolute right-6 top-6 text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
Cancel
{t("onboarding.cancel")}
</button>
) : null}
<AnimatePresence mode="wait">
{step === "language" ? (
<motion.div
key="language"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<GlobeSimple size={12} />
{t("onboarding.languageSelection")} / Language
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
{t("onboarding.selectLanguage")} / Select Language
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-2.5">
{APP_LOCALES.map((locale) => (
<button
key={locale}
type="button"
onClick={() => onLocaleChange(locale)}
className={`flex w-full items-center gap-3 rounded-[10px] border px-4 py-3.5 text-left transition ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]"
: "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"
}`}
>
<div className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent)]"
: "border-[color:var(--line)]"
}`}>
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-white" /> : null}
</div>
<span className={`text-[14px] font-medium ${
currentLocale === locale ? "text-[color:var(--text)]" : "text-[color:var(--muted-strong)]"
}`}>
{LOCALE_LABELS[locale]}
</span>
</button>
))}
</div>
<div className="mt-8 flex items-center justify-end">
<ActionButton
label={t("onboarding.continue")}
onClick={() => setStep("company")}
icon={GlobeSimple}
tone="accent"
/>
</div>
</motion.div>
) : null}
{step === "company" ? (
<motion.div
key="company"
@@ -257,29 +329,29 @@ export function CompanyOnboarding({
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Step 1 of 3
{t("onboarding.stepLabel1")} 1 / 3
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
Create the operating boundary
{t("onboarding.step1Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input
label="Company name"
label={t("onboarding.companyName")}
value={companyName}
onChange={setCompanyName}
placeholder="e.g. Northline Systems"
placeholder={t("onboarding.companyNamePlaceholder")}
/>
<TextArea
label="Company description"
label={t("onboarding.companyDescription")}
value={companyDescription}
onChange={setCompanyDescription}
placeholder="Optional context for the company operating model."
placeholder={t("onboarding.companyDescriptionPlaceholder")}
rows={3}
/>
<Select
label="Primary connector"
label={t("onboarding.primaryConnector")}
value={selectedConnector?.id ?? ""}
onChange={setSelectedConnectorId}
options={executionReadyConnectors.map((connector) => ({
@@ -288,28 +360,27 @@ export function CompanyOnboarding({
}))}
/>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
Only connectors in `ready` state with session resume and structured transcripts are offered here.
Re-run connector health checks from Settings if the list is empty.
{t("onboarding.connectorInfo")}
</div>
<div>
<label
htmlFor="onboarding-workspace-path"
className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]"
>
Workspace folder <span className="font-normal text-[color:var(--muted)]">(optional)</span>
{t("onboarding.workspaceFolder")} <span className="font-normal text-[color:var(--muted)]">{t("onboarding.workspaceFolderOptional")}</span>
</label>
<div className="flex gap-3">
<Input
id="onboarding-workspace-path"
ariaLabel="Workspace folder"
ariaLabel={t("onboarding.workspaceFolder")}
value={workspacePath}
onChange={setWorkspacePath}
placeholder="/Users/you/project"
placeholder={t("onboarding.workspaceFolderPlaceholder")}
/>
<ActionButton label="Choose" onClick={() => void pickDirectory()} icon={FolderOpen} />
<ActionButton label={currentLocale === "zh" ? "选择" : "Choose"} onClick={() => void pickDirectory()} icon={FolderOpen} />
</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
If set, the first execution task will be bound to this path and the path will be registered as the primary workspace.
{t("onboarding.workspaceHelp")}
</div>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
@@ -321,9 +392,9 @@ export function CompanyOnboarding({
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
/>
<div>
<div className="text-[13px] font-medium text-[color:var(--text)]">Auto-approve hire and strategy approvals</div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{t("onboarding.autoApproveLabel")}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
Useful for single-operator flows. Leave it off if you want the board to gate organization changes explicitly.
{t("onboarding.autoApproveHelp")}
</div>
</div>
</label>
@@ -336,9 +407,18 @@ export function CompanyOnboarding({
</div>
) : null}
<div className="mt-8 flex items-center justify-end">
<div className="mt-8 flex items-center justify-between">
{isFirstLaunch ? (
<button
type="button"
onClick={() => setStep("language")}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
{t("onboarding.back")}
</button>
) : <div />}
<ActionButton
label="Continue"
label={t("onboarding.continue")}
onClick={() => void continueToMission()}
icon={Buildings}
tone="accent"
@@ -358,39 +438,36 @@ export function CompanyOnboarding({
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Step 2 of 3
{t("onboarding.stepLabel2")} 2 / 3
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
Define the first real goal
</div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">
The first goal seeds the operating project and the first task. This should be specific enough that the CEO agent can decompose it into concrete work.
{t("onboarding.step2Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input
label="Goal"
label={t("onboarding.goal")}
value={goalTitle}
onChange={setGoalTitle}
placeholder="e.g. Ship the first customer-ready desktop demo"
placeholder={t("onboarding.goalPlaceholder")}
/>
<TextArea
label="Acceptance criteria"
label={t("onboarding.acceptanceCriteria")}
value={goalDescription}
onChange={setGoalDescription}
placeholder="Describe what successful execution looks like, what cannot be skipped, and any delivery constraints."
placeholder={t("onboarding.acceptanceCriteriaPlaceholder")}
rows={5}
/>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] text-[color:var(--muted)]">
<div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">
<Cpu size={12} />
Execution handoff
{t("onboarding.executionHandoff")}
</div>
<div className="space-y-2">
<div>The selected connector becomes the CEO agent runtime for the first launch.</div>
<div>The app will create the company, the primary workspace, the CEO agent, the kickoff project, and the first task.</div>
<div>If the worker is already ready, the first run will be queued immediately; otherwise the scheduler will pick it up once the runtime is available.</div>
<div>{t("onboarding.executionHandoffLine1")}</div>
<div>{t("onboarding.executionHandoffLine2")}</div>
<div>{t("onboarding.executionHandoffLine3")}</div>
</div>
</div>
</div>
@@ -407,10 +484,10 @@ export function CompanyOnboarding({
onClick={() => setStep("company")}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
Back
{t("onboarding.back")}
</button>
<ActionButton
label={loading ? "Launching..." : "Launch Company"}
label={loading ? t("onboarding.launching") : t("onboarding.launchCompany")}
onClick={() => void launchCompany()}
icon={Lightning}
tone="accent"
@@ -433,42 +510,41 @@ export function CompanyOnboarding({
<CheckCircle size={24} weight="fill" />
</div>
<h2 className="mt-5 text-[28px] tracking-[-0.03em] text-[color:var(--text)]">
{launchSummary.companyName} is live.
{launchSummary.companyName} {t("onboarding.isLive")}
</h2>
<p className="mt-3 text-[14px] leading-relaxed text-[color:var(--muted)]">
The company shell is created. The first project and task are registered, and the kickoff execution is
{launchSummary.runId ? " queued." : " prepared for deferred execution once the runtime is ready."}
{launchSummary.runId ? t("onboarding.launchDescription") : t("onboarding.launchDescriptionDeferred")}
</p>
<div className="mt-6 grid gap-3 md:grid-cols-2">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Connector</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.connector")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{selectedConnector?.label ?? "Unavailable"}
{selectedConnector?.label ?? t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Lead agent</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.leadAgent")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.leadAgentId ? "CEO agent created" : "Lead agent unavailable"}
{launchSummary.leadAgentId ? t("onboarding.ceoAgentCreated") : t("onboarding.leadAgentUnavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Primary workspace</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.primaryWorkspace")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.workspaceId ? "Registered" : "Not bound"}
{launchSummary.workspaceId ? t("onboarding.registered") : t("onboarding.notBound")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Kickoff project</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.kickoffProject")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.projectId ? "Created" : "Unavailable"}
{launchSummary.projectId ? t("onboarding.created") : t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Initial run</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.initialRun")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.runId ? "Queued in orchestration" : "Deferred until runtime is ready"}
{launchSummary.runId ? t("onboarding.queuedInOrchestration") : t("onboarding.deferredUntilReady")}
</div>
</div>
</div>
@@ -481,7 +557,7 @@ export function CompanyOnboarding({
<div className="mt-8 flex items-center justify-end">
<ActionButton
label={loading ? "Opening..." : "Open Control Center"}
label={loading ? t("onboarding.opening") : t("onboarding.openControlCenter")}
onClick={() => void finishLaunch()}
icon={Rocket}
tone="accent"
+3 -1
View File
@@ -29,6 +29,7 @@ import type {
import { timeAgo } from "../lib/formatters"
import { DEPARTMENT_OPTIONS } from "@shared/constants"
import { ActionButton, Select } from "./ui"
import { useT } from "../i18n"
const ACCENT = "var(--accent)"
const LINE = "var(--line)"
@@ -1611,6 +1612,7 @@ export function AutomationPanel({
companyId,
onRefresh,
}: AutomationPanelProps) {
const t = useT()
const [activeSection, setActiveSection] = useState<"rules" | "workflows">("rules")
return (
@@ -1630,7 +1632,7 @@ export function AutomationPanel({
<Robot size={20} weight="fill" />
</div>
<div>
<div className="text-[13px] font-semibold text-[color:var(--text)]">Automation Engine</div>
<div className="text-[13px] font-semibold text-[color:var(--text)]">{t("auto.automationEngine")}</div>
<div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">
Rules trigger actions based on company events. Workflows chain multiple steps into pipelines.
Together, they let your AI company run fully autonomously -- responding to changes, routing work,
+47 -43
View File
@@ -34,6 +34,7 @@ import { getAgentInitials, formatMessageTime } from "../lib/formatters"
import { DEPARTMENT_COLORS } from "@shared/constants"
import { unwrap } from "../lib/desktop"
import { InlineNotice, Modal, Select, Input, TextArea } from "./ui"
import { useT } from "../i18n"
const TERRACOTTA = "#d97745"
const TERRACOTTA_SOFT = "#d9774518"
@@ -815,6 +816,7 @@ interface ComposeModalProps {
}
function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: ComposeModalProps) {
const t = useT()
const [fromAgentId, setFromAgentId] = useState(agents[0]?.id ?? "")
const [toAgentId, setToAgentId] = useState("")
const [channel, setChannel] = useState<MessageChannel>("company")
@@ -893,11 +895,11 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
}, [agents, body, channel, channelTargetId, companyId, fromAgentId, onClose, onSent, priority, subject, toAgentId])
return (
<Modal open={open} title="Compose Message" onClose={onClose} width="560px">
<Modal open={open} title={t("comm.composeMessage")} onClose={onClose} width="560px">
<div className="space-y-4">
{sendError ? <InlineNotice message={sendError} /> : null}
<Select
label="From Agent"
label={t("comm.fromAgent")}
value={fromAgentId}
onChange={setFromAgentId}
options={agents.map((a) => ({
@@ -907,15 +909,15 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
/>
<Select
label="Channel"
label={t("comm.channel")}
value={channel}
onChange={(v) => setChannel(v as MessageChannel)}
options={[
{ value: "company", label: "Company-wide broadcast" },
{ value: "department", label: "Department channel" },
{ value: "project", label: "Project channel" },
{ value: "incident", label: "Incident channel" },
{ value: "direct", label: "Direct message" },
{ value: "company", label: t("comm.companyBroadcast") },
{ value: "department", label: t("comm.departmentChannel") },
{ value: "project", label: t("comm.projectChannel") },
{ value: "incident", label: t("comm.incidentChannel") },
{ value: "direct", label: t("comm.directMessage") },
]}
/>
@@ -963,17 +965,17 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
) : null}
<Input
label="Subject"
label={t("comm.subject")}
value={subject}
onChange={setSubject}
placeholder="Message subject"
placeholder={t("comm.messageSubject")}
/>
<TextArea
label="Message"
label={t("comm.message")}
value={body}
onChange={setBody}
placeholder="Write your message..."
placeholder={t("comm.writeMessage")}
rows={5}
/>
@@ -994,7 +996,7 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
onClick={onClose}
className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]"
>
Cancel
{t("common.cancel")}
</button>
<button
type="button"
@@ -1018,7 +1020,7 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
) : (
<PaperPlaneTilt size={14} weight="fill" />
)}
Send Message
{t("comm.sendMessage")}
</button>
</div>
</div>
@@ -1040,6 +1042,7 @@ function FilterBar({
filters: FilterState
onChangeFilters: (next: FilterState) => void
}) {
const t = useT()
const [expanded, setExpanded] = useState(false)
return (
@@ -1057,7 +1060,7 @@ function FilterBar({
onChange={(e) =>
onChangeFilters({ ...filters, search: e.target.value })
}
placeholder="Search messages..."
placeholder={t("comm.searchMessages")}
className="w-full rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] py-1.5 pl-8 pr-3 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-1"
style={{ focusRingColor: TERRACOTTA } as React.CSSProperties}
/>
@@ -1079,7 +1082,7 @@ function FilterBar({
}
>
<Funnel size={12} />
Filters
{t("comm.filters")}
</button>
</div>
@@ -1102,12 +1105,12 @@ function FilterBar({
}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
>
<option value="all">All channels</option>
<option value="direct">Direct</option>
<option value="department">Department</option>
<option value="company">Company</option>
<option value="project">Project</option>
<option value="incident">Incident</option>
<option value="all">{t("comm.allChannels")}</option>
<option value="direct">{t("comm.direct")}</option>
<option value="department">{t("comm.department")}</option>
<option value="company">{t("comm.company")}</option>
<option value="project">{t("nav.projects")}</option>
<option value="incident">{t("comm.incident")}</option>
</select>
<select
@@ -1120,10 +1123,10 @@ function FilterBar({
}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
>
<option value="all">All priorities</option>
<option value="urgent">Urgent</option>
<option value="normal">Normal</option>
<option value="low">Low</option>
<option value="all">{t("comm.allPriorities")}</option>
<option value="urgent">{t("comm.urgent")}</option>
<option value="normal">{t("comm.normal")}</option>
<option value="low">{t("comm.low")}</option>
</select>
<button
@@ -1138,7 +1141,7 @@ function FilterBar({
}`}
style={filters.unreadOnly ? { backgroundColor: TERRACOTTA } : undefined}
>
Unread only
{t("comm.unreadOnly")}
</button>
{(filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly) ? (
@@ -1154,7 +1157,7 @@ function FilterBar({
}
className="text-[10px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
>
Clear filters
{t("comm.clearFilters")}
</button>
) : null}
</motion.div>
@@ -1189,6 +1192,7 @@ export function CommunicationHub({
onNavigateToTask,
onRefresh,
}: CommunicationHubProps) {
const t = useT()
const [selectedChannelKey, setSelectedChannelKey] = useState<string | null>(null)
const [showSidebar, setShowSidebar] = useState(true)
const [showCompose, setShowCompose] = useState(false)
@@ -1447,7 +1451,7 @@ export function CommunicationHub({
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">
Communication Hub
{t("comm.communicationHub")}
</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{messages.length} message{messages.length !== 1 ? "s" : ""}
@@ -1464,7 +1468,7 @@ export function CommunicationHub({
style={{ backgroundColor: TERRACOTTA }}
>
<Plus size={13} weight="bold" />
New message
{t("comm.newMessage")}
</button>
</div>
@@ -1486,7 +1490,7 @@ export function CommunicationHub({
{/* Sidebar header */}
<div className="flex items-center justify-between border-b border-[color:var(--line)] px-4 py-3">
<span className="text-[13px] font-semibold text-[color:var(--text)]">
Channels
{t("comm.channels")}
</span>
<button
type="button"
@@ -1527,7 +1531,7 @@ export function CommunicationHub({
{taskDiscussionChannels.length > 0 ? (
<div className="mb-3">
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
Task Discussions
{t("comm.taskDiscussions")}
</div>
{taskDiscussionChannels.map((discussion) => (
<button
@@ -1551,7 +1555,7 @@ export function CommunicationHub({
{channels.length === 0 ? (
<div className="px-3 py-6 text-center text-[11px] text-[color:var(--muted)]">
No active departments yet.
{t("comm.noActiveDepartments")}
</div>
) : null}
</div>
@@ -1622,7 +1626,7 @@ export function CommunicationHub({
</>
) : (
<span className="text-[13px] text-[color:var(--muted)]">
Select a channel
{t("comm.selectChannel")}
</span>
)}
@@ -1641,10 +1645,10 @@ export function CommunicationHub({
className="mb-3 text-[color:var(--muted)]"
/>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
Select a channel to view messages
{t("comm.selectChannelView")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
Monitor internal agent communications across departments
{t("comm.monitorComms")}
</div>
</div>
) : activeChannel.kind === "task" ? (
@@ -1652,10 +1656,10 @@ export function CommunicationHub({
<div className="flex h-full flex-col items-center justify-center text-center">
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
No discussion yet
{t("comm.noDiscussionYet")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
Comments for this task will appear here as agents and the board coordinate.
{t("comm.commentsAppearHere")}
</div>
</div>
) : (
@@ -1675,13 +1679,13 @@ export function CommunicationHub({
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
{filters.search || filters.unreadOnly || filters.priority !== "all"
? "No messages match your filters"
: "No messages in this channel yet"}
? t("comm.noMatchFilters")
: t("comm.noMessagesChannel")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
{filters.search || filters.unreadOnly || filters.priority !== "all"
? "Try adjusting your search or filters"
: "Messages will appear here as agents work and communicate"}
? t("comm.adjustFilters")
: t("comm.messagesAppear")}
</div>
</div>
) : (
@@ -1746,7 +1750,7 @@ export function CommunicationHub({
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-2.5">
<div className="flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
<ChatCircle size={14} />
<span>Select a channel to read and send messages.</span>
<span>{t("comm.selectChannelSend")}</span>
</div>
</div>
)}
@@ -19,6 +19,7 @@ import type {
SprintRecord,
} from "@shared/types";
import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, Modal, Select, StatusPill, TextArea } from "./ui";
import { useT } from "../i18n";
const documentTypeOptions = [
"prd",
@@ -450,13 +451,14 @@ export function DocumentCenter({
goals: GoalRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeDocument = useMemo(() => documents.find((document) => document.id === editingId) ?? null, [documents, editingId]);
return (
<div>
<SectionHeader title="Document Center" count={documents.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.documentCenter")} count={documents.length} action={() => setEditingId("__new__")} />
<ListFrame>
{documents.map((document) => (
<ListRow key={document.id}>
@@ -521,13 +523,14 @@ export function KnowledgeBaseManager({
entries: KnowledgeEntryRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeEntry = useMemo(() => entries.find((entry) => entry.id === editingId) ?? null, [entries, editingId]);
return (
<div>
<SectionHeader title="Knowledge Base" count={entries.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.knowledgeBase")} count={entries.length} action={() => setEditingId("__new__")} />
<ListFrame>
{entries.map((entry) => (
<ListRow key={entry.id}>
@@ -591,13 +594,14 @@ export function MeetingsManager({
agents: AgentRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeMeeting = useMemo(() => meetings.find((meeting) => meeting.id === editingId) ?? null, [meetings, editingId]);
return (
<div>
<SectionHeader title="Meetings" count={meetings.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.meetings")} count={meetings.length} action={() => setEditingId("__new__")} />
<ListFrame>
{meetings.map((meeting) => (
<ListRow key={meeting.id}>
@@ -660,13 +664,14 @@ export function SprintManager({
sprints: SprintRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeSprint = useMemo(() => sprints.find((sprint) => sprint.id === editingId) ?? null, [sprints, editingId]);
return (
<div>
<SectionHeader title="Sprint Board" count={sprints.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.sprintBoard")} count={sprints.length} action={() => setEditingId("__new__")} />
<ListFrame>
{sprints.map((sprint) => (
<ListRow key={sprint.id}>
+5 -3
View File
@@ -3,6 +3,7 @@ import { CheckCircle, SignIn } from "@phosphor-icons/react";
import type { ConnectorRecord } from "@shared/types";
import { unwrap } from "../lib/desktop";
import { ActionButton, InlineNotice, Input, Select, StatusPill, TextArea } from "./ui";
import { useT } from "../i18n";
function getAuthActionCopy(connector: ConnectorRecord) {
switch (connector.id) {
@@ -40,6 +41,7 @@ function getAuthActionCopy(connector: ConnectorRecord) {
}
export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRecord; onSaved: () => Promise<void> }) {
const t = useT();
const [model, setModel] = useState(connector.model ?? "");
const [envBindingText, setEnvBindingText] = useState(connector.envBindingText);
const [notes, setNotes] = useState(connector.notes);
@@ -173,14 +175,14 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
) : null}
<div className="grid gap-4 md:grid-cols-2">
<div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">Status</div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("conn.status")}</div>
<div className="flex items-center gap-3">
<StatusPill status={connector.status} />
<span className="text-[12px] text-[color:var(--muted)]">{connector.version ?? "version unknown"}</span>
</div>
</div>
<div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">Capability matrix</div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("conn.capabilityMatrix")}</div>
<div className="grid grid-cols-2 gap-2">
{Object.entries(connector.capabilityMatrix).map(([key, value]) => (
<div key={key} className={`rounded-[6px] border px-3 py-2 text-[12px] ${value ? "border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] text-[color:var(--accent)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)] text-[color:var(--muted)]"}`}>
@@ -233,7 +235,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
) : null}
<div className="flex gap-3">
<ActionButton
label="Save connector"
label={t("conn.saveConnector")}
tone="accent"
onClick={() => void (async () => {
setSaving(true);
+25 -23
View File
@@ -42,6 +42,7 @@ import type {
import { isConnectorExecutionReady } from "@shared/connector-policy";
import { formatMoney, getAgentInitials, timeAgo } from "../lib/formatters";
import { StatusPill } from "./ui";
import { useT } from "../i18n";
interface DashboardProps {
agents: AgentRecord[];
@@ -589,6 +590,7 @@ function GoalProgress({ goals, tasks, onNavigate }: { goals: GoalRecord[]; tasks
}
export function Dashboard(props: DashboardProps) {
const t = useT();
const { agents, tasks, runs, approvals, goals, projects, activity, costs, connectors, messages = [], browserActions = [], workflows = [], sprints = [], onNavigate } = props;
const healthScore = useMemo(
@@ -638,51 +640,51 @@ export function Dashboard(props: DashboardProps) {
{/* Metric Cards */}
<div className="grid flex-1 grid-cols-2 gap-2 lg:grid-cols-3">
<MetricCard
label="Live Runs"
label={t("dash.liveRuns")}
value={liveRuns.length}
icon={Heartbeat}
tone={liveRuns.length > 0 ? "success" : "default"}
onClick={() => onNavigate("runs")}
subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} running` : "All quiet"}
subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} ${t("dash.running")}` : t("dash.allQuiet")}
/>
<MetricCard
label="Active Tasks"
label={t("dash.activeTasks")}
value={activeTasks.length}
icon={Briefcase}
tone={activeTasks.length > 0 ? "accent" : "default"}
onClick={() => onNavigate("tasks")}
subtitle={`${tasks.filter((t) => t.status === "todo").length} queued`}
subtitle={`${tasks.filter((t) => t.status === "todo").length} ${t("dash.queued")}`}
/>
<MetricCard
label="Approvals"
label={t("dash.approvals")}
value={pendingApprovals.length}
icon={ShieldCheck}
tone={pendingApprovals.length > 0 ? "warn" : "default"}
onClick={() => onNavigate("approvals")}
subtitle={pendingApprovals.length > 0 ? "Needs your attention" : "All clear"}
subtitle={pendingApprovals.length > 0 ? t("dash.needsAttention") : t("dash.allClear")}
/>
<MetricCard
label="Failed Runs"
label={t("dash.failedRuns")}
value={failedRuns.length}
icon={Warning}
tone={failedRuns.length > 0 ? "danger" : "default"}
onClick={() => onNavigate("runs")}
/>
<MetricCard
label="Total Spend"
label={t("dash.totalSpend")}
value={formatMoney(totalSpend)}
icon={CurrencyCircleDollar}
onClick={() => onNavigate("costs")}
/>
<MetricCard
label="Projects"
label={t("dash.projects")}
value={projects.filter((p) => p.status === "in_progress").length}
icon={Target}
onClick={() => onNavigate("projects")}
subtitle={`${projects.length} total`}
subtitle={`${projects.length} ${t("dash.total")}`}
/>
<MetricCard
label="Unread Messages"
label={t("dash.unreadMessages")}
value={unreadMessages.length}
icon={Users}
tone={urgentMessages.length > 0 ? "warn" : unreadMessages.length > 0 ? "accent" : "default"}
@@ -690,7 +692,7 @@ export function Dashboard(props: DashboardProps) {
subtitle={urgentMessages.length > 0 ? `${urgentMessages.length} urgent` : unreadMessages.length > 0 ? "Needs triage" : "All read"}
/>
<MetricCard
label="Browser Ops"
label={t("dash.browserOps")}
value={blockedBrowserActions.length}
icon={Cpu}
tone={blockedBrowserActions.length > 0 ? "warn" : "default"}
@@ -698,7 +700,7 @@ export function Dashboard(props: DashboardProps) {
subtitle={blockedBrowserActions.length > 0 ? "Blocked or failed" : "Nominal"}
/>
<MetricCard
label="Workflow Stalls"
label={t("dash.workflowStalls")}
value={stalledWorkflows.length}
icon={Lightning}
tone={stalledWorkflows.length > 0 ? "warn" : "default"}
@@ -711,13 +713,13 @@ export function Dashboard(props: DashboardProps) {
{/* Department Overview */}
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Departments</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.departments")}</div>
<button
type="button"
onClick={() => onNavigate("orgchart")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
Org chart <TreeStructure size={12} />
{t("dash.orgChart")} <TreeStructure size={12} />
</button>
</div>
<DepartmentOverview agents={agents} tasks={tasks} runs={runs} onNavigate={onNavigate} />
@@ -728,7 +730,7 @@ export function Dashboard(props: DashboardProps) {
{activeSprint ? (
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Current Sprint</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.currentSprint")}</div>
<button
type="button"
onClick={() => onNavigate("sprints")}
@@ -743,7 +745,7 @@ export function Dashboard(props: DashboardProps) {
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Goal Progress</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.goalProgress")}</div>
<button
type="button"
onClick={() => onNavigate("goals")}
@@ -759,7 +761,7 @@ export function Dashboard(props: DashboardProps) {
{/* Task Pipeline */}
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Task Pipeline</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.taskPipeline")}</div>
<button
type="button"
onClick={() => onNavigate("tasks")}
@@ -778,13 +780,13 @@ export function Dashboard(props: DashboardProps) {
{/* Workforce */}
<section className="lg:col-span-3">
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Workforce</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.workforce")}</div>
<button
type="button"
onClick={() => onNavigate("orgchart")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
Org chart <TreeStructure size={12} />
{t("dash.orgChart")} <TreeStructure size={12} />
</button>
</div>
<WorkforceGrid agents={agents} onSelectAgent={(id) => onNavigate("agents", id)} />
@@ -793,7 +795,7 @@ export function Dashboard(props: DashboardProps) {
{/* Activity Feed */}
<section className="lg:col-span-2">
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Recent Activity</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.recentActivity")}</div>
<button
type="button"
onClick={() => onNavigate("activity")}
@@ -810,7 +812,7 @@ export function Dashboard(props: DashboardProps) {
{/* Connector Health */}
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Infrastructure</div>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.infrastructure")}</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
{connectors.map((connector) => {
const isReady = isConnectorExecutionReady(connector);
@@ -842,7 +844,7 @@ export function Dashboard(props: DashboardProps) {
{/* Needs Attention */}
{(pendingApprovals.length > 0 || failedRuns.length > 0 || urgentMessages.length > 0 || blockedBrowserActions.length > 0 || stalledWorkflows.length > 0) ? (
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Needs Attention</div>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">{t("dash.needsAttentionSection")}</div>
<div className="space-y-1.5">
{pendingApprovals.slice(0, 5).map((approval) => {
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
+123 -100
View File
@@ -1,4 +1,4 @@
import { useEffect, useState, useMemo } from "react";
import { useCallback, useEffect, useState, useMemo } from "react";
import {
ArrowClockwise,
ArrowSquareOut,
@@ -39,6 +39,9 @@ import type {
TaskStatus,
WorkspaceRecord,
} from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { APP_LOCALES, LOCALE_LABELS } from "@shared/locale";
import { useLocale, useT } from "../i18n";
import { formatMoney, formatTime } from "../lib/formatters";
import { unwrap } from "../lib/desktop";
import { ActionButton, ConfirmDialog, EmptyState, Field, InlineNotice, Input, ListFrame, ListRow, Select, StatusPill, TextArea } from "./ui";
@@ -120,8 +123,9 @@ export function TaskQueue({
selectedTaskId: string | null;
onSelect: (taskId: string) => void;
}) {
const t = useT();
if (tasks.length === 0) {
return <EmptyState title="Task queue is empty" />;
return <EmptyState title={t("exec.taskQueueEmpty")} />;
}
return (
<ListFrame>
@@ -188,6 +192,7 @@ export function TaskInspector({
onRefresh: () => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const t = useT();
const [editing, setEditing] = useState(false);
const [editTitle, setEditTitle] = useState(task.title);
const [editDescription, setEditDescription] = useState(task.description);
@@ -313,8 +318,8 @@ export function TaskInspector({
<div className="space-y-5">
<ConfirmDialog
open={confirmDelete}
title="Delete task"
message={`Permanently delete "${task.title}"? This cannot be undone.`}
title={t("exec.deleteTask")}
message={`${t("exec.deleteTaskConfirm").replace("{0}", task.title)}`}
errorMessage={deleteError}
onConfirm={() => void handleDelete()}
onCancel={() => {
@@ -342,20 +347,20 @@ export function TaskInspector({
{editing ? (
<div className="grid gap-3 border-y border-[color:var(--line)] py-4 md:grid-cols-2">
<div className="md:col-span-2">
<Input label="Title" value={editTitle} onChange={setEditTitle} placeholder="Task title" />
<Input label={t("exec.title")} value={editTitle} onChange={setEditTitle} placeholder={t("exec.taskTitle")} />
</div>
<div className="md:col-span-2">
<TextArea label="Description" value={editDescription} onChange={setEditDescription} placeholder="Description" rows={4} />
<TextArea label={t("exec.description")} value={editDescription} onChange={setEditDescription} placeholder={t("exec.description")} rows={4} />
</div>
<Select label="Priority" value={editPriority} onChange={v => setEditPriority(v as TaskRecord["priority"])} options={["critical", "high", "medium", "low"].map(v => ({ value: v, label: v }))} />
<Select label="Status" value={editStatus} onChange={v => setEditStatus(v as TaskStatus)} options={["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"].map(v => ({ value: v, label: v.replaceAll("_", " ") }))} />
<Select label="Assignee" value={editAssignee} onChange={setEditAssignee} options={[{ value: "", label: "Unassigned" }, ...agents.map(a => ({ value: a.id, label: a.name }))]} />
<Select label="Workspace" value={editWorkspace} onChange={setEditWorkspace} options={[{ value: "", label: "No workspace" }, ...workspaces.map(w => ({ value: w.id, label: w.name }))]} />
<Select label="Project" value={editProject} onChange={setEditProject} options={[{ value: "", label: "No project" }, ...projects.map(p => ({ value: p.id, label: p.name }))]} />
<Select label="Goal" value={editGoal} onChange={setEditGoal} options={[{ value: "", label: "No goal" }, ...goals.map(g => ({ value: g.id, label: g.title }))]} />
<Select label={t("exec.priority")} value={editPriority} onChange={v => setEditPriority(v as TaskRecord["priority"])} options={["critical", "high", "medium", "low"].map(v => ({ value: v, label: v }))} />
<Select label={t("exec.status")} value={editStatus} onChange={v => setEditStatus(v as TaskStatus)} options={["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"].map(v => ({ value: v, label: v.replaceAll("_", " ") }))} />
<Select label={t("exec.assignee")} value={editAssignee} onChange={setEditAssignee} options={[{ value: "", label: t("exec.unassigned") }, ...agents.map(a => ({ value: a.id, label: a.name }))]} />
<Select label={t("exec.workspace")} value={editWorkspace} onChange={setEditWorkspace} options={[{ value: "", label: t("exec.noWorkspace") }, ...workspaces.map(w => ({ value: w.id, label: w.name }))]} />
<Select label={t("exec.project")} value={editProject} onChange={setEditProject} options={[{ value: "", label: t("exec.noProject") }, ...projects.map(p => ({ value: p.id, label: p.name }))]} />
<Select label={t("exec.goal")} value={editGoal} onChange={setEditGoal} options={[{ value: "", label: t("exec.noGoal") }, ...goals.map(g => ({ value: g.id, label: g.title }))]} />
<div className="md:col-span-2 flex gap-3">
<ActionButton label="Save" tone="accent" onClick={() => void saveChanges()} icon={CheckCircle} loading={savingChanges} />
<ActionButton label="Cancel" onClick={() => setEditing(false)} icon={XCircle} />
<ActionButton label={t("exec.save")} tone="accent" onClick={() => void saveChanges()} icon={CheckCircle} loading={savingChanges} />
<ActionButton label={t("exec.cancel")} onClick={() => setEditing(false)} icon={XCircle} />
</div>
</div>
) : (
@@ -389,16 +394,16 @@ export function TaskInspector({
<div className="rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3">
<div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--warn)]">
<ShieldCheck size={16} weight="fill" />
{task.status === "blocked" ? "Governance action required" : "Task is waiting on review"}
{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}
</div>
<div className="mt-1 text-[13px] leading-relaxed text-[color:var(--muted-strong)]">
{task.status === "blocked"
? "Create an approval so leadership can resolve the blocker, provide credentials, or authorize the next privileged action."
: "Create an approval entry if this task needs an explicit operator decision before it can move forward."}
? t("exec.blockedGovernance")
: t("exec.reviewGovernance")}
</div>
<div className="mt-3">
<ActionButton
label={task.status === "blocked" ? "Create unblock approval" : "Create review approval"}
label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")}
onClick={() => void handleCreateApproval()}
icon={ShieldCheck}
loading={creatingApproval}
@@ -410,23 +415,23 @@ export function TaskInspector({
{/* Contextual actions — only show what makes sense for current state */}
<div className="flex items-center gap-2 pt-1">
{activeRun ? (
<ActionButton label="Cancel run" onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" />
<ActionButton label={t("exec.cancelRun")} onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" />
) : (
<>
{["todo", "in_progress", "blocked"].includes(task.status) && (
<ActionButton label="Run" onClick={() => void onRun(task.id)} icon={Play} tone="accent" />
<ActionButton label={t("exec.run")} onClick={() => void onRun(task.id)} icon={Play} tone="accent" />
)}
{task.status === "in_review" && (
<ActionButton label="Done" onClick={() => void quickStatus("done")} icon={CheckCircle} tone="accent" loading={updatingStatus === "done"} />
<ActionButton label={t("exec.done")} onClick={() => void quickStatus("done")} icon={CheckCircle} tone="accent" loading={updatingStatus === "done"} />
)}
{task.status === "blocked" && (
<ActionButton label="Unblock" onClick={() => void quickStatus("in_progress")} icon={Play} loading={updatingStatus === "in_progress"} />
<ActionButton label={t("exec.unblock")} onClick={() => void quickStatus("in_progress")} icon={Play} loading={updatingStatus === "in_progress"} />
)}
</>
)}
<ActionButton label="Discussion" onClick={() => onOpenDiscussion(task.id)} icon={ArrowRight} />
<ActionButton label="Edit" onClick={startEdit} icon={Pencil} />
<ActionButton label="Delete" onClick={() => setConfirmDelete(true)} icon={Trash} tone="danger" />
<ActionButton label={t("exec.discussion")} onClick={() => onOpenDiscussion(task.id)} icon={ArrowRight} />
<ActionButton label={t("exec.edit")} onClick={startEdit} icon={Pencil} />
<ActionButton label={t("exec.delete")} onClick={() => setConfirmDelete(true)} icon={Trash} tone="danger" />
</div>
</>
)}
@@ -439,7 +444,7 @@ export function TaskInspector({
<div className="space-y-2">
<div className="flex items-center gap-2">
<ListChecks size={14} className="text-[color:var(--muted)]" />
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Subtasks ({subtasks.length})</span>
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.subtasks")} ({subtasks.length})</span>
<span className="text-[10px] text-[color:var(--success)]">
{subtasks.filter((t) => t.status === "done").length}/{subtasks.length} done
</span>
@@ -490,7 +495,7 @@ export function TaskInspector({
{/* Run history — only if there are runs */}
{runs.length > 0 && (
<div className="space-y-2">
<div className="text-[11px] font-medium uppercase tracking-[0.08em] text-[color:var(--muted)]">Run history</div>
<div className="text-[11px] font-medium uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runHistory")}</div>
<div className="space-y-2">
{runs.slice(0, 8).map((run) => {
const runAgent = agents.find((a) => a.id === run.agentId);
@@ -549,6 +554,7 @@ function TaskComments({
comments: CommentRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [newComment, setNewComment] = useState("");
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
@@ -586,11 +592,11 @@ function TaskComments({
<div className="space-y-3">
<div className="flex items-center gap-2">
<Robot size={16} className="text-[color:var(--muted)]" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">Communication</span>
<span className="text-[13px] font-semibold text-[color:var(--text)]">{t("exec.communication")}</span>
<span className="text-[11px] text-[color:var(--muted)]">({comments.length})</span>
</div>
{comments.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">No messages yet. Agents will post updates here during runs.</div>
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noMessagesYet")}</div>
) : (
<div className="max-h-[240px] space-y-2 overflow-y-auto rounded-lg border border-[color:var(--line)] p-3">
{comments.map((comment) => {
@@ -615,12 +621,12 @@ function TaskComments({
<div className="flex gap-2">
<input
className="flex-1 rounded-md border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-[color:var(--accent)] focus:outline-none"
placeholder="Send a message to the team..."
placeholder={t("exec.sendMessage")}
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); void handleSubmit(); } }}
/>
<ActionButton label="Send" tone="accent" onClick={() => void handleSubmit()} icon={ArrowRight} loading={submitting} />
<ActionButton label={t("exec.send")} tone="accent" onClick={() => void handleSubmit()} icon={ArrowRight} loading={submitting} />
</div>
</div>
);
@@ -642,6 +648,7 @@ export function TaskCreator({
workspaces: WorkspaceRecord[];
onSaved: () => Promise<void>;
}) {
const t = useT();
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [goalId, setGoalId] = useState(goals[0]?.id ?? "");
@@ -679,17 +686,17 @@ export function TaskCreator({
return (
<div className="grid gap-4 md:grid-cols-2">
<Input label="Title" value={title} onChange={setTitle} placeholder="Task title" />
<Select label="Priority" value={priority} onChange={setPriority} options={["critical", "high", "medium", "low"].map((value) => ({ value, label: value }))} />
<Input label={t("exec.title")} value={title} onChange={setTitle} placeholder={t("exec.taskTitle")} />
<Select label={t("exec.priority")} value={priority} onChange={setPriority} options={["critical", "high", "medium", "low"].map((value) => ({ value, label: value }))} />
<div className="md:col-span-2"><TextArea label="Description" value={description} onChange={setDescription} placeholder="What should the agent do, and what result should come back into the run summary?" rows={5} /></div>
<Select label="Goal" value={goalId} onChange={setGoalId} options={[{ value: "", label: "No goal" }, ...goals.map((goal) => ({ value: goal.id, label: goal.title }))]} />
<Select label="Project" value={projectId} onChange={setProjectId} options={[{ value: "", label: "No project" }, ...projects.map((project) => ({ value: project.id, label: project.name }))]} />
<Select label="Assignee" value={assigneeAgentId} onChange={setAssigneeAgentId} options={[{ value: "", label: "Unassigned" }, ...agents.map((agent) => ({ value: agent.id, label: agent.name }))]} />
<Select label="Workspace" value={workspaceId} onChange={setWorkspaceId} options={[{ value: "", label: "No workspace" }, ...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name }))]} />
<Select label={t("exec.goal")} value={goalId} onChange={setGoalId} options={[{ value: "", label: t("exec.noGoal") }, ...goals.map((goal) => ({ value: goal.id, label: goal.title }))]} />
<Select label={t("exec.project")} value={projectId} onChange={setProjectId} options={[{ value: "", label: t("exec.noProject") }, ...projects.map((project) => ({ value: project.id, label: project.name }))]} />
<Select label={t("exec.assignee")} value={assigneeAgentId} onChange={setAssigneeAgentId} options={[{ value: "", label: t("exec.unassigned") }, ...agents.map((agent) => ({ value: agent.id, label: agent.name }))]} />
<Select label={t("exec.workspace")} value={workspaceId} onChange={setWorkspaceId} options={[{ value: "", label: t("exec.noWorkspace") }, ...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name }))]} />
{submitError ? <div className="md:col-span-2"><InlineNotice message={submitError} /></div> : null}
<div className="md:col-span-2">
<ActionButton
label="Create task"
label={t("exec.createTask")}
tone="accent"
onClick={() => void handleCreateTask()}
icon={ListChecks}
@@ -702,9 +709,10 @@ export function TaskCreator({
export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agents: AgentRecord[] }) {
const t = useT();
const reviewTasks = tasks.filter((t) => t.status === "in_review");
if (reviewTasks.length === 0) {
return <EmptyState title="No agent reviews in progress" />;
return <EmptyState title={t("exec.noReviewsInProgress")} />;
}
return (
<ListFrame>
@@ -730,8 +738,9 @@ export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agen
export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { approvals: ApprovalRecord[]; selectedApprovalId: string | null; onSelect: (id: string) => void }) {
const t = useT();
if (approvals.length === 0) {
return <EmptyState title="No approvals" />;
return <EmptyState title={t("exec.noApprovals")} />;
}
return (
<ListFrame>
@@ -774,6 +783,7 @@ export function ApprovalInspector({
onDecide: (state: "approved" | "rejected" | "revision_requested", note?: string) => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const t = useT();
const [decisionNote, setDecisionNote] = useState("");
const [confirmDelete, setConfirmDelete] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
@@ -813,8 +823,8 @@ export function ApprovalInspector({
<div className="space-y-5">
<ConfirmDialog
open={confirmDelete}
title="Delete approval"
message={`Permanently delete this approval? This cannot be undone.`}
title={t("exec.deleteApproval")}
message={t("exec.deleteApprovalConfirm")}
errorMessage={actionError}
onConfirm={() => void handleDelete()}
onCancel={() => {
@@ -928,9 +938,9 @@ export function ApprovalInspector({
<div className="flex flex-wrap gap-3">
{!isDecided ? (
<>
<ActionButton label="Approve" tone="accent" onClick={() => void handleDecision("approved")} icon={CheckCircle} loading={acting === "approved"} />
<ActionButton label="Request revision" onClick={() => void handleDecision("revision_requested")} icon={ClockCounterClockwise} loading={acting === "revision_requested"} />
<ActionButton label="Reject" tone="danger" onClick={() => void handleDecision("rejected")} icon={XCircle} loading={acting === "rejected"} />
<ActionButton label={t("exec.approve")} tone="accent" onClick={() => void handleDecision("approved")} icon={CheckCircle} loading={acting === "approved"} />
<ActionButton label={t("exec.requestRevision")} onClick={() => void handleDecision("revision_requested")} icon={ClockCounterClockwise} loading={acting === "revision_requested"} />
<ActionButton label={t("exec.reject")} tone="danger" onClick={() => void handleDecision("rejected")} icon={XCircle} loading={acting === "rejected"} />
</>
) : null}
{isDecided ? (
@@ -949,6 +959,7 @@ export function ApprovalInspector({
export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["costs"]; agents: ProfileSnapshot["agents"]; runs: ProfileSnapshot["runs"] }) {
const t = useT();
const [connectorFilter, setConnectorFilter] = useState("all");
const connectorIds = useMemo(() => {
@@ -964,7 +975,7 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
const total = useMemo(() => filtered.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0), [filtered]);
if (costs.length === 0) {
return <EmptyState title="No cost data yet" />;
return <EmptyState title={t("exec.noCostData")} />;
}
return (
@@ -973,7 +984,7 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
<Select
value={connectorFilter}
onChange={setConnectorFilter}
options={[{ value: "all", label: "All connectors" }, ...connectorIds.map((id) => ({ value: id, label: id }))]}
options={[{ value: "all", label: t("exec.allConnectors") }, ...connectorIds.map((id) => ({ value: id, label: id }))]}
/>
</div>
<ListFrame>
@@ -1066,6 +1077,7 @@ export function BudgetSummary({ agents, costs }: { agents: ProfileSnapshot["agen
export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
const t = useT();
const [search, setSearch] = useState("");
const [entityFilter, setEntityFilter] = useState<string>("all");
const [visibleCount, setVisibleCount] = useState(100);
@@ -1090,7 +1102,7 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
const visible = filtered.slice(0, visibleCount);
if (activity.length === 0) {
return <EmptyState title="No activity yet" />;
return <EmptyState title={t("exec.noActivityYet")} />;
}
return (
<div className="space-y-3">
@@ -1103,18 +1115,18 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2.5 pl-8 pr-3.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search activity..."
placeholder={t("exec.searchActivity")}
/>
</div>
<Select
value={entityFilter}
onChange={setEntityFilter}
options={ENTITY_TYPE_OPTIONS.map((v) => ({ value: v, label: v === "all" ? "All types" : v }))}
options={ENTITY_TYPE_OPTIONS.map((v) => ({ value: v, label: v === "all" ? t("exec.allTypes") : v }))}
/>
</div>
{visible.length === 0 ? (
<EmptyState title="No matching activity" />
<EmptyState title={t("exec.noMatchingActivity")} />
) : (
<>
<ListFrame>
@@ -1167,6 +1179,7 @@ export function WorkspaceList({
onOpen: (path: string) => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const t = useT();
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const toDelete = workspaces.find((w) => w.id === confirmDeleteId) ?? null;
@@ -1183,14 +1196,14 @@ export function WorkspaceList({
}
if (workspaces.length === 0) {
return <EmptyState title="No workspaces yet" />;
return <EmptyState title={t("exec.noWorkspacesYet")} />;
}
return (
<>
<ConfirmDialog
open={confirmDeleteId !== null}
title="Delete workspace"
message={`Delete workspace "${toDelete?.name ?? ""}"? This only removes it from the company registry; files on disk are not affected.`}
title={t("exec.deleteWorkspace")}
message={t("exec.deleteWorkspaceConfirm").replace("{0}", toDelete?.name ?? "")}
errorMessage={deleteError}
onConfirm={() => confirmDeleteId && void handleDelete(confirmDeleteId)}
onCancel={() => {
@@ -1214,9 +1227,9 @@ export function WorkspaceList({
</div>
<div className="flex items-center gap-2">
{workspace.isPrimary ? <StatusPill status="ready" /> : null}
<ActionButton label="Inspect" onClick={() => onSelect(workspace.id)} icon={FolderOpen} />
<ActionButton label="Reveal" onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} />
<ActionButton label="Delete" onClick={() => setConfirmDeleteId(workspace.id)} icon={Trash} tone="danger" />
<ActionButton label={t("exec.inspect")} onClick={() => onSelect(workspace.id)} icon={FolderOpen} />
<ActionButton label={t("exec.reveal")} onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} />
<ActionButton label={t("exec.delete")} onClick={() => setConfirmDeleteId(workspace.id)} icon={Trash} tone="danger" />
</div>
</div>
<div className="text-[12px] text-[color:var(--muted)]">
@@ -1249,8 +1262,9 @@ export function WorkspaceInspector({
project: ProjectRecord | null;
onOpen: (path: string) => Promise<void>;
}) {
const t = useT();
if (!workspace) {
return <EmptyState title="Select a workspace" detail="Choose a workspace to inspect the agents, tasks, runs, and documents bound to it." />;
return <EmptyState title={t("exec.selectWorkspace")} detail={t("exec.selectWorkspaceDetail")} />;
}
const workspaceAgents = agents.filter((agent) => agent.workspaceId === workspace.id);
@@ -1274,34 +1288,34 @@ export function WorkspaceInspector({
{workspace.repoRef ? <span> · Ref: <span className="text-[color:var(--text)]">{workspace.repoRef}</span></span> : null}
</div>
</div>
<ActionButton label="Open folder" onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} />
<ActionButton label={t("exec.openFolder")} onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} />
</div>
</div>
<div className="grid gap-3 md:grid-cols-4">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">Agents</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.agents")}</div>
<div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{workspaceAgents.length}</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">Tasks</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.tasks")}</div>
<div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{workspaceTasks.length}</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">Runs</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runs")}</div>
<div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{workspaceRuns.length}</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">Docs</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.docs")}</div>
<div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{workspaceDocuments.length}</div>
</div>
</div>
<div className="grid gap-4 xl:grid-cols-3">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">Assigned Agents</div>
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.assignedAgents")}</div>
{workspaceAgents.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">No agents are currently bound to this workspace.</div>
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noAgentsBound")}</div>
) : (
<div className="space-y-2">
{workspaceAgents.map((agent) => (
@@ -1318,9 +1332,9 @@ export function WorkspaceInspector({
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">Open Tasks</div>
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.openTasks")}</div>
{workspaceTasks.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">No tasks target this workspace yet.</div>
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noTasksTarget")}</div>
) : (
<div className="space-y-2">
{workspaceTasks.slice(0, 6).map((task) => (
@@ -1337,9 +1351,9 @@ export function WorkspaceInspector({
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">Recent Runs</div>
<div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.recentRuns")}</div>
{workspaceRuns.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">No runs have executed in this workspace.</div>
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noRunsExecuted")}</div>
) : (
<div className="space-y-2">
{workspaceRuns.slice(0, 6).map((run) => (
@@ -1361,6 +1375,7 @@ export function WorkspaceInspector({
export function WorkspaceCreator({ companyId, projects, onSaved }: { companyId: string; projects: ProjectRecord[]; onSaved: () => Promise<void> }) {
const t = useT();
const [name, setName] = useState("");
const [localPath, setLocalPath] = useState("");
const [repoUrl, setRepoUrl] = useState("");
@@ -1400,23 +1415,23 @@ export function WorkspaceCreator({ companyId, projects, onSaved }: { companyId:
return (
<div className="grid gap-4">
<Input label="Workspace name" value={name} onChange={setName} placeholder="Workspace name" />
<Field label="Local path">
<Input label={t("exec.workspaceName")} value={name} onChange={setName} placeholder={t("exec.workspaceName")} />
<Field label={t("exec.localPath")}>
<div className="flex gap-3">
<Input value={localPath} onChange={setLocalPath} placeholder="/Users/you/project" />
<ActionButton
label="Choose"
label={t("exec.choose")}
onClick={() => void handleChooseDirectory()}
icon={FolderOpen}
/>
</div>
</Field>
<Input label="Repository URL" value={repoUrl} onChange={setRepoUrl} placeholder="https://github.com/org/repo" />
<Input label="Branch / ref" value={repoRef} onChange={setRepoRef} placeholder="main" />
<Select label="Project" value={projectId} onChange={setProjectId} options={[{ value: "", label: "No project binding" }, ...projects.map((project) => ({ value: project.id, label: project.name }))]} />
<Input label={t("exec.repoUrl")} value={repoUrl} onChange={setRepoUrl} placeholder="https://github.com/org/repo" />
<Input label={t("exec.branchRef")} value={repoRef} onChange={setRepoRef} placeholder="main" />
<Select label={t("exec.project")} value={projectId} onChange={setProjectId} options={[{ value: "", label: t("exec.noProjectBinding") }, ...projects.map((project) => ({ value: project.id, label: project.name }))]} />
{submitError ? <InlineNotice message={submitError} /> : null}
<ActionButton
label="Add workspace"
label={t("exec.addWorkspace")}
tone="accent"
onClick={() => void handleSaveWorkspace()}
icon={FolderOpen}
@@ -1438,7 +1453,10 @@ export function SettingsPane({
companyId: string;
onSaved: () => Promise<void>;
}) {
const t = useT();
const currentLocale = useLocale();
const [theme, setTheme] = useState(snapshot.theme);
const [selectedLocale, setSelectedLocale] = useState<AppLocale>(snapshot.locale ?? currentLocale);
const [updatesEnabled, setUpdatesEnabled] = useState(snapshot.updatesEnabled);
const [confirmDeleteSecretId, setConfirmDeleteSecretId] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
@@ -1450,7 +1468,8 @@ export function SettingsPane({
useEffect(() => {
setTheme(snapshot.theme);
setUpdatesEnabled(snapshot.updatesEnabled);
}, [snapshot.theme, snapshot.updatesEnabled]);
if (snapshot.locale) setSelectedLocale(snapshot.locale);
}, [snapshot.theme, snapshot.updatesEnabled, snapshot.locale]);
async function handleDeleteSecret(id: string) {
setDeleteSecretError(null);
@@ -1471,7 +1490,7 @@ export function SettingsPane({
setActionNotice(null);
setLoadingAction("save");
try {
unwrap(await window.agentCompany.updateSettings({ theme, updatesEnabled }));
unwrap(await window.agentCompany.updateSettings({ theme, locale: selectedLocale, updatesEnabled }));
await onSaved();
setActionNotice("Settings saved.");
} catch (error) {
@@ -1548,20 +1567,23 @@ export function SettingsPane({
{/* Profile info */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Profile</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.profile")}</div>
<div className="border-y border-[color:var(--line)] py-4">
<div className="mb-2 text-[12px] text-[color:var(--muted)]">Profile path</div>
<div className="mb-2 text-[12px] text-[color:var(--muted)]">{t("exec.profilePath")}</div>
<div className="mono text-[12px] text-[color:var(--muted-strong)]">{snapshot.profilePath}</div>
</div>
</div>
{/* Theme & updates */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Preferences</div>
<Select value={theme} onChange={(value) => setTheme(value as ProfileSnapshot["theme"])} options={[{ value: "system", label: "System" }, { value: "dark", label: "Dark" }, { value: "light", label: "Light" }]} />
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.preferences")}</div>
<Select label={t("settings.theme")} value={theme} onChange={(value) => setTheme(value as ProfileSnapshot["theme"])} options={[{ value: "system", label: t("settings.system") }, { value: "dark", label: t("settings.dark") }, { value: "light", label: t("settings.light") }]} />
<div className="mt-3">
<Select label={t("settings.language")} value={selectedLocale} onChange={(value) => setSelectedLocale(value as AppLocale)} options={APP_LOCALES.map((loc) => ({ value: loc, label: LOCALE_LABELS[loc] }))} />
</div>
<div className="border-y border-[color:var(--line)] py-4 mt-3">
<label className="flex items-center justify-between text-[13px] text-[color:var(--muted-strong)]">
<span>Enable updater checks</span>
<span>{t("exec.enableUpdater")}</span>
<input type="checkbox" checked={updatesEnabled} onChange={(event) => setUpdatesEnabled(event.target.checked)} />
</label>
</div>
@@ -1573,7 +1595,7 @@ export function SettingsPane({
if (!company) return null;
return (
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Company autonomy</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.companyAutonomy")}</div>
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3">
<label className="flex items-center gap-3">
<input
@@ -1597,9 +1619,9 @@ export function SettingsPane({
className="h-4 w-4 rounded accent-[color:var(--accent)]"
/>
<div>
<div className="text-[13px] font-medium text-[color:var(--text)]">Auto-approve hire and strategy approvals</div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.autoApproveLabel")}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
Automatically approve agent hiring requests and CEO strategy approvals so the company can continue operating without waiting on the board for those specific approval types.
{t("exec.autoApproveDesc")}
</div>
</div>
</label>
@@ -1610,18 +1632,18 @@ export function SettingsPane({
{/* Actions */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Actions</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.actions")}</div>
<div className="grid gap-3 md:grid-cols-2">
<ActionButton label="Save settings" tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} />
<ActionButton label="Check for updates" onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} />
<ActionButton label="Backup profile" onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} />
<ActionButton label="Restore profile" onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} />
<ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} />
<ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} />
<ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} />
<ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} />
</div>
</div>
{/* Secret backend info */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Secret backend</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretBackend")}</div>
<div className="border-y border-[color:var(--line)] py-3 text-[13px] text-[color:var(--muted-strong)]">
{snapshot.backend.secretBackend}
</div>
@@ -1629,18 +1651,18 @@ export function SettingsPane({
{/* Secrets table */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Secrets ({secrets.length})</div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secrets")} ({secrets.length})</div>
{secrets.length === 0 ? (
<EmptyState title="No secrets stored" />
<EmptyState title={t("exec.noSecretsStored")} />
) : (
<div className="border-y border-[color:var(--line)] overflow-x-auto">
<table className="w-full text-left text-[13px]">
<thead>
<tr className="border-b border-[color:var(--line)]">
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Name</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Description</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Backend</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Updated</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretName")}</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretDescription")}</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretBackendCol")}</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretUpdated")}</th>
<th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]" />
</tr>
</thead>
@@ -1667,6 +1689,7 @@ export function SettingsPane({
export function SecretCreator({ companyId, onSaved }: { companyId: string; onSaved: () => Promise<void> }) {
const t = useT();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [value, setValue] = useState("");
@@ -1686,7 +1709,7 @@ export function SecretCreator({ companyId, onSaved }: { companyId: string; onSav
return (
<div className="grid gap-4">
<div>
<Input label="Secret name" value={name} onChange={(v) => { setName(v); if (errors.name) setErrors((e) => ({ ...e, name: undefined })); }} placeholder="e.g. ANTHROPIC_API_KEY" />
<Input label={t("exec.secretNameLabel")} value={name} onChange={(v) => { setName(v); if (errors.name) setErrors((e) => ({ ...e, name: undefined })); }} placeholder="e.g. ANTHROPIC_API_KEY" />
{errors.name ? <div className="mt-1 text-[12px] text-[color:var(--danger)]" role="alert">{errors.name}</div> : null}
</div>
<Input label="Description" value={description} onChange={setDescription} placeholder="What this secret is used for" />
@@ -1696,7 +1719,7 @@ export function SecretCreator({ companyId, onSaved }: { companyId: string; onSav
</div>
{submitError ? <InlineNotice message={submitError} /> : null}
<ActionButton
label="Store secret"
label={t("exec.storeSecret")}
tone="accent"
onClick={() => void (async () => {
if (!validate()) return;
+32 -46
View File
@@ -6,6 +6,7 @@ import {
X,
} from "@phosphor-icons/react";
import type { AgentRecord, ApprovalRecord } from "@shared/types";
import { useT } from "../i18n";
import { getAgentInitials } from "../lib/formatters";
import { unwrap } from "../lib/desktop";
import { ActionButton, EmptyState, Panel, StatusPill } from "./ui";
@@ -39,17 +40,6 @@ function getHiringStage(agent: AgentRecord, approval: ApprovalRecord | null): Hi
return "active";
}
function stageLabel(stage: HiringStage): string {
switch (stage) {
case "requisition": return "Requisition";
case "pending_approval": return "Pending Approval";
case "approved": return "Approved";
case "onboarding": return "Onboarding";
case "active": return "Active";
case "rejected": return "Rejected";
}
}
function stageColor(stage: HiringStage): string {
switch (stage) {
case "requisition": return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]";
@@ -64,8 +54,18 @@ function stageColor(stage: HiringStage): string {
const STAGE_ORDER: HiringStage[] = ["requisition", "pending_approval", "approved", "onboarding", "active", "rejected"];
export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent }: HiringPipelineProps) {
const t = useT();
const [filterStage, setFilterStage] = useState<HiringStage | "all">("all");
const stageLabelMap: Record<HiringStage, string> = {
requisition: t("hiring.requisition"),
pending_approval: t("hiring.pendingApproval"),
approved: t("hiring.approved"),
onboarding: t("hiring.onboarding"),
active: t("hiring.active"),
rejected: t("status.rejected"),
};
const hireApprovals = approvals.filter(a => a.type === "hire_agent");
const candidates: HiringCandidate[] = agents.map(agent => {
@@ -108,7 +108,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
return (
<div className="space-y-4">
<Panel title="Hiring Pipeline" eyebrow={`${activeHires} active hiring processes`}>
<Panel title={t("hiring.title")} eyebrow={`${activeHires} ${t("hiring.activeProcesses")}`}>
<div className="flex items-center gap-1 px-4 py-3 overflow-x-auto">
{STAGE_ORDER.filter(s => s !== "rejected").map((stage, idx) => (
<div key={stage} className="flex items-center gap-1">
@@ -118,7 +118,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
filterStage === stage ? "ring-1 ring-[color:var(--accent)]/40 bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]"
} ${stageColor(stage)}`}
>
{stageLabel(stage)}
{stageLabelMap[stage]}
<span className="ml-1.5 inline-flex items-center justify-center h-5 w-5 rounded-full bg-[color:var(--line)] text-[10px]">
{stageCounts[stage]}
</span>
@@ -135,7 +135,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
filterStage === "rejected" ? "ring-1 ring-[color:var(--danger)]/40 bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]"
} ${stageColor("rejected")}`}
>
Rejected
{stageLabelMap.rejected}
<span className="ml-1.5 inline-flex items-center justify-center h-5 w-5 rounded-full bg-[color:var(--line)] text-[10px]">
{stageCounts.rejected}
</span>
@@ -145,7 +145,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
</Panel>
{pendingCount > 0 && (
<Panel title="Awaiting Board Decision" eyebrow={`${pendingCount} hire requests need your review`}>
<Panel title={t("hiring.pendingApproval")}>
<div className="divide-y divide-[color:var(--line)]">
{allCandidates
.filter(c => c.stage === "requisition" || c.stage === "pending_approval")
@@ -159,25 +159,14 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-[color:var(--text)]">{ag?.name ?? "Proposed Agent"}</span>
<span className="font-medium text-sm text-[color:var(--text)]">{ag?.name ?? t("hiring.pendingHires")}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded ${stageColor(candidate.stage)}`}>
{stageLabel(candidate.stage)}
{stageLabelMap[candidate.stage]}
</span>
</div>
<div className="text-xs text-[color:var(--muted)] mt-0.5">
{ag ? `${ag.role} — ${ag.title}` : a?.payloadSummary}
</div>
{ag?.department && (
<div className="text-xs text-[color:var(--muted)] mt-0.5">
Dept: {ag.department} | Budget: ${ag.budgetMonthlyUsd}/mo | Connector: {ag.connectorId}
</div>
)}
{a?.payloadSummary && (
<div className="text-xs text-[color:var(--muted)] mt-1">{a.payloadSummary}</div>
)}
{a?.impactSummary && (
<div className="text-xs text-[color:var(--muted)]">{a.impactSummary}</div>
)}
{ag?.capabilities && (
<div className="text-xs text-[color:var(--muted)] mt-1 line-clamp-2">{ag.capabilities}</div>
)}
@@ -186,7 +175,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
{a && a.state === "pending" && (
<>
<ActionButton
label="Approve"
label={t("hiring.approved")}
icon={Check}
tone="accent"
onClick={async () => {
@@ -200,7 +189,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
}}
/>
<ActionButton
label="Reject"
label={t("status.rejected")}
icon={X}
tone="danger"
onClick={async () => {
@@ -224,18 +213,15 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
)}
<Panel
title={filterStage === "all" ? "All Agents" : `${stageLabel(filterStage)} (${filteredCandidates.length})`}
title={filterStage === "all" ? t("hiring.allAgents") : `${stageLabelMap[filterStage]} (${filteredCandidates.length})`}
action={
filterStage !== "all" ? (
<ActionButton label="Show all" icon={X} onClick={() => setFilterStage("all")} />
<ActionButton label={t("hiring.allAgents")} icon={X} onClick={() => setFilterStage("all")} />
) : undefined
}
>
{filteredCandidates.length === 0 ? (
<EmptyState
title="No hiring activity"
detail="When agents propose new hires via the API, they will appear here for your review."
/>
<EmptyState title={t("panel.noData")} />
) : (
<div className="divide-y divide-[color:var(--line)]">
{filteredCandidates.map((candidate) => {
@@ -258,7 +244,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-[color:var(--text)] truncate">{ag.name}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded ${stageColor(candidate.stage)}`}>
{stageLabel(candidate.stage)}
{stageLabelMap[candidate.stage]}
</span>
<StatusPill status={ag.status} />
</div>
@@ -268,10 +254,10 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
</div>
<div className="flex-shrink-0 text-right">
<div className="text-xs text-[color:var(--muted)]">
{manager ? `Reports to ${manager.name}` : "Top level"}
{manager ? `${t("hiring.reportsTo")} ${manager.name}` : t("hiring.topLevel")}
</div>
{requestedBy && (
<div className="text-[10px] text-[color:var(--muted)]">Proposed by {requestedBy.name}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("hiring.proposedBy")} {requestedBy.name}</div>
)}
<div className="text-xs text-[color:var(--muted)]">${ag.budgetMonthlyUsd}/mo</div>
</div>
@@ -284,28 +270,28 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<div className="grid grid-cols-4 gap-3">
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">Total Agents</div>
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.totalAgents")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">{agents.filter(a => a.status !== "terminated").length}</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">Pending Hires</div>
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.pendingHires")}</div>
<div className="text-2xl font-bold text-[color:var(--warn)] mt-1">{agents.filter(a => a.status === "pending_approval").length}</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">Departments</div>
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.departments")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">
{new Set(agents.filter(a => a.department && a.status !== "terminated").map(a => a.department)).size}
</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">Monthly Payroll</div>
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.monthlyPayroll")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">
${agents.filter(a => a.status !== "terminated" && a.status !== "pending_approval").reduce((s, a) => s + a.budgetMonthlyUsd, 0).toLocaleString()}
</div>
</div>
</div>
<Panel title="Department Headcount">
<Panel title={t("hiring.departmentHeadcount")}>
<div className="grid grid-cols-3 gap-2 p-4">
{(["executive", "engineering", "product", "design", "marketing", "sales", "hr", "finance", "legal", "customer_support", "operations", "research"] as const).map(dept => {
const deptAgents = agents.filter(a => a.department === dept && a.status !== "terminated");
@@ -317,11 +303,11 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<div className="text-xs font-medium text-[color:var(--text)] capitalize">{dept.replace("_", " ")}</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-sm font-bold text-[color:var(--text)]">{active}</span>
<span className="text-[10px] text-[color:var(--muted)]">active</span>
<span className="text-[10px] text-[color:var(--muted)]">{t("hiring.active")}</span>
{pending > 0 && (
<>
<span className="text-sm font-bold text-[color:var(--warn)]">+{pending}</span>
<span className="text-[10px] text-[color:var(--warn)]">pending</span>
<span className="text-[10px] text-[color:var(--warn)]">{t("hiring.pendingApproval")}</span>
</>
)}
</div>
+23 -19
View File
@@ -18,6 +18,7 @@ import {
} from "@phosphor-icons/react";
import type { InboxItem, InboxItemType, SectionId, StandupReport, CompanyMetrics } from "@shared/types";
import { formatMoney, timeAgo, getAgentInitials, formatDuration } from "../lib/formatters";
import { useT } from "../i18n";
const typeConfig: Record<InboxItemType, { icon: React.ElementType; color: string; bg: string; label: string }> = {
pending_approval: { icon: ShieldCheck, color: "var(--warn)", bg: "var(--warn-soft)", label: "Approval" },
@@ -41,6 +42,7 @@ interface InboxProps {
}
export function Inbox({ items, onNavigate }: InboxProps) {
const t = useT();
const [filter, setFilter] = useState<InboxItemType | "all">("all");
const filteredItems = useMemo(() => {
@@ -88,7 +90,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
<Bell size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">Inbox</h2>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("inbox.title")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention
</p>
@@ -123,8 +125,8 @@ export function Inbox({ items, onNavigate }: InboxProps) {
{filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Tray size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">All clear!</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">No items need your attention right now</div>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("inbox.allClear")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("inbox.noItemsNow")}</div>
</div>
) : (
<div className="space-y-1.5">
@@ -189,12 +191,13 @@ interface StandupProps {
}
export function StandupView({ report, onNavigate }: StandupProps) {
const t = useT();
if (!report) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Clock size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No standup data</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Company needs active agents to generate standup reports</div>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("standup.noData")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("standup.noDataDesc")}</div>
</div>
);
}
@@ -207,7 +210,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
<Heartbeat size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">Daily Standup</h2>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("standup.dailyStandup")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
Generated {new Date(report.generatedAt).toLocaleString()}
</p>
@@ -217,31 +220,31 @@ export function StandupView({ report, onNavigate }: StandupProps) {
<div className="mt-3 flex gap-4">
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Productive</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.productive")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--danger)]">{report.blockers.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Blockers</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.blockers")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--warn)]">{report.pendingApprovals}</div>
<div className="text-[10px] text-[color:var(--muted)]">Approvals</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.pendingApprovals")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className={`text-[18px] font-bold ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
{report.budgetUtilization}%
</div>
<div className="text-[10px] text-[color:var(--muted)]">Budget Used</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.budgetUsed")}</div>
</div>
</div>
</div>
{report.companyHighlights.length > 0 ? (
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Highlights</div>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.highlights")}</div>
<div className="space-y-1.5">
{report.companyHighlights.map((h, i) => (
<div key={i} className="flex items-center gap-2 rounded-[6px] bg-[color:var(--success-soft)] px-3 py-2 text-[12px] text-[color:var(--success)]">
@@ -268,7 +271,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
) : null}
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agent Status</div>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.agentStatus")}</div>
<div className="space-y-2">
{report.agentReports.map((ar) => (
<button
@@ -323,12 +326,13 @@ interface PerformanceProps {
}
export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
const t = useT();
if (!metrics) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Target size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No metrics yet</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Metrics will appear after agents start working</div>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("perf.noMetrics")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("perf.noMetricsDesc")}</div>
</div>
);
}
@@ -337,24 +341,24 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
<div className="space-y-6">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
<CompactMetric
label="Success Rate"
label={t("perf.successRate")}
value={`${metrics.overallSuccessRate}%`}
tone={metrics.overallSuccessRate >= 80 ? "success" : metrics.overallSuccessRate >= 50 ? "warn" : "danger"}
/>
<CompactMetric
label="Tasks Done"
label={t("perf.tasksDone")}
value={metrics.completedTasks}
tone="success"
subtitle={`of ${metrics.totalTasks} total`}
/>
<CompactMetric
label="Throughput"
label={t("perf.throughput")}
value={`${metrics.throughputTasksPerDay.toFixed(1)}`}
tone="accent"
subtitle="tasks/day"
subtitle={t("perf.tasksPerDay")}
/>
<CompactMetric
label="Avg Cost/Task"
label={t("perf.avgCostTask")}
value={formatMoney(metrics.avgCostPerTask)}
tone="default"
/>
+9 -4
View File
@@ -30,6 +30,7 @@ import {
StatusPill,
TextArea,
} from "./ui";
import { useT } from "../i18n";
function BudgetBar({ spent, budget }: { spent: number; budget: number }) {
const pct = budget > 0 ? Math.min((spent / budget) * 100, 100) : 0;
@@ -80,8 +81,9 @@ export function AgentRoster({
selectedAgentId: string | null;
onSelect: (agentId: string) => void;
}) {
const t = useT();
if (agents.length === 0) {
return <EmptyState title="No agents yet" />;
return <EmptyState title={t("org.noAgentsYet")} />;
}
return (
<ListFrame>
@@ -617,8 +619,9 @@ export function GoalList({
selectedGoalId: string | null;
onSelect: (goalId: string) => void;
}) {
const t = useT();
if (goals.length === 0) {
return <EmptyState title="No goals yet" />;
return <EmptyState title={t("org.noGoalsYet")} />;
}
return (
<ListFrame>
@@ -847,8 +850,9 @@ export function ProjectList({
selectedProjectId: string | null;
onSelect: (projectId: string) => void;
}) {
const t = useT();
if (projects.length === 0) {
return <EmptyState title="No projects yet" />;
return <EmptyState title={t("org.noProjectsYet")} />;
}
return (
<ListFrame>
@@ -1183,6 +1187,7 @@ export function OrgChart({
agents: AgentRecord[];
onSelectAgent?: (agentId: string) => void;
}) {
const t = useT();
const containerRef = useRef<HTMLDivElement>(null);
const [pan, setPan] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
@@ -1257,7 +1262,7 @@ export function OrgChart({
}, [zoom, pan]);
if (agents.length === 0) {
return <EmptyState title="No agents to display" />;
return <EmptyState title={t("org.noAgentsToDisplay")} />;
}
return (
+22 -20
View File
@@ -10,6 +10,7 @@ import {
import type { AgentRecord, RunRecord, TaskRecord, WorkspaceRecord } from "@shared/types";
import { formatMoney, formatTime, timeAgo } from "../lib/formatters";
import { ActionButton, EmptyState, Input, ListFrame, ListRow, Panel, Select, StatusPill } from "./ui";
import { useT } from "../i18n";
const LOG_CHUNK_BYTES = 65_536;
@@ -64,6 +65,7 @@ export function RunCenter({
onOpenLogPath,
onOpenConsole,
}: RunCenterProps) {
const t = useT();
const [statusFilter, setStatusFilter] = useState<"all" | RunRecord["status"]>("all");
const [search, setSearch] = useState("");
const [logState, setLogState] = useState<RunLogState>(initialLogState);
@@ -265,8 +267,8 @@ export function RunCenter({
if (runs.length === 0) {
return (
<EmptyState
title="No runs yet"
detail="Start a task run from the task queue or onboarding handoff. This surface becomes the operator console for queue state, failures, and live log tails."
title={t("runs.noRunsYet")}
detail={t("runs.noRunsDetail")}
/>
);
}
@@ -275,22 +277,22 @@ export function RunCenter({
<div className="space-y-10">
<div className="grid gap-3 md:grid-cols-3">
<RunSummaryCard
label="Active"
label={t("runs.activeLabel")}
value={groupedCounts.active}
detail="Queued or running right now"
detail={t("runs.queuedOrRunning")}
icon={PlayCircle}
/>
<RunSummaryCard
label="Needs attention"
label={t("runs.needsAttention")}
value={groupedCounts.attention}
detail="Failed, timed out, or interrupted"
detail={t("runs.failedTimedInterrupted")}
icon={WarningCircle}
tone="danger"
/>
<RunSummaryCard
label="Completed"
label={t("runs.completedLabel")}
value={groupedCounts.completed}
detail="Finished without current intervention"
detail={t("runs.finishedWithout")}
icon={ListChecks}
/>
</div>
@@ -298,7 +300,7 @@ export function RunCenter({
<div className="grid gap-8 xl:grid-cols-[minmax(0,420px)_minmax(0,1fr)]">
<div className="space-y-5">
<Panel
title="Run queue"
title={t("runs.runQueue")}
action={(
<Select
compact
@@ -322,7 +324,7 @@ export function RunCenter({
ariaLabel="Search runs"
value={search}
onChange={setSearch}
placeholder="Search by task, agent, connector, workspace, or error"
placeholder={t("runs.searchRuns")}
/>
</div>
<ListFrame>
@@ -359,8 +361,8 @@ export function RunCenter({
</ListFrame>
{filteredRuns.length === 0 ? (
<EmptyState
title="No runs match this filter"
detail="Widen the status filter or clear the search query to inspect the full run history."
title={t("runs.noRunsMatch")}
detail={t("runs.noRunsMatchDetail")}
/>
) : null}
</Panel>
@@ -370,17 +372,17 @@ export function RunCenter({
{selectedRun ? (
<>
<Panel
title="Run detail"
title={t("runs.runDetail")}
action={(
<div className="flex items-center gap-2">
<ActionButton
label="Open console"
label={t("runs.openConsole")}
icon={Lightning}
onClick={() => onOpenConsole(selectedRun.id)}
/>
{isActiveStatus(selectedRun.status) ? (
<ActionButton
label="Cancel run"
label={t("runs.cancelRun")}
tone="danger"
icon={WarningCircle}
onClick={() => void onCancelRun(selectedRun.id)}
@@ -409,7 +411,7 @@ export function RunCenter({
{selectedRun.logPath ? (
<div className="mt-5">
<ActionButton
label="Open log file"
label={t("runs.openLogFile")}
icon={FolderOpen}
onClick={() => void onOpenLogPath(selectedRun.logPath!)}
/>
@@ -418,18 +420,18 @@ export function RunCenter({
</Panel>
<Panel
title="Log tail"
eyebrow="Incremental reader"
title={t("runs.logTail")}
eyebrow={t("runs.incrementalReader")}
action={(
<div className="flex items-center gap-2">
<ActionButton
label="Reload tail"
label={t("runs.reloadTail")}
icon={ArrowClockwise}
onClick={() => setLogReloadKey((current) => current + 1)}
/>
{logState.offset > 0 ? (
<ActionButton
label="Load older"
label={t("runs.loadOlder")}
icon={ListChecks}
onClick={() => void loadOlderLogs()}
disabled={logState.loading}
+5 -3
View File
@@ -31,6 +31,7 @@ import {
import type { BrowserActionRecord, SocialAccountRecord, SocialPlatform } from "@shared/types";
import { timeAgo } from "../lib/formatters";
import { ActionButton, Input } from "./ui";
import { useT } from "../i18n";
const platformConfig: Record<SocialPlatform, { label: string; color: string; bgColor: string; icon: ElementType }> = {
twitter: { label: "Twitter / X", color: "#1D9BF0", bgColor: "#1D9BF014", icon: XLogo },
@@ -418,6 +419,7 @@ export function SocialAccountsPanel({
onCancelAction,
onNavigate,
}: SocialPanelProps) {
const t = useT();
const [showAddDialog, setShowAddDialog] = useState(false);
const [activeTab, setActiveTab] = useState<"accounts" | "history">("accounts");
@@ -434,7 +436,7 @@ export function SocialAccountsPanel({
<Megaphone size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">Social Media</h2>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("social.socialMedia")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{accounts.length} account{accounts.length !== 1 ? "s" : ""} connected
</p>
@@ -491,9 +493,9 @@ export function SocialAccountsPanel({
accounts.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Megaphone size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No social accounts</div>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("social.noAccounts")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
Add your first social media account to enable marketing automation
{t("social.noAccountsDesc")}
</div>
<div className="mt-4">
<ActionButton
+797
View File
@@ -0,0 +1,797 @@
export const en: Record<string, string> = {
// ── Navigation items ──
"nav.overview": "Dashboard",
"nav.inbox": "Inbox",
"nav.standup": "Standup",
"nav.performance": "Performance",
"nav.runs": "Runs",
"nav.agents": "Agents",
"nav.orgchart": "Org Chart",
"nav.hiring": "Hiring",
"nav.communication": "Messages",
"nav.goals": "Goals",
"nav.projects": "Projects",
"nav.tasks": "Tasks",
"nav.sprints": "Sprints",
"nav.automation": "Automation",
"nav.approvals": "Approvals",
"nav.documents": "Documents",
"nav.knowledge": "Knowledge",
"nav.meetings": "Meetings",
"nav.costs": "Costs",
"nav.activity": "Activity",
"nav.social": "Social Media",
"nav.connectors": "Connectors",
"nav.workspaces": "Workspaces",
"nav.settings": "Settings",
// ── Navigation group labels ──
"nav.group.overview": "Overview",
"nav.group.organization": "Organization",
"nav.group.automation": "Automation",
"nav.group.knowledge": "Knowledge",
"nav.group.operations": "Operations",
"nav.group.infrastructure": "Infrastructure",
// ── App-level strings ──
"app.loading": "Loading local profile...",
"app.selectCompany": "Select company",
"app.newCompany": "New company",
"app.commandPalette": "Command palette",
"app.refresh": "Refresh",
"app.console": "Console",
"app.hideConsole": "Hide console",
"app.showConsole": "Show console",
"app.collapse": "Collapse",
"app.expand": "Expand",
"app.noResults": "No results for",
"app.clear": "Clear",
"app.search": "Search sections, agents, tasks, goals, projects...",
// ── Onboarding flow ──
"onboarding.title": "Start a real local-first\nagent company.",
"onboarding.subtitle": "AgentCompany Desktop",
"onboarding.description": "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.",
"onboarding.step1Title": "Create the operating boundary",
"onboarding.step2Title": "Define the first real goal",
"onboarding.step3Title": "Launch summary",
"onboarding.stepLabel1": "Company",
"onboarding.stepLabel2": "Mission",
"onboarding.stepLabel3": "Launch",
"onboarding.companyName": "Company name",
"onboarding.companyNamePlaceholder": "e.g. Northline Systems",
"onboarding.companyDescription": "Company description",
"onboarding.companyDescriptionPlaceholder": "Optional context for the company operating model.",
"onboarding.primaryConnector": "Primary connector",
"onboarding.connectorReadiness": "Connector readiness",
"onboarding.connectorInfo": "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.",
"onboarding.workspaceFolder": "Workspace folder",
"onboarding.workspaceFolderOptional": "(optional)",
"onboarding.workspaceFolderPlaceholder": "/Users/you/project",
"onboarding.workspaceHelp": "If set, the first execution task will be bound to this path and the path will be registered as the primary workspace.",
"onboarding.autoApprove": "Auto-approve hire and strategy approvals",
"onboarding.autoApproveLabel": "Auto-approve hire and strategy approvals",
"onboarding.autoApproveHelp": "Useful for single-operator flows. Leave it off if you want the board to gate organization changes explicitly.",
"onboarding.continue": "Continue",
"onboarding.back": "Back",
"onboarding.goal": "Goal",
"onboarding.goalPlaceholder": "e.g. Ship the first customer-ready desktop demo",
"onboarding.acceptanceCriteria": "Acceptance criteria",
"onboarding.acceptanceCriteriaPlaceholder": "Describe what successful execution looks like, what cannot be skipped, and any delivery constraints.",
"onboarding.executionHandoff": "Execution handoff",
"onboarding.executionHandoffLine1": "The selected connector becomes the CEO agent runtime for the first launch.",
"onboarding.executionHandoffLine2": "The app will create the company, the primary workspace, the CEO agent, the kickoff project, and the first task.",
"onboarding.executionHandoffLine3": "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.",
"onboarding.launching": "Launching...",
"onboarding.launchCompany": "Launch Company",
"onboarding.isLive": "is live.",
"onboarding.launchDescription": "The company shell is created. The first project and task are registered, and the kickoff execution is queued.",
"onboarding.launchDescriptionDeferred": "The company shell is created. The first project and task are registered, and the kickoff execution is prepared for deferred execution once the runtime is ready.",
"onboarding.connector": "Connector",
"onboarding.leadAgent": "Lead agent",
"onboarding.ceoAgentCreated": "CEO agent created",
"onboarding.leadAgentUnavailable": "Lead agent unavailable",
"onboarding.primaryWorkspace": "Primary workspace",
"onboarding.registered": "Registered",
"onboarding.notBound": "Not bound",
"onboarding.kickoffProject": "Kickoff project",
"onboarding.created": "Created",
"onboarding.unavailable": "Unavailable",
"onboarding.initialRun": "Initial run",
"onboarding.queuedInOrchestration": "Queued in orchestration",
"onboarding.deferredUntilReady": "Deferred until runtime is ready",
"onboarding.opening": "Opening...",
"onboarding.openControlCenter": "Open Control Center",
"onboarding.cancel": "Cancel",
"onboarding.enterCompanyName": "Enter a company name.",
"onboarding.connectorRequired": "At least one execution-ready connector is required before you can continue.",
"onboarding.describeFirstGoal": "Describe the first real company goal.",
"onboarding.unableToBootstrap": "Unable to bootstrap the company.",
"onboarding.unableToOpenControlCenter": "Unable to open the control center.",
"onboarding.connectorNotInstalled": "Install, authenticate, and fully validate at least one supported CLI runtime before creating the company.",
"onboarding.connectorWarning": "Install, authenticate, and fully validate at least one supported CLI runtime before creating the company.",
"onboarding.languageSelection": "Language",
"onboarding.selectLanguage": "Select language",
// ── Settings ──
"settings.profile": "Profile",
"settings.theme": "Theme",
"settings.system": "System",
"settings.light": "Light",
"settings.dark": "Dark",
"settings.updates": "Updates",
"settings.enabled": "Enabled",
"settings.disabled": "Disabled",
"settings.logRetention": "Log retention",
"settings.days": "days",
"settings.backup": "Backup profile",
"settings.restore": "Restore profile",
"settings.secretBindings": "Secret bindings",
"settings.language": "Language",
// ── Status labels ──
"status.idle": "Idle",
"status.active": "Active",
"status.running": "Running",
"status.paused": "Paused",
"status.error": "Error",
"status.pending_approval": "Pending approval",
"status.terminated": "Terminated",
"status.not_installed": "Not installed",
"status.detected": "Detected",
"status.auth_required": "Auth required",
"status.ready": "Ready",
"status.degraded": "Degraded",
"status.queued": "Queued",
"status.succeeded": "Succeeded",
"status.failed": "Failed",
"status.cancelled": "Cancelled",
"status.timed_out": "Timed out",
"status.interrupted": "Interrupted",
"status.backlog": "Backlog",
"status.todo": "To do",
"status.in_progress": "In progress",
"status.in_review": "In review",
"status.done": "Done",
"status.blocked": "Blocked",
"status.planned": "Planned",
"status.achieved": "Achieved",
"status.completed": "Completed",
"status.active_sprint": "Active sprint",
"status.review": "Review",
"status.scheduling": "Scheduling",
"status.draft": "Draft",
"status.approved": "Approved",
"status.rejected": "Rejected",
"status.revision_requested": "Revision requested",
"status.login_required": "Login required",
"status.suspended": "Suspended",
"status.pending": "Pending",
// ── Common labels ──
"common.save": "Save",
"common.delete": "Delete",
"common.cancel": "Cancel",
"common.edit": "Edit",
"common.new": "New",
"common.close": "Close",
"common.yes": "Yes",
"common.no": "No",
"common.confirm": "Confirm",
"common.status": "Status",
"common.started": "Started",
"common.finished": "Finished",
"common.cost": "Cost",
"common.openLog": "Open log",
"common.noRunSelected": "No run selected",
"common.waitingForLog": "Waiting for log output...",
"common.sections": "Sections",
"common.agents": "Agents",
"common.tasks": "Tasks",
"common.runs": "Runs",
"common.goals": "Goals",
"common.projects": "Projects",
// ── Console panel ──
"console.title": "Console",
"console.noRuns": "No runs",
// ── Entity labels (command palette sections) ──
"entity.sections": "Sections",
"entity.agents": "Agents",
"entity.tasks": "Tasks",
"entity.runs": "Runs",
"entity.goals": "Goals",
"entity.projects": "Projects",
// ── Hiring page ──
"hiring.title": "Hiring Pipeline",
"hiring.activeProcesses": "active hiring processes",
"hiring.requisition": "Requisition",
"hiring.pendingApproval": "Pending Approval",
"hiring.approved": "Approved",
"hiring.onboarding": "Onboarding",
"hiring.active": "Active",
"hiring.allAgents": "All Agents",
"hiring.totalAgents": "Total Agents",
"hiring.pendingHires": "Pending Hires",
"hiring.departments": "Departments",
"hiring.monthlyPayroll": "Monthly Payroll",
"hiring.departmentHeadcount": "Department Headcount",
"hiring.topLevel": "Top level",
"hiring.reportsTo": "Reports to",
"hiring.proposedBy": "Proposed by",
// ── Organization page ──
"org.agentRoster": "Agent roster",
"org.newHireRequest": "New hire request",
"org.editAgent": "Edit",
"org.requestNewHire": "Request new hire",
"org.goalStack": "Goal stack",
"org.newGoal": "New goal",
"org.newProject": "New project",
// ── Tasks page ──
"tasks.taskQueue": "Task queue",
"tasks.newTask": "New task",
// ── Approvals page ──
"approvals.reviewsInProgress": "Agent reviews in progress",
"approvals.pendingApprovals": "Pending approvals",
"approvals.approvalDetail": "Approval detail",
// ── Costs page ──
"costs.recentEntries": "Recent cost entries",
"costs.budgetPosture": "Budget posture",
// ── Activity page ──
"activity.timeline": "Activity timeline",
// ── Workspaces page ──
"workspaces.registry": "Workspace registry",
"workspaces.controlCenter": "Workspace control center",
"workspaces.addWorkspace": "Add workspace",
// ── Settings page ──
"settings.profileAndRelease": "Profile and release",
"settings.secretBindingsTitle": "Secret bindings",
// ── Connectors page ──
"connectors.registry": "Connector registry",
// ── Dashboard ──
"dashboard.title": "Dashboard",
// ── Runs page ──
"runs.title": "Run center",
// ── Documents page ──
"documents.title": "Documents",
// ── Knowledge page ──
"knowledge.title": "Knowledge base",
// ── Meetings page ──
"meetings.title": "Meetings",
// ── Sprints page ──
"sprints.title": "Sprints",
// ── Social page ──
"social.title": "Social accounts",
// ── Automation page ──
"automation.title": "Automation",
// ── Communication page ──
"communication.title": "Communication hub",
// ── Common panel titles ──
"panel.choose": "Choose",
"panel.noData": "No data",
"panel.empty": "Empty",
"panel.loading": "Loading...",
// ── Execution / Task Inspector ──
"exec.taskQueueEmpty": "Task queue is empty",
"exec.deleteTask": "Delete task",
"exec.deleteTaskConfirm": "Permanently delete \"{0}\"? This cannot be undone.",
"exec.budgetUsed": "has used {0}% of their monthly budget",
"exec.taskChangesSaved": "Task changes saved.",
"exec.taskMovedTo": "Task moved to {0}.",
"exec.approvalCreatedBlocked": "Approval created and routed to leadership.",
"exec.approvalCreated": "Approval created.",
"exec.title": "Title",
"exec.description": "Description",
"exec.priority": "Priority",
"exec.assignee": "Assignee",
"exec.unassigned": "Unassigned",
"exec.workspace": "Workspace",
"exec.noWorkspace": "No workspace",
"exec.project": "Project",
"exec.noProject": "No project",
"exec.goal": "Goal",
"exec.noGoal": "No goal",
"exec.save": "Save",
"exec.cancel": "Cancel",
"exec.assigneeLabel": "Assignee:",
"exec.projectLabel": "Project:",
"exec.goalLabel": "Goal:",
"exec.parentLabel": "Parent:",
"exec.workspaceLabel": "Workspace:",
"exec.costLabel": "Cost:",
"exec.beingReviewedBy": "Being reviewed by {0}",
"exec.submittedForReview": "{0} submitted this task for review. {1} will approve, request revisions, or escalate.",
"exec.governanceRequired": "Governance action required",
"exec.waitingOnReview": "Task is waiting on review",
"exec.blockedGovernance": "Create an approval so leadership can resolve the blocker, provide credentials, or authorize the next privileged action.",
"exec.reviewGovernance": "Create an approval entry if this task needs an explicit operator decision before it can move forward.",
"exec.createUnblockApproval": "Create unblock approval",
"exec.createReviewApproval": "Create review approval",
"exec.cancelRun": "Cancel run",
"exec.run": "Run",
"exec.done": "Done",
"exec.unblock": "Unblock",
"exec.discussion": "Discussion",
"exec.edit": "Edit",
"exec.delete": "Delete",
"exec.subtasks": "Subtasks",
"exec.doneCount": "{0}/{1} done",
"exec.runHistory": "Run history",
"exec.moreRuns": "+{0} more runs",
"exec.runningEllipsis": "Running...",
"exec.completedRun": "Completed run",
"exec.communication": "Communication",
"exec.noMessagesYet": "No messages yet. Agents will post updates here during runs.",
"exec.sendMessage": "Send a message to the team...",
"exec.send": "Send",
"exec.createTask": "Create task",
"exec.taskTitle": "Task title",
"exec.noReviewsInProgress": "No agent reviews in progress",
"exec.awaitingReview": "awaiting review",
"exec.reviewedBy": "reviewed by",
"exec.noApprovals": "No approvals",
"exec.deleteApproval": "Delete approval",
"exec.deleteApprovalConfirm": "Permanently delete this approval? This cannot be undone.",
"exec.requestedBy": "Requested by:",
"exec.task": "Task:",
"exec.created": "Created:",
"exec.updated": "Updated:",
"exec.proposedHire": "Proposed Hire",
"exec.name": "Name:",
"exec.role": "Role:",
"exec.titleLabel": "Title:",
"exec.capabilities": "Capabilities:",
"exec.connector": "Connector:",
"exec.budget": "Budget:",
"exec.reportsTo": "Reports to:",
"exec.orgStrategy": "Organization Strategy",
"exec.decisionNote": "Decision note",
"exec.decisionNotePlaceholder": "Optional note to accompany your decision...",
"exec.approve": "Approve",
"exec.requestRevision": "Request revision",
"exec.reject": "Reject",
"exec.lastNote": "Last note:",
"exec.noCostData": "No cost data yet",
"exec.allConnectors": "All connectors",
"exec.runCost": "Run cost",
"exec.unknownAgent": "Unknown agent",
"exec.unattributed": "unattributed",
"exec.totalEntries": "Total ({0} entries)",
"exec.knownSpend": "Known spend",
"exec.unattributedEntries": "Unattributed entries",
"exec.noActivityYet": "No activity yet",
"exec.searchActivity": "Search activity...",
"exec.allTypes": "All types",
"exec.noMatchingActivity": "No matching activity",
"exec.loadMore": "Load more ({0} remaining)",
"exec.noWorkspacesYet": "No workspaces yet",
"exec.deleteWorkspace": "Delete workspace",
"exec.deleteWorkspaceConfirm": "Delete workspace \"{0}\"? This only removes it from the company registry; files on disk are not affected.",
"exec.inspect": "Inspect",
"exec.reveal": "Reveal",
"exec.noRemoteUrl": "No remote URL",
"exec.selectWorkspace": "Select a workspace",
"exec.selectWorkspaceDetail": "Choose a workspace to inspect the agents, tasks, runs, and documents bound to it.",
"exec.agents": "Agents",
"exec.tasks": "Tasks",
"exec.runs": "Runs",
"exec.docs": "Docs",
"exec.assignedAgents": "Assigned Agents",
"exec.noAgentsBound": "No agents are currently bound to this workspace.",
"exec.openTasks": "Open Tasks",
"exec.noTasksTarget": "No tasks target this workspace yet.",
"exec.recentRuns": "Recent Runs",
"exec.noRunsExecuted": "No runs have executed in this workspace.",
"exec.openFolder": "Open folder",
"exec.noLinkedProject": "No linked project",
"exec.workspaceName": "Workspace name",
"exec.localPath": "Local path",
"exec.choose": "Choose",
"exec.repoUrl": "Repository URL",
"exec.branchRef": "Branch / ref",
"exec.noProjectBinding": "No project binding",
"exec.addWorkspace": "Add workspace",
"exec.profile": "Profile",
"exec.profilePath": "Profile path",
"exec.preferences": "Preferences",
"exec.enableUpdater": "Enable updater checks",
"exec.companyAutonomy": "Company autonomy",
"exec.autoApproveLabel": "Auto-approve hire and strategy approvals",
"exec.autoApproveDesc": "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.",
"exec.actions": "Actions",
"exec.saveSettings": "Save settings",
"exec.checkForUpdates": "Check for updates",
"exec.backupProfile": "Backup profile",
"exec.restoreProfile": "Restore profile",
"exec.secretBackend": "Secret backend",
"exec.secrets": "Secrets",
"exec.noSecretsStored": "No secrets stored",
"exec.secretName": "Name",
"exec.secretDescription": "Description",
"exec.secretBackendCol": "Backend",
"exec.secretUpdated": "Updated",
"exec.noDescription": "No description",
"exec.deleteSecret": "Delete secret",
"exec.deleteSecretConfirm": "Permanently delete secret \"{0}\"? Any connectors referencing it will lose access.",
"exec.secretDeleted": "Secret deleted.",
"exec.settingsSaved": "Settings saved.",
"exec.updateCheckStarted": "Update check started.",
"exec.updateCheckSkipped": "Update check skipped because no release update feed is configured.",
"exec.profileBackupCreated": "Profile backup created at {0}.",
"exec.profileRestored": "Profile restored.",
"exec.storeSecret": "Store secret",
"exec.secretNameLabel": "Secret name",
"exec.secretNameRequired": "Secret name is required.",
"exec.secretNameFormat": "Name must be alphanumeric with underscores, starting with a letter or underscore.",
"exec.secretValueLabel": "Secret value",
"exec.secretValueEmpty": "Secret value cannot be empty.",
"exec.secretValuePlaceholder": "Paste the secret value. It will be encrypted before persistence.",
"exec.secretBindingInfo": "Connector secret bindings use `ENV_NAME=secret-name` lines. Values stay outside renderer state after submission.",
"exec.status": "Status",
// ── Communication hub ──
"comm.communicationHub": "Communication Hub",
"comm.newMessage": "New message",
"comm.channels": "Channels",
"comm.taskDiscussions": "Task Discussions",
"comm.noActiveDepartments": "No active departments yet.",
"comm.channelsCount": "{0} channels",
"comm.agentsCount": "{0} agents",
"comm.selectChannel": "Select a channel",
"comm.selectChannelView": "Select a channel to view messages",
"comm.monitorComms": "Monitor internal agent communications across departments",
"comm.noDiscussionYet": "No discussion yet",
"comm.commentsAppearHere": "Comments for this task will appear here as agents and the board coordinate.",
"comm.noMatchFilters": "No messages match your filters",
"comm.noMessagesChannel": "No messages in this channel yet",
"comm.adjustFilters": "Try adjusting your search or filters",
"comm.messagesAppear": "Messages will appear here as agents work and communicate",
"comm.openTask": "Open task",
"comm.selectChannelSend": "Select a channel to read and send messages.",
"comm.replyInDiscussion": "Reply in task discussion...",
"comm.typeMessage": "Type a message...",
"comm.sendShortcut": "Press {0}+Enter to send",
"comm.reply": "Reply",
"comm.replyCount": "{0} reply",
"comm.repliesCount": "{0} replies",
"comm.replyingTo": "Replying to",
"comm.searchMessages": "Search messages...",
"comm.filters": "Filters",
"comm.allChannels": "All channels",
"comm.direct": "Direct",
"comm.department": "Department",
"comm.company": "Company",
"comm.incident": "Incident",
"comm.allPriorities": "All priorities",
"comm.urgent": "Urgent",
"comm.normal": "Normal",
"comm.low": "Low",
"comm.unreadOnly": "Unread only",
"comm.clearFilters": "Clear filters",
"comm.composeMessage": "Compose Message",
"comm.fromAgent": "From Agent",
"comm.channel": "Channel",
"comm.companyBroadcast": "Company-wide broadcast",
"comm.departmentChannel": "Department channel",
"comm.projectChannel": "Project channel",
"comm.incidentChannel": "Incident channel",
"comm.directMessage": "Direct message",
"comm.toAgent": "To Agent",
"comm.selectRecipient": "Select a recipient...",
"comm.selectDepartment": "Select department...",
"comm.selectProject": "Select project...",
"comm.subject": "Subject",
"comm.messageSubject": "Message subject",
"comm.message": "Message",
"comm.writeMessage": "Write your message...",
"comm.sendMessage": "Send Message",
"comm.subjectOptional": "Subject (optional)",
"comm.selectConversation": "Select a direct conversation partner before sending a DM.",
"comm.directMessages": "Direct Messages",
"comm.companyWide": "Company-wide",
"comm.unread": "unread",
"comm.messages": "messages",
"comm.message_singular": "message",
"comm.comments": "comments",
"comm.comment_singular": "comment",
// ── Dashboard ──
"dash.companyHealth": "Company Health",
"dash.excellent": "Excellent",
"dash.good": "Good",
"dash.fair": "Fair",
"dash.poor": "Poor",
"dash.critical": "Critical",
"dash.noAgentsYet": "No agents hired yet",
"dash.createCeoAgent": "Create a CEO agent to start building your company",
"dash.noActivityYet": "No activity yet. Start by creating agents and tasks.",
"dash.liveRuns": "Live Runs",
"dash.running": "running",
"dash.allQuiet": "All quiet",
"dash.activeTasks": "Active Tasks",
"dash.queued": "queued",
"dash.approvals": "Approvals",
"dash.needsAttention": "Needs your attention",
"dash.allClear": "All clear",
"dash.failedRuns": "Failed Runs",
"dash.totalSpend": "Total Spend",
"dash.projects": "Projects",
"dash.total": "total",
"dash.unreadMessages": "Unread Messages",
"dash.needsTriage": "Needs triage",
"dash.allRead": "All read",
"dash.browserOps": "Browser Ops",
"dash.blockedOrFailed": "Blocked or failed",
"dash.nominal": "Nominal",
"dash.workflowStalls": "Workflow Stalls",
"dash.pausedOrFailed": "Paused or failed",
"dash.healthy": "Healthy",
"dash.departments": "Departments",
"dash.orgChart": "Org chart",
"dash.currentSprint": "Current Sprint",
"dash.manage": "Manage",
"dash.goalProgress": "Goal Progress",
"dash.allGoals": "All goals",
"dash.taskPipeline": "Task Pipeline",
"dash.viewAll": "View all",
"dash.workforce": "Workforce",
"dash.recentActivity": "Recent Activity",
"dash.infrastructure": "Infrastructure",
"dash.needsAttentionSection": "Needs Attention",
"dash.daysLeft": "{0} days left",
"dash.tasksComplete": "{0}/{1} tasks complete",
"dash.people": "People",
"dash.active": "Active",
"dash.blocked": "Blocked",
"dash.backlog": "Backlog",
"dash.todo": "Todo",
"dash.inProgress": "In Progress",
"dash.inReview": "In Review",
"dash.noTasksYet": "No tasks yet",
"dash.tasksAppearBoard": "Tasks created by agents will appear here on the board",
"dash.runFailed": "Run failed",
"dash.browserAction": "Browser action",
"dash.workflow": "Workflow",
"dash.points": "points",
// ── Organization ──
"org.noAgentsYet": "No agents yet",
"org.noTitle": "No title",
"org.noGoalsYet": "No goals yet",
"org.noProjectsYet": "No projects yet",
"org.noAgentsToDisplay": "No agents to display",
"org.saveChanges": "Save changes",
"org.submitHireRequest": "Submit hire request",
"org.createGoal": "Create goal",
"org.createProject": "Create project",
"org.goalTitle": "Goal title",
"org.successCriteria": "Success criteria",
"org.ownerAgent": "Owner agent",
"org.noOwner": "No owner",
"org.parentGoal": "Parent goal",
"org.noParentGoal": "No parent goal",
"org.projectName": "Project name",
"org.linkedGoal": "Linked goal",
"org.noLinkedGoal": "No linked goal",
"org.leadAgent": "Lead agent",
"org.noLead": "No lead",
"org.targetDate": "Target date",
"org.noManager": "No manager",
"org.agentName": "Agent name",
"org.monthlyBudget": "Monthly budget (USD)",
"org.continuousExecution": "Continuous Execution",
"org.enableHeartbeat": "Enable heartbeat",
"org.wakeNow": "Wake now",
"org.thinkingEffort": "Thinking effort",
"org.default": "Default",
"org.model": "Model",
"org.interval": "Interval",
"org.fit": "Fit",
// ── Runs ──
"runs.noRunsYet": "No runs yet",
"runs.noRunsDetail": "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.",
"runs.activeLabel": "Active",
"runs.queuedOrRunning": "Queued or running right now",
"runs.needsAttention": "Needs attention",
"runs.failedTimedInterrupted": "Failed, timed out, or interrupted",
"runs.completedLabel": "Completed",
"runs.finishedWithout": "Finished without current intervention",
"runs.runQueue": "Run queue",
"runs.searchRuns": "Search by task, agent, connector, workspace, or error",
"runs.noRunsMatch": "No runs match this filter",
"runs.noRunsMatchDetail": "Widen the status filter or clear the search query to inspect the full run history.",
"runs.runDetail": "Run detail",
"runs.openConsole": "Open console",
"runs.cancelRun": "Cancel run",
"runs.noSummary": "No summary yet",
"runs.unknownAgent": "Unknown agent",
"runs.noExclusiveLock": "No exclusive workspace lock",
"runs.connectorDefault": "Connector default",
"runs.openLogFile": "Open log file",
"runs.logTail": "Log tail",
"runs.incrementalReader": "Incremental reader",
"runs.reloadTail": "Reload tail",
"runs.loadOlder": "Load older",
"runs.showingBytes": "Showing bytes {0} to {1} of {2}",
"runs.refreshing": "Refreshing...",
"runs.atLatestTail": "At latest tail",
"runs.tailWindowLoaded": "Tail window loaded",
"runs.noLogOutput": "No log output captured yet.",
// ── Automation ──
"auto.automationRules": "Automation Rules",
"auto.newRule": "New Rule",
"auto.totalRules": "Total Rules",
"auto.totalExecutions": "Total Executions",
"auto.recentActivity": "Recent Activity",
"auto.rules": "Rules",
"auto.activityLog": "Activity Log",
"auto.noAutomationRules": "No automation rules",
"auto.noAutomationDesc": "Create your first rule to make the company autonomous. Rules trigger actions based on events like task changes, run completions, and agent idle states.",
"auto.createFirstRule": "Create First Rule",
"auto.noRulesMatch": "No rules match the current filter.",
"auto.noActivityLog": "No automation activity yet. Rules will log executions here when they fire.",
"auto.editRule": "Edit Automation Rule",
"auto.newRuleTitle": "New Automation Rule",
"auto.ruleName": "Rule Name",
"auto.ruleDescription": "Description",
"auto.trigger": "Trigger",
"auto.conditionsJson": "Conditions (JSON)",
"auto.action": "Action",
"auto.actionConfigJson": "Action Configuration (JSON)",
"auto.sourceDepartment": "Source Department",
"auto.targetDepartment": "Target Department",
"auto.priorityLabel": "Priority",
"auto.lowPriority": "Low priority",
"auto.highPriority": "High priority",
"auto.saveChanges": "Save Changes",
"auto.createRule": "Create Rule",
"auto.workflowPipelines": "Workflow Pipelines",
"auto.newWorkflow": "New Workflow",
"auto.noWorkflowPipelines": "No workflow pipelines",
"auto.noWorkflowDesc": "Workflows are multi-step automation pipelines. They chain actions together so your AI company runs end-to-end processes automatically.",
"auto.createFirstWorkflow": "Create First Workflow",
"auto.editWorkflow": "Edit Workflow",
"auto.newWorkflowTitle": "New Workflow",
"auto.workflowName": "Workflow Name",
"auto.workflowDescription": "Description",
"auto.triggerConfig": "Trigger Configuration (JSON)",
"auto.pipelineSteps": "Pipeline Steps (JSON Array)",
"auto.createWorkflow": "Create Workflow",
"auto.automationEngine": "Automation Engine",
"auto.automationEngineDesc": "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, and keeping every department in sync without manual intervention.",
"auto.automationRulesTab": "Automation Rules",
"auto.workflowsTab": "Workflows",
"auto.noStepsDefined": "No steps defined",
"auto.requiresApproval": "Requires approval",
"auto.deleteConfirm": "Are you sure you want to delete",
"auto.cannotUndo": "This action cannot be undone.",
// ── Social ──
"social.socialMedia": "Social Media",
"social.accountsConnected": "{0} account{1} connected",
"social.addAccount": "Add Account",
"social.howItWorks": "How it works",
"social.howItWorksDesc": "Add your social media accounts here. Click \"Login\" to open a browser where you can sign in. Your session is saved locally so marketing agents can post content, reply to users, and browse feeds using browser automation. All actions are logged and can require Board approval.",
"social.accounts": "Accounts",
"social.actionHistory": "Action History",
"social.noAccounts": "No social accounts",
"social.noAccountsDesc": "Add your first social media account to enable marketing automation",
"social.noActions": "No browser actions yet. Agents will create actions when working on social media tasks.",
"social.addSocialAccount": "Add Social Account",
"social.platform": "Platform",
"social.usernameHandle": "Username / Handle",
"social.displayName": "Display Name (optional)",
"social.requireApproval": "Require Board Approval for Posts",
"social.requireApprovalDesc": "Agents must get approval before publishing content to this account",
"social.approvalRequired": "Approval required",
"social.login": "Login",
"social.openTask": "Open task",
"social.openApproval": "Open approval",
"social.screenshot": "Screenshot",
// ── Company Operations ──
"ops.documentCenter": "Document Center",
"ops.knowledgeBase": "Knowledge Base",
"ops.meetings": "Meetings",
"ops.sprintBoard": "Sprint Board",
"ops.newDocument": "New document",
"ops.newKnowledgeEntry": "New knowledge entry",
"ops.newMeeting": "New meeting",
"ops.newSprint": "New sprint",
"ops.deleteDocument": "Delete document",
"ops.deleteDocumentMsg": "This removes the document from the company knowledge surface.",
"ops.deleteKnowledgeEntry": "Delete knowledge entry",
"ops.deleteKnowledgeMsg": "This removes the entry from the shared company knowledge base.",
"ops.deleteMeeting": "Delete meeting",
"ops.deleteMeetingMsg": "This removes the meeting and its operational context from the company timeline.",
"ops.deleteSprint": "Delete sprint",
"ops.deleteSprintMsg": "This removes the sprint board entry. Historical run and task records remain.",
"ops.items": "items",
"ops.item": "item",
"ops.new": "New",
// ── Inbox / Standup / Performance ──
"inbox.title": "Inbox",
"inbox.itemsNeedAttention": "{0} item{1} need{2} your attention",
"inbox.allClear": "All clear!",
"inbox.noItemsNow": "No items need your attention right now",
"inbox.all": "All",
"inbox.approvalsFilter": "Approvals",
"inbox.reviewsFilter": "Reviews",
"inbox.failedFilter": "Failed",
"inbox.blockedFilter": "Blocked",
"inbox.budgetFilter": "Budget",
"inbox.messagesFilter": "Messages",
"standup.dailyStandup": "Daily Standup",
"standup.generated": "Generated",
"standup.productive": "Productive",
"standup.blockers": "Blockers",
"standup.pendingApprovals": "Approvals",
"standup.budgetUsed": "Budget Used",
"standup.highlights": "Highlights",
"standup.agentStatus": "Agent Status",
"standup.noData": "No standup data",
"standup.noDataDesc": "Company needs active agents to generate standup reports",
"standup.inProgress": "in progress",
"standup.doneLabel": "done",
"standup.blockedLabel": "blocked",
"perf.noMetrics": "No metrics yet",
"perf.noMetricsDesc": "Metrics will appear after agents start working",
"perf.successRate": "Success Rate",
"perf.tasksDone": "Tasks Done",
"perf.throughput": "Throughput",
"perf.tasksPerDay": "tasks/day",
"perf.avgCostTask": "Avg Cost/Task",
"perf.runActivity30d": "Run Activity (30d)",
"perf.succeeded": "succeeded",
"perf.failed": "failed",
"perf.agentPerformance": "Agent Performance",
"perf.agent": "Agent",
"perf.tasksDoneCol": "Tasks Done",
"perf.successPct": "Success %",
"perf.runsCol": "Runs",
"perf.avgDuration": "Avg Duration",
"perf.costCol": "Cost",
"perf.ofTotal": "of {0} total",
// ── Connectors ──
"conn.saveConnector": "Save connector",
"conn.status": "Status",
"conn.capabilityMatrix": "Capability matrix",
"conn.command": "Command",
"conn.commandInfo": "AgentCompany pins connector execution to the audited binary for this runtime. Command overrides are not accepted from the renderer.",
"conn.model": "Model",
"conn.modelRequired": "Model (required)",
"conn.connectorDefault": "Use connector default",
"conn.selectModel": "Please select a model...",
"conn.loadingModels": "Loading available models...",
"conn.secretBindings": "Secret bindings",
"conn.operationalNotes": "Operational notes",
"conn.healthCheckInfo": "Connector health checks run automatically on app launch, after save, and after terminal login is opened.",
"conn.refreshingState": "Refreshing connector state...",
"conn.latestHealthCheck": "Latest health-check detail",
"conn.configDoc": "Configuration doc",
};
+25
View File
@@ -0,0 +1,25 @@
import { createContext, useContext } from "react";
import type { AppLocale } from "@shared/locale";
import { DEFAULT_LOCALE } from "@shared/locale";
import { en } from "./en";
import { zh } from "./zh";
const translations: Record<AppLocale, Record<string, string>> = { en, zh };
export type TranslationFn = (key: string, fallback?: string) => string;
export function createTranslator(locale: AppLocale): TranslationFn {
const dict = translations[locale] ?? translations[DEFAULT_LOCALE];
return (key: string, fallback?: string) => dict[key] ?? fallback ?? key;
}
export const LocaleContext = createContext<AppLocale>(DEFAULT_LOCALE);
export const TranslationContext = createContext<TranslationFn>(createTranslator(DEFAULT_LOCALE));
export function useLocale(): AppLocale {
return useContext(LocaleContext);
}
export function useT(): TranslationFn {
return useContext(TranslationContext);
}
+797
View File
@@ -0,0 +1,797 @@
export const zh: Record<string, string> = {
// ── 导航项 ──
"nav.overview": "总览",
"nav.inbox": "收件箱",
"nav.standup": "站会",
"nav.performance": "绩效",
"nav.runs": "运行",
"nav.agents": "智能体",
"nav.orgchart": "组织架构",
"nav.hiring": "招聘",
"nav.communication": "消息",
"nav.goals": "目标",
"nav.projects": "项目",
"nav.tasks": "任务",
"nav.sprints": "迭代",
"nav.automation": "自动化",
"nav.approvals": "审批",
"nav.documents": "文档",
"nav.knowledge": "知识库",
"nav.meetings": "会议",
"nav.costs": "成本",
"nav.activity": "活动",
"nav.social": "社交媒体",
"nav.connectors": "连接器",
"nav.workspaces": "工作区",
"nav.settings": "设置",
// ── 导航分组 ──
"nav.group.overview": "概览",
"nav.group.organization": "组织",
"nav.group.automation": "自动化",
"nav.group.knowledge": "知识",
"nav.group.operations": "运营",
"nav.group.infrastructure": "基础设施",
// ── 应用级文本 ──
"app.loading": "正在加载本地配置...",
"app.selectCompany": "选择公司",
"app.newCompany": "新建公司",
"app.commandPalette": "命令面板",
"app.refresh": "刷新",
"app.console": "控制台",
"app.hideConsole": "隐藏控制台",
"app.showConsole": "显示控制台",
"app.collapse": "收起",
"app.expand": "展开",
"app.noResults": "未找到匹配结果",
"app.clear": "清除",
"app.search": "搜索版块、智能体、任务、目标、项目...",
// ── 引导流程 ──
"onboarding.title": "创建一个本地优先的\n智能体公司。",
"onboarding.subtitle": "AgentCompany 桌面端",
"onboarding.description": "此流程将创建一个公司,绑定一个可执行的连接器,关联一个可选的工作区,编写第一个目标,创建运营项目,并安排第一个执行任务。",
"onboarding.step1Title": "建立运营边界",
"onboarding.step2Title": "定义第一个目标",
"onboarding.step3Title": "启动概要",
"onboarding.stepLabel1": "公司",
"onboarding.stepLabel2": "使命",
"onboarding.stepLabel3": "启动",
"onboarding.companyName": "公司名称",
"onboarding.companyNamePlaceholder": "例如:北线系统",
"onboarding.companyDescription": "公司描述",
"onboarding.companyDescriptionPlaceholder": "可选的公司运营模式说明。",
"onboarding.primaryConnector": "主连接器",
"onboarding.connectorReadiness": "连接器就绪状态",
"onboarding.connectorInfo": "此处仅展示处于 `ready` 状态且支持会话恢复和结构化记录的连接器。如列表为空,请在设置中重新运行连接器健康检查。",
"onboarding.workspaceFolder": "工作区目录",
"onboarding.workspaceFolderOptional": "(可选)",
"onboarding.workspaceFolderPlaceholder": "/Users/you/project",
"onboarding.workspaceHelp": "设置后,首个执行任务将绑定到此路径,该路径将被注册为主工作区。",
"onboarding.autoApprove": "自动批准招聘和战略审批",
"onboarding.autoApproveLabel": "自动批准招聘和战略审批",
"onboarding.autoApproveHelp": "适用于单操作员流程。如需董事会对组织变更进行明确审批,请关闭此选项。",
"onboarding.continue": "继续",
"onboarding.back": "返回",
"onboarding.goal": "目标",
"onboarding.goalPlaceholder": "例如:交付第一个面向客户的桌面演示",
"onboarding.acceptanceCriteria": "验收标准",
"onboarding.acceptanceCriteriaPlaceholder": "描述成功执行的标准、不可跳过的事项以及交付约束。",
"onboarding.executionHandoff": "执行交接",
"onboarding.executionHandoffLine1": "所选连接器将作为首次启动的CEO智能体运行时。",
"onboarding.executionHandoffLine2": "应用将创建公司、主工作区、CEO智能体、启动项目和第一个任务。",
"onboarding.executionHandoffLine3": "如果执行器已就绪,首次运行将立即排队;否则调度器将在运行时可用后执行。",
"onboarding.launching": "正在启动...",
"onboarding.launchCompany": "启动公司",
"onboarding.isLive": "已上线。",
"onboarding.launchDescription": "公司已创建。首个项目和任务已注册,启动执行已排入队列。",
"onboarding.launchDescriptionDeferred": "公司已创建。首个项目和任务已注册,启动执行将在运行时就绪后进行。",
"onboarding.connector": "连接器",
"onboarding.leadAgent": "首席智能体",
"onboarding.ceoAgentCreated": "CEO智能体已创建",
"onboarding.leadAgentUnavailable": "首席智能体不可用",
"onboarding.primaryWorkspace": "主工作区",
"onboarding.registered": "已注册",
"onboarding.notBound": "未绑定",
"onboarding.kickoffProject": "启动项目",
"onboarding.created": "已创建",
"onboarding.unavailable": "不可用",
"onboarding.initialRun": "首次运行",
"onboarding.queuedInOrchestration": "已排入编排队列",
"onboarding.deferredUntilReady": "延迟至运行时就绪",
"onboarding.opening": "正在打开...",
"onboarding.openControlCenter": "打开控制中心",
"onboarding.cancel": "取消",
"onboarding.enterCompanyName": "请输入公司名称。",
"onboarding.connectorRequired": "继续前需要至少一个处于可执行状态的连接器。",
"onboarding.describeFirstGoal": "请描述公司的第一个目标。",
"onboarding.unableToBootstrap": "无法初始化公司。",
"onboarding.unableToOpenControlCenter": "无法打开控制中心。",
"onboarding.connectorNotInstalled": "请先安装、认证并验证至少一个受支持的CLI运行时,再创建公司。",
"onboarding.connectorWarning": "请先安装、认证并验证至少一个受支持的CLI运行时,再创建公司。",
"onboarding.languageSelection": "语言",
"onboarding.selectLanguage": "选择语言",
// ── 设置 ──
"settings.profile": "配置",
"settings.theme": "主题",
"settings.system": "跟随系统",
"settings.light": "浅色",
"settings.dark": "深色",
"settings.updates": "更新",
"settings.enabled": "已启用",
"settings.disabled": "已禁用",
"settings.logRetention": "日志保留",
"settings.days": "天",
"settings.backup": "备份配置",
"settings.restore": "恢复配置",
"settings.secretBindings": "密钥绑定",
"settings.language": "语言",
// ── 状态标签 ──
"status.idle": "空闲",
"status.active": "活跃",
"status.running": "运行中",
"status.paused": "已暂停",
"status.error": "错误",
"status.pending_approval": "待审批",
"status.terminated": "已终止",
"status.not_installed": "未安装",
"status.detected": "已检测",
"status.auth_required": "需要认证",
"status.ready": "就绪",
"status.degraded": "降级",
"status.queued": "排队中",
"status.succeeded": "已成功",
"status.failed": "已失败",
"status.cancelled": "已取消",
"status.timed_out": "已超时",
"status.interrupted": "已中断",
"status.backlog": "待办池",
"status.todo": "待处理",
"status.in_progress": "进行中",
"status.in_review": "审核中",
"status.done": "已完成",
"status.blocked": "已阻塞",
"status.planned": "已规划",
"status.achieved": "已达成",
"status.completed": "已完成",
"status.active_sprint": "当前迭代",
"status.review": "审核",
"status.scheduling": "调度中",
"status.draft": "草稿",
"status.approved": "已批准",
"status.rejected": "已拒绝",
"status.revision_requested": "要求修改",
"status.login_required": "需要登录",
"status.suspended": "已挂起",
"status.pending": "待处理",
// ── 通用标签 ──
"common.save": "保存",
"common.delete": "删除",
"common.cancel": "取消",
"common.edit": "编辑",
"common.new": "新建",
"common.close": "关闭",
"common.yes": "是",
"common.no": "否",
"common.confirm": "确认",
"common.status": "状态",
"common.started": "开始时间",
"common.finished": "结束时间",
"common.cost": "费用",
"common.openLog": "打开日志",
"common.noRunSelected": "未选择运行",
"common.waitingForLog": "等待日志输出...",
"common.sections": "版块",
"common.agents": "智能体",
"common.tasks": "任务",
"common.runs": "运行",
"common.goals": "目标",
"common.projects": "项目",
// ── 控制台面板 ──
"console.title": "控制台",
"console.noRuns": "暂无运行",
// ── 实体标签(命令面板分类) ──
"entity.sections": "版块",
"entity.agents": "智能体",
"entity.tasks": "任务",
"entity.runs": "运行",
"entity.goals": "目标",
"entity.projects": "项目",
// ── 招聘页面 ──
"hiring.title": "招聘管线",
"hiring.activeProcesses": "个招聘流程",
"hiring.requisition": "申请",
"hiring.pendingApproval": "待审批",
"hiring.approved": "已批准",
"hiring.onboarding": "入职中",
"hiring.active": "已入职",
"hiring.allAgents": "全部智能体",
"hiring.totalAgents": "智能体总数",
"hiring.pendingHires": "待入职",
"hiring.departments": "部门数",
"hiring.monthlyPayroll": "月度薪资",
"hiring.departmentHeadcount": "部门人数",
"hiring.topLevel": "最高层级",
"hiring.reportsTo": "汇报给",
"hiring.proposedBy": "由...推荐",
// ── 组织页面 ──
"org.agentRoster": "智能体名册",
"org.newHireRequest": "新招聘申请",
"org.editAgent": "编辑",
"org.requestNewHire": "申请新招聘",
"org.goalStack": "目标栈",
"org.newGoal": "新目标",
"org.newProject": "新项目",
// ── 任务页面 ──
"tasks.taskQueue": "任务队列",
"tasks.newTask": "新任务",
// ── 审批页面 ──
"approvals.reviewsInProgress": "智能体审核中",
"approvals.pendingApprovals": "待审批",
"approvals.approvalDetail": "审批详情",
// ── 成本页面 ──
"costs.recentEntries": "近期费用",
"costs.budgetPosture": "预算概况",
// ── 活动页面 ──
"activity.timeline": "活动时间线",
// ── 工作区页面 ──
"workspaces.registry": "工作区注册表",
"workspaces.controlCenter": "工作区控制中心",
"workspaces.addWorkspace": "添加工作区",
// ── 设置页面 ──
"settings.profileAndRelease": "配置与发布",
"settings.secretBindingsTitle": "密钥绑定",
// ── 连接器页面 ──
"connectors.registry": "连接器注册表",
// ── 总览 ──
"dashboard.title": "总览",
// ── 运行页面 ──
"runs.title": "运行中心",
// ── 文档页面 ──
"documents.title": "文档",
// ── 知识库页面 ──
"knowledge.title": "知识库",
// ── 会议页面 ──
"meetings.title": "会议",
// ── 迭代页面 ──
"sprints.title": "迭代",
// ── 社交页面 ──
"social.title": "社交账号",
// ── 自动化页面 ──
"automation.title": "自动化",
// ── 沟通页面 ──
"communication.title": "沟通中心",
// ── 通用面板标题 ──
"panel.choose": "选择",
"panel.noData": "暂无数据",
"panel.empty": "空",
"panel.loading": "加载中...",
// ── 执行/任务检查器 ──
"exec.taskQueueEmpty": "任务队列为空",
"exec.deleteTask": "删除任务",
"exec.deleteTaskConfirm": "永久删除「{0}」?此操作无法撤消。",
"exec.budgetUsed": "已使用 {0}% 的月度预算",
"exec.taskChangesSaved": "任务更改已保存。",
"exec.taskMovedTo": "任务已移至{0}。",
"exec.approvalCreatedBlocked": "审批已创建并转交管理层。",
"exec.approvalCreated": "审批已创建。",
"exec.title": "标题",
"exec.description": "描述",
"exec.priority": "优先级",
"exec.assignee": "负责人",
"exec.unassigned": "未分配",
"exec.workspace": "工作区",
"exec.noWorkspace": "无工作区",
"exec.project": "项目",
"exec.noProject": "无项目",
"exec.goal": "目标",
"exec.noGoal": "无目标",
"exec.save": "保存",
"exec.cancel": "取消",
"exec.assigneeLabel": "负责人:",
"exec.projectLabel": "项目:",
"exec.goalLabel": "目标:",
"exec.parentLabel": "父任务:",
"exec.workspaceLabel": "工作区:",
"exec.costLabel": "费用:",
"exec.beingReviewedBy": "正在由 {0} 审核",
"exec.submittedForReview": "{0} 已提交此任务进行审核。{1} 将批准、请求修改或升级处理。",
"exec.governanceRequired": "需要治理操作",
"exec.waitingOnReview": "任务等待审核中",
"exec.blockedGovernance": "创建审批以便管理层解决阻塞、提供凭证或授权下一步特权操作。",
"exec.reviewGovernance": "如果此任务需要操作者明确决策才能继续,请创建审批条目。",
"exec.createUnblockApproval": "创建解除阻塞审批",
"exec.createReviewApproval": "创建审核审批",
"exec.cancelRun": "取消运行",
"exec.run": "运行",
"exec.done": "完成",
"exec.unblock": "解除阻塞",
"exec.discussion": "讨论",
"exec.edit": "编辑",
"exec.delete": "删除",
"exec.subtasks": "子任务",
"exec.doneCount": "{0}/{1} 完成",
"exec.runHistory": "运行历史",
"exec.moreRuns": "还有 {0} 条运行记录",
"exec.runningEllipsis": "运行中...",
"exec.completedRun": "已完成的运行",
"exec.communication": "沟通",
"exec.noMessagesYet": "暂无消息。智能体在运行时会在此发布更新。",
"exec.sendMessage": "向团队发送消息...",
"exec.send": "发送",
"exec.createTask": "创建任务",
"exec.taskTitle": "任务标题",
"exec.noReviewsInProgress": "没有进行中的智能体审核",
"exec.awaitingReview": "等待审核",
"exec.reviewedBy": "由...审核",
"exec.noApprovals": "暂无审批",
"exec.deleteApproval": "删除审批",
"exec.deleteApprovalConfirm": "永久删除此审批?此操作无法撤消。",
"exec.requestedBy": "请求者:",
"exec.task": "任务:",
"exec.created": "创建时间:",
"exec.updated": "更新时间:",
"exec.proposedHire": "拟招聘",
"exec.name": "姓名:",
"exec.role": "角色:",
"exec.titleLabel": "职位:",
"exec.capabilities": "能力:",
"exec.connector": "连接器:",
"exec.budget": "预算:",
"exec.reportsTo": "汇报给:",
"exec.orgStrategy": "组织战略",
"exec.decisionNote": "决策备注",
"exec.decisionNotePlaceholder": "附加决策的可选备注...",
"exec.approve": "批准",
"exec.requestRevision": "要求修改",
"exec.reject": "拒绝",
"exec.lastNote": "最近备注:",
"exec.noCostData": "暂无费用数据",
"exec.allConnectors": "全部连接器",
"exec.runCost": "运行费用",
"exec.unknownAgent": "未知智能体",
"exec.unattributed": "未归因",
"exec.totalEntries": "合计({0} 条记录)",
"exec.knownSpend": "已知支出",
"exec.unattributedEntries": "未归因条目",
"exec.noActivityYet": "暂无活动",
"exec.searchActivity": "搜索活动...",
"exec.allTypes": "全部类型",
"exec.noMatchingActivity": "无匹配活动",
"exec.loadMore": "加载更多(还剩 {0} 条)",
"exec.noWorkspacesYet": "暂无工作区",
"exec.deleteWorkspace": "删除工作区",
"exec.deleteWorkspaceConfirm": "删除工作区「{0}」?这只会从公司注册表中移除,不会影响磁盘上的文件。",
"exec.inspect": "检查",
"exec.reveal": "在文件管理器中显示",
"exec.noRemoteUrl": "无远程地址",
"exec.selectWorkspace": "选择一个工作区",
"exec.selectWorkspaceDetail": "选择一个工作区以检查绑定的智能体、任务、运行和文档。",
"exec.agents": "智能体",
"exec.tasks": "任务",
"exec.runs": "运行",
"exec.docs": "文档",
"exec.assignedAgents": "分配的智能体",
"exec.noAgentsBound": "当前没有智能体绑定到此工作区。",
"exec.openTasks": "进行中的任务",
"exec.noTasksTarget": "尚无任务指向此工作区。",
"exec.recentRuns": "最近运行",
"exec.noRunsExecuted": "此工作区尚未执行过运行。",
"exec.openFolder": "打开文件夹",
"exec.noLinkedProject": "无关联项目",
"exec.workspaceName": "工作区名称",
"exec.localPath": "本地路径",
"exec.choose": "选择",
"exec.repoUrl": "仓库地址",
"exec.branchRef": "分支 / 引用",
"exec.noProjectBinding": "不绑定项目",
"exec.addWorkspace": "添加工作区",
"exec.profile": "配置文件",
"exec.profilePath": "配置文件路径",
"exec.preferences": "偏好设置",
"exec.enableUpdater": "启用更新检查",
"exec.companyAutonomy": "公司自治",
"exec.autoApproveLabel": "自动批准招聘和战略审批",
"exec.autoApproveDesc": "自动批准智能体招聘请求和CEO战略审批,使公司无需等待董事会即可继续运营。",
"exec.actions": "操作",
"exec.saveSettings": "保存设置",
"exec.checkForUpdates": "检查更新",
"exec.backupProfile": "备份配置",
"exec.restoreProfile": "恢复配置",
"exec.secretBackend": "密钥后端",
"exec.secrets": "密钥",
"exec.noSecretsStored": "暂无存储的密钥",
"exec.secretName": "名称",
"exec.secretDescription": "描述",
"exec.secretBackendCol": "后端",
"exec.secretUpdated": "更新时间",
"exec.noDescription": "无描述",
"exec.deleteSecret": "删除密钥",
"exec.deleteSecretConfirm": "永久删除密钥「{0}」?引用它的连接器将失去访问权限。",
"exec.secretDeleted": "密钥已删除。",
"exec.settingsSaved": "设置已保存。",
"exec.updateCheckStarted": "更新检查已开始。",
"exec.updateCheckSkipped": "更新检查已跳过,因为未配置发布更新源。",
"exec.profileBackupCreated": "配置备份已创建于 {0}。",
"exec.profileRestored": "配置已恢复。",
"exec.storeSecret": "存储密钥",
"exec.secretNameLabel": "密钥名称",
"exec.secretNameRequired": "密钥名称为必填项。",
"exec.secretNameFormat": "名称必须是字母数字加下划线,以字母或下划线开头。",
"exec.secretValueLabel": "密钥值",
"exec.secretValueEmpty": "密钥值不能为空。",
"exec.secretValuePlaceholder": "粘贴密钥值。提交前将进行加密。",
"exec.secretBindingInfo": "连接器密钥绑定使用 `ENV_NAME=secret-name` 格式。提交后值将不再保留在渲染进程状态中。",
"exec.status": "状态",
// ── 沟通中心 ──
"comm.communicationHub": "沟通中心",
"comm.newMessage": "新消息",
"comm.channels": "频道",
"comm.taskDiscussions": "任务讨论",
"comm.noActiveDepartments": "尚无活跃部门。",
"comm.channelsCount": "{0} 个频道",
"comm.agentsCount": "{0} 个智能体",
"comm.selectChannel": "选择一个频道",
"comm.selectChannelView": "选择一个频道以查看消息",
"comm.monitorComms": "监控各部门之间的内部智能体通信",
"comm.noDiscussionYet": "暂无讨论",
"comm.commentsAppearHere": "智能体和董事会协调时,此任务的评论将显示在这里。",
"comm.noMatchFilters": "没有匹配筛选条件的消息",
"comm.noMessagesChannel": "此频道暂无消息",
"comm.adjustFilters": "尝试调整搜索条件或筛选器",
"comm.messagesAppear": "智能体工作和通信时,消息将显示在这里",
"comm.openTask": "打开任务",
"comm.selectChannelSend": "选择一个频道以读取和发送消息。",
"comm.replyInDiscussion": "在任务讨论中回复...",
"comm.typeMessage": "输入消息...",
"comm.sendShortcut": "按 {0}+Enter 发送",
"comm.reply": "回复",
"comm.replyCount": "{0} 条回复",
"comm.repliesCount": "{0} 条回复",
"comm.replyingTo": "回复",
"comm.searchMessages": "搜索消息...",
"comm.filters": "筛选",
"comm.allChannels": "全部频道",
"comm.direct": "私信",
"comm.department": "部门",
"comm.company": "公司",
"comm.incident": "事件",
"comm.allPriorities": "全部优先级",
"comm.urgent": "紧急",
"comm.normal": "普通",
"comm.low": "低",
"comm.unreadOnly": "仅未读",
"comm.clearFilters": "清除筛选",
"comm.composeMessage": "撰写消息",
"comm.fromAgent": "发送者",
"comm.channel": "频道",
"comm.companyBroadcast": "全公司广播",
"comm.departmentChannel": "部门频道",
"comm.projectChannel": "项目频道",
"comm.incidentChannel": "事件频道",
"comm.directMessage": "私信",
"comm.toAgent": "收件人",
"comm.selectRecipient": "选择收件人...",
"comm.selectDepartment": "选择部门...",
"comm.selectProject": "选择项目...",
"comm.subject": "主题",
"comm.messageSubject": "消息主题",
"comm.message": "消息",
"comm.writeMessage": "编写消息内容...",
"comm.sendMessage": "发送消息",
"comm.subjectOptional": "主题(可选)",
"comm.selectConversation": "请先选择一个私信对象再发送消息。",
"comm.directMessages": "私信",
"comm.companyWide": "全公司",
"comm.unread": "未读",
"comm.messages": "条消息",
"comm.message_singular": "条消息",
"comm.comments": "条评论",
"comm.comment_singular": "条评论",
// ── 总览仪表盘 ──
"dash.companyHealth": "公司健康度",
"dash.excellent": "优秀",
"dash.good": "良好",
"dash.fair": "一般",
"dash.poor": "较差",
"dash.critical": "危急",
"dash.noAgentsYet": "尚未招聘智能体",
"dash.createCeoAgent": "创建CEO智能体以开始构建您的公司",
"dash.noActivityYet": "暂无活动。请先创建智能体和任务。",
"dash.liveRuns": "实时运行",
"dash.running": "运行中",
"dash.allQuiet": "一切平静",
"dash.activeTasks": "活跃任务",
"dash.queued": "排队中",
"dash.approvals": "审批",
"dash.needsAttention": "需要您关注",
"dash.allClear": "一切正常",
"dash.failedRuns": "失败的运行",
"dash.totalSpend": "总支出",
"dash.projects": "项目",
"dash.total": "总计",
"dash.unreadMessages": "未读消息",
"dash.needsTriage": "需要分拣",
"dash.allRead": "全部已读",
"dash.browserOps": "浏览器操作",
"dash.blockedOrFailed": "已阻塞或失败",
"dash.nominal": "正常",
"dash.workflowStalls": "工作流停滞",
"dash.pausedOrFailed": "已暂停或失败",
"dash.healthy": "健康",
"dash.departments": "部门",
"dash.orgChart": "组织架构",
"dash.currentSprint": "当前迭代",
"dash.manage": "管理",
"dash.goalProgress": "目标进度",
"dash.allGoals": "全部目标",
"dash.taskPipeline": "任务管线",
"dash.viewAll": "查看全部",
"dash.workforce": "工作力",
"dash.recentActivity": "最近活动",
"dash.infrastructure": "基础设施",
"dash.needsAttentionSection": "需要关注",
"dash.daysLeft": "剩余 {0} 天",
"dash.tasksComplete": "{0}/{1} 任务完成",
"dash.people": "人员",
"dash.active": "活跃",
"dash.blocked": "阻塞",
"dash.backlog": "待办池",
"dash.todo": "待办",
"dash.inProgress": "进行中",
"dash.inReview": "审核中",
"dash.noTasksYet": "暂无任务",
"dash.tasksAppearBoard": "智能体创建的任务将显示在此看板上",
"dash.runFailed": "运行失败",
"dash.browserAction": "浏览器操作",
"dash.workflow": "工作流",
"dash.points": "点数",
// ── 组织 ──
"org.noAgentsYet": "暂无智能体",
"org.noTitle": "无职位",
"org.noGoalsYet": "暂无目标",
"org.noProjectsYet": "暂无项目",
"org.noAgentsToDisplay": "没有可显示的智能体",
"org.saveChanges": "保存更改",
"org.submitHireRequest": "提交招聘请求",
"org.createGoal": "创建目标",
"org.createProject": "创建项目",
"org.goalTitle": "目标标题",
"org.successCriteria": "成功标准",
"org.ownerAgent": "负责智能体",
"org.noOwner": "无负责人",
"org.parentGoal": "父目标",
"org.noParentGoal": "无父目标",
"org.projectName": "项目名称",
"org.linkedGoal": "关联目标",
"org.noLinkedGoal": "无关联目标",
"org.leadAgent": "负责人",
"org.noLead": "无负责人",
"org.targetDate": "目标日期",
"org.noManager": "无上级",
"org.agentName": "智能体名称",
"org.monthlyBudget": "月度预算(USD)",
"org.continuousExecution": "持续执行",
"org.enableHeartbeat": "启用心跳",
"org.wakeNow": "立即唤醒",
"org.thinkingEffort": "思考力度",
"org.default": "默认",
"org.model": "模型",
"org.interval": "间隔",
"org.fit": "适配",
// ── 运行 ──
"runs.noRunsYet": "暂无运行",
"runs.noRunsDetail": "从任务队列或引导交接启动任务运行。此界面将成为队列状态、失败和实时日志追踪的运营控制台。",
"runs.activeLabel": "活跃",
"runs.queuedOrRunning": "正在排队或运行中",
"runs.needsAttention": "需要关注",
"runs.failedTimedInterrupted": "失败、超时或中断",
"runs.completedLabel": "已完成",
"runs.finishedWithout": "已完成,无需当前干预",
"runs.runQueue": "运行队列",
"runs.searchRuns": "按任务、智能体、连接器、工作区或错误搜索",
"runs.noRunsMatch": "没有匹配此筛选条件的运行",
"runs.noRunsMatchDetail": "放宽状态筛选或清除搜索查询以查看完整运行历史。",
"runs.runDetail": "运行详情",
"runs.openConsole": "打开控制台",
"runs.cancelRun": "取消运行",
"runs.noSummary": "暂无摘要",
"runs.unknownAgent": "未知智能体",
"runs.noExclusiveLock": "无独占工作区锁",
"runs.connectorDefault": "连接器默认",
"runs.openLogFile": "打开日志文件",
"runs.logTail": "日志尾部",
"runs.incrementalReader": "增量读取器",
"runs.reloadTail": "重新加载尾部",
"runs.loadOlder": "加载更早的",
"runs.showingBytes": "显示字节 {0} 到 {1},共 {2}",
"runs.refreshing": "刷新中…",
"runs.atLatestTail": "已在最新尾部",
"runs.tailWindowLoaded": "尾部窗口已加载",
"runs.noLogOutput": "尚未捕获日志输出。",
// ── 自动化 ──
"auto.automationRules": "自动化规则",
"auto.newRule": "新规则",
"auto.totalRules": "规则总数",
"auto.totalExecutions": "执行总数",
"auto.recentActivity": "最近活动",
"auto.rules": "规则",
"auto.activityLog": "活动日志",
"auto.noAutomationRules": "暂无自动化规则",
"auto.noAutomationDesc": "创建您的第一条规则以使公司自主运营。规则根据任务变更、运行完成和智能体空闲等事件触发操作。",
"auto.createFirstRule": "创建第一条规则",
"auto.noRulesMatch": "没有匹配当前筛选条件的规则。",
"auto.noActivityLog": "暂无自动化活动。规则触发时将在此记录执行日志。",
"auto.editRule": "编辑自动化规则",
"auto.newRuleTitle": "新建自动化规则",
"auto.ruleName": "规则名称",
"auto.ruleDescription": "描述",
"auto.trigger": "触发器",
"auto.conditionsJson": "条件(JSON)",
"auto.action": "动作",
"auto.actionConfigJson": "动作配置(JSON)",
"auto.sourceDepartment": "源部门",
"auto.targetDepartment": "目标部门",
"auto.priorityLabel": "优先级",
"auto.lowPriority": "低优先级",
"auto.highPriority": "高优先级",
"auto.saveChanges": "保存更改",
"auto.createRule": "创建规则",
"auto.workflowPipelines": "工作流管线",
"auto.newWorkflow": "新工作流",
"auto.noWorkflowPipelines": "暂无工作流管线",
"auto.noWorkflowDesc": "工作流是多步骤自动化管线。它们将多个操作串联起来,使您的AI公司能自动端到端运行流程。",
"auto.createFirstWorkflow": "创建第一个工作流",
"auto.editWorkflow": "编辑工作流",
"auto.newWorkflowTitle": "新建工作流",
"auto.workflowName": "工作流名称",
"auto.workflowDescription": "描述",
"auto.triggerConfig": "触发器配置(JSON)",
"auto.pipelineSteps": "管线步骤(JSON数组)",
"auto.createWorkflow": "创建工作流",
"auto.automationEngine": "自动化引擎",
"auto.automationEngineDesc": "规则根据公司事件触发操作。工作流将多个步骤串联成管线。两者结合,让您的AI公司完全自主运营——响应变化、分配工作,保持各部门同步,无需人工干预。",
"auto.automationRulesTab": "自动化规则",
"auto.workflowsTab": "工作流",
"auto.noStepsDefined": "未定义步骤",
"auto.requiresApproval": "需要审批",
"auto.deleteConfirm": "确定要删除",
"auto.cannotUndo": "此操作无法撤消。",
// ── 社交 ──
"social.socialMedia": "社交媒体",
"social.accountsConnected": "已连接 {0} 个账号",
"social.addAccount": "添加账号",
"social.howItWorks": "使用说明",
"social.howItWorksDesc": "在此添加您的社交媒体账号。点击「登录」打开浏览器进行登录。您的会话将保存在本地,以便营销智能体通过浏览器自动化发布内容、回复用户和浏览动态。所有操作均有日志记录,且可要求董事会审批。",
"social.accounts": "账号",
"social.actionHistory": "操作历史",
"social.noAccounts": "暂无社交账号",
"social.noAccountsDesc": "添加您的第一个社交媒体账号以启用营销自动化",
"social.noActions": "暂无浏览器操作。智能体处理社交媒体任务时将创建操作。",
"social.addSocialAccount": "添加社交账号",
"social.platform": "平台",
"social.usernameHandle": "用户名 / 账号名",
"social.displayName": "显示名称(可选)",
"social.requireApproval": "发布内容需董事会审批",
"social.requireApprovalDesc": "智能体在向此账号发布内容前必须获得审批",
"social.approvalRequired": "需要审批",
"social.login": "登录",
"social.openTask": "打开任务",
"social.openApproval": "打开审批",
"social.screenshot": "截图",
// ── 公司运营 ──
"ops.documentCenter": "文档中心",
"ops.knowledgeBase": "知识库",
"ops.meetings": "会议",
"ops.sprintBoard": "迭代看板",
"ops.newDocument": "新文档",
"ops.newKnowledgeEntry": "新知识条目",
"ops.newMeeting": "新会议",
"ops.newSprint": "新迭代",
"ops.deleteDocument": "删除文档",
"ops.deleteDocumentMsg": "这将从公司知识面中移除该文档。",
"ops.deleteKnowledgeEntry": "删除知识条目",
"ops.deleteKnowledgeMsg": "这将从公司共享知识库中移除该条目。",
"ops.deleteMeeting": "删除会议",
"ops.deleteMeetingMsg": "这将从公司时间线中移除该会议及其运营上下文。",
"ops.deleteSprint": "删除迭代",
"ops.deleteSprintMsg": "这将移除迭代看板条目。历史运行和任务记录将保留。",
"ops.items": "个项目",
"ops.item": "个项目",
"ops.new": "新建",
// ── 收件箱/站会/绩效 ──
"inbox.title": "收件箱",
"inbox.itemsNeedAttention": "{0} 个事项需要您关注",
"inbox.allClear": "一切正常!",
"inbox.noItemsNow": "目前没有需要您关注的事项",
"inbox.all": "全部",
"inbox.approvalsFilter": "审批",
"inbox.reviewsFilter": "审核",
"inbox.failedFilter": "失败",
"inbox.blockedFilter": "阻塞",
"inbox.budgetFilter": "预算",
"inbox.messagesFilter": "消息",
"standup.dailyStandup": "每日站会",
"standup.generated": "生成时间",
"standup.productive": "有产出",
"standup.blockers": "阻塞项",
"standup.pendingApprovals": "待审批",
"standup.budgetUsed": "预算使用率",
"standup.highlights": "亮点",
"standup.agentStatus": "智能体状态",
"standup.noData": "暂无站会数据",
"standup.noDataDesc": "公司需要活跃的智能体才能生成站会报告",
"standup.inProgress": "进行中",
"standup.doneLabel": "已完成",
"standup.blockedLabel": "已阻塞",
"perf.noMetrics": "暂无指标",
"perf.noMetricsDesc": "智能体开始工作后将显示指标",
"perf.successRate": "成功率",
"perf.tasksDone": "完成任务",
"perf.throughput": "吞吐量",
"perf.tasksPerDay": "任务/天",
"perf.avgCostTask": "平均任务成本",
"perf.runActivity30d": "运行活动(30天)",
"perf.succeeded": "成功",
"perf.failed": "失败",
"perf.agentPerformance": "智能体绩效",
"perf.agent": "智能体",
"perf.tasksDoneCol": "完成任务",
"perf.successPct": "成功率",
"perf.runsCol": "运行次数",
"perf.avgDuration": "平均时长",
"perf.costCol": "费用",
"perf.ofTotal": "共 {0}",
// ── 连接器 ──
"conn.saveConnector": "保存连接器",
"conn.status": "状态",
"conn.capabilityMatrix": "能力矩阵",
"conn.command": "命令",
"conn.commandInfo": "AgentCompany将连接器执行固定到此运行时的审计二进制文件。渲染进程不接受命令覆盖。",
"conn.model": "模型",
"conn.modelRequired": "模型(必填)",
"conn.connectorDefault": "使用连接器默认值",
"conn.selectModel": "请选择模型...",
"conn.loadingModels": "正在加载可用模型...",
"conn.secretBindings": "密钥绑定",
"conn.operationalNotes": "运维备注",
"conn.healthCheckInfo": "连接器健康检查在应用启动、保存后和打开终端登录后自动运行。",
"conn.refreshingState": "正在刷新连接器状态...",
"conn.latestHealthCheck": "最新健康检查详情",
"conn.configDoc": "配置文档",
};
+2 -2
View File
@@ -82,7 +82,7 @@ body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
@@ -120,7 +120,7 @@ select {
}
.mono {
font-family: "JetBrains Mono Variable", monospace;
font-family: "JetBrains Mono Variable", "Microsoft YaHei", monospace;
}
.surface {
+9 -14
View File
@@ -2,25 +2,20 @@ import type { CapabilityMatrix, ConnectorRecord } from "./types";
type ConnectorExecutionCandidate = Pick<ConnectorRecord, "status" | "capabilityMatrix"> & Partial<Pick<ConnectorRecord, "label">>;
function missingExecutionCapabilities(capabilityMatrix: CapabilityMatrix | null | undefined) {
const missing: string[] = [];
if (!capabilityMatrix?.sessionResume) missing.push("session resume");
if (!capabilityMatrix?.structuredTranscript) missing.push("structured transcript");
return missing;
}
export function isConnectorExecutionReady(connector: ConnectorExecutionCandidate) {
return connector.status === "ready" && missingExecutionCapabilities(connector.capabilityMatrix).length === 0;
return connector.status === "ready";
}
export function getConnectorExecutionReadinessIssue(connector: ConnectorExecutionCandidate) {
const label = connector.label ?? "Connector";
if (connector.status !== "ready") {
return `${label} is ${connector.status.replaceAll("_", " ")} and cannot be used for autonomous execution.`;
}
const missing = missingExecutionCapabilities(connector.capabilityMatrix);
if (missing.length === 0) {
if (connector.status === "ready") {
return null;
}
return `${label} does not provide ${missing.join(" and ")}, which AgentCompany requires for autonomous execution.`;
if (connector.status === "not_installed") {
return `${label} is not installed.`;
}
if (connector.status === "auth_required") {
return `${label} requires authentication. Run the CLI login command first.`;
}
return `${label} is ${connector.status.replaceAll("_", " ")} and cannot be used for autonomous execution.`;
}
+1
View File
@@ -244,6 +244,7 @@ export type SidebarBadges = z.infer<typeof sidebarBadgesSchema>;
export const settingsSchema = z.object({
theme: z.enum(["system", "light", "dark"]).optional(),
locale: z.enum(["en", "zh"]).optional(),
updatesEnabled: z.boolean().optional(),
logRetentionDays: z.number().int().min(1).max(365).optional(),
});
+4
View File
@@ -0,0 +1,4 @@
export type AppLocale = "en" | "zh";
export const APP_LOCALES: readonly AppLocale[] = ["en", "zh"] as const;
export const LOCALE_LABELS: Record<AppLocale, string> = { en: "English", zh: "中文" };
export const DEFAULT_LOCALE: AppLocale = "en";
+13
View File
@@ -0,0 +1,13 @@
import type { AppLocale } from "./locale";
/**
* Returns a locale-appropriate system prompt instruction for AI agents.
* When locale is "zh", the instruction tells the agent to respond entirely in Chinese.
* For "en" (or any other locale), returns an empty string (no additional instruction needed).
*/
export function getLocalePromptInstruction(locale: AppLocale): string {
if (locale === "zh") {
return "重要:你必须全程使用中文进行所有沟通、回复、任务描述、评论和消息。所有输出内容必须是中文。";
}
return "";
}
+1
View File
@@ -289,6 +289,7 @@ export interface ProfileSnapshot {
profilePath: string;
updatesEnabled: boolean;
theme: "system" | "light" | "dark";
locale: "en" | "zh";
backend: {
secretBackend: string;
};