This commit is contained in:
ZacharyZhang-NY committed 2026-03-16 00:22:21 -04:00
1 parent d067589276
commit 41eafc7c44
6 files changed
+555 -85

No files matched your search

+26
View File
@@ -759,6 +759,32 @@ export class AgentApiServer {
detail: input.payloadSummary,
});
// Auto-approve hire requests if company has auto_approve_hires enabled
const snapshot = this.db.listSnapshot();
const company = snapshot.companies.find((c) => c.id === companyId);
if (company?.autoApproveHires && (input.type === "hire_agent" || input.type === "approve_ceo_strategy")) {
this.db.decideApproval({ approvalId: id, state: "approved", decisionNote: "Auto-approved by company policy" });
this.db.addActivity({
companyId,
actor: "board",
action: "approval.approved",
entityType: "approval",
entityId: id,
detail: "Auto-approved by company policy",
});
// Wake the requesting agent so they know the hire was approved
this.onWakeAgent(token.agentId, companyId, "approval_resolved");
// Wake the newly hired agent
try {
const fullApproval = this.db.getApprovalRecord(id);
const payload = JSON.parse(fullApproval.payloadJson || "{}") as { agentId?: string };
if (payload.agentId) {
this.onWakeAgent(payload.agentId, companyId, "assignment");
}
} catch { /* ignore if payload is malformed */ }
log.info(`[auto-approve] Auto-approved hire request: ${input.payloadSummary}`);
}
this.onDomainChanged();
this.json(res, 201, { id });
});
+14 -7
View File
@@ -423,8 +423,8 @@ export class AppDatabase {
secretBackend: this.vault.getBackendLabel(),
},
companies: this.readAll<CompanyRecord>(
"select id, name, description, status, created_at as createdAt, updated_at as updatedAt from companies order by created_at asc",
),
"select id, name, description, status, auto_approve_hires as autoApproveHires, created_at as createdAt, updated_at as updatedAt from companies order by created_at asc",
).map((record) => ({ ...record, autoApproveHires: toBoolean(record.autoApproveHires) })) as CompanyRecord[],
connectors,
workspaces: this.readAll<WorkspaceRecord>(
"select id, company_id as companyId, name, local_path as localPath, repo_url as repoUrl, repo_ref as repoRef, project_id as projectId, state, is_primary as isPrimary, created_at as createdAt, updated_at as updatedAt from workspaces order by created_at desc",
@@ -462,18 +462,19 @@ export class AppDatabase {
};
}
saveCompany(input: { id?: string | null; name: string; description: string; status: string }) {
saveCompany(input: { id?: string | null; name: string; description: string; status: string; autoApproveHires?: boolean }) {
const id = ensureId(input.id ?? null);
const ts = nowIso();
const autoApprove = input.autoApproveHires ? 1 : 0;
const existing = this.db.prepare("select id from companies where id = ?").get(id) as { id: string } | undefined;
if (existing) {
this.db
.prepare("update companies set name = ?, description = ?, status = ?, updated_at = ? where id = ?")
.run(input.name, input.description, input.status, ts, id);
.prepare("update companies set name = ?, description = ?, status = ?, auto_approve_hires = ?, updated_at = ? where id = ?")
.run(input.name, input.description, input.status, autoApprove, ts, id);
} else {
this.db
.prepare("insert into companies (id, name, description, status, created_at, updated_at) values (?, ?, ?, ?, ?, ?)")
.run(id, input.name, input.description, input.status, ts, ts);
.prepare("insert into companies (id, name, description, status, auto_approve_hires, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)")
.run(id, input.name, input.description, input.status, autoApprove, ts, ts);
if (!this.getCurrentCompanyId()) {
this.setCurrentCompany(id);
}
@@ -1439,6 +1440,12 @@ export class AppDatabase {
// Column already exists.
}
}
// Auto-approve column on companies
try {
this.db.prepare("alter table companies add column auto_approve_hires integer not null default 0").run();
} catch {
// Column already exists.
}
// Comments table
this.db.exec(`
create table if not exists task_comments (
+313 -60
View File
@@ -1,9 +1,21 @@
import { useState } from "react";
import { motion } from "framer-motion";
import { Buildings, CheckCircle, FolderOpen, WarningCircle, XCircle } from "@phosphor-icons/react";
import { useCallback, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Buildings,
CheckCircle,
FolderOpen,
Lightning,
Robot,
Rocket,
Target,
WarningCircle,
XCircle,
} from "@phosphor-icons/react";
import type { ProfileSnapshot } from "@shared/types";
import { unwrap } from "../lib/desktop";
import { ActionButton, Input } from "./ui";
import { ActionButton, Input, TextArea } from "./ui";
type Step = "company" | "goal" | "launch";
export function CompanyOnboarding({
connectors,
@@ -12,12 +24,19 @@ export function CompanyOnboarding({
connectors: ProfileSnapshot["connectors"];
onDone: () => Promise<void>;
}) {
const [step, setStep] = useState<Step>("company");
const [companyName, setCompanyName] = useState("");
const [workspacePath, setWorkspacePath] = useState("");
const [autoApproveHires, setAutoApproveHires] = useState(true);
const [goalTitle, setGoalTitle] = useState("");
const [goalDescription, setGoalDescription] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [createdCompanyId, setCreatedCompanyId] = useState<string | null>(null);
const hasReadyConnector = connectors.some((c) => c.status === "ready" || c.status === "degraded");
const readyConnectors = connectors.filter((c) => c.status === "ready" || c.status === "degraded");
const bestConnector = readyConnectors[0] ?? null;
async function pickDirectory() {
setError(null);
@@ -31,7 +50,7 @@ export function CompanyOnboarding({
}
}
async function submit() {
const createCompany = useCallback(async () => {
if (loading) return;
if (!companyName.trim()) {
setError("Enter a company name.");
@@ -49,6 +68,7 @@ export function CompanyOnboarding({
name: companyName.trim(),
description: "",
status: "active",
autoApproveHires,
}),
);
if (workspacePath) {
@@ -64,32 +84,133 @@ export function CompanyOnboarding({
);
}
unwrap(await window.agentCompany.setCurrentCompany(companyId));
await onDone();
setCreatedCompanyId(companyId);
setStep("goal");
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to create the company.");
} finally {
setLoading(false);
}
}
}, [loading, companyName, hasReadyConnector, autoApproveHires, workspacePath]);
const createGoalAndLaunch = useCallback(async () => {
if (loading || !createdCompanyId) return;
setLoading(true);
setError(null);
try {
// Create goal
if (goalTitle.trim()) {
unwrap(
await window.agentCompany.saveGoal({
companyId: createdCompanyId,
title: goalTitle.trim(),
description: goalDescription.trim() || `Achieve: ${goalTitle.trim()}`,
status: "active",
parentId: null,
ownerAgentId: null,
}),
);
}
// Create CEO agent automatically
if (bestConnector) {
const ceoId = unwrap(
await window.agentCompany.saveAgent({
companyId: createdCompanyId,
name: "CEO",
role: "CEO",
title: "Chief Executive Officer",
reportsTo: null,
connectorId: bestConnector.id,
workspaceId: null,
model: null,
capabilities: "Executive leadership, strategic planning, team building, goal decomposition, delegation, and organizational management. Responsible for hiring the leadership team, setting strategy, and coordinating all company operations.",
budgetMonthlyUsd: 250,
status: "idle",
}),
);
// Enable heartbeat on CEO for autonomous operation
unwrap(await window.agentCompany.setHeartbeat({ agentId: ceoId, companyId: createdCompanyId, enabled: true, intervalSec: 120 }));
setStep("launch");
// Auto-trigger the CEO's first run after a brief delay for UI
setTimeout(async () => {
try {
await window.agentCompany.triggerHeartbeat({ agentId: ceoId, companyId: createdCompanyId, trigger: "assignment" });
} catch {
// Non-fatal — CEO will wake on next heartbeat cycle
}
}, 1500);
}
await onDone();
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to set up the company.");
setLoading(false);
}
}, [loading, createdCompanyId, goalTitle, goalDescription, bestConnector, onDone]);
const skipGoalAndFinish = useCallback(async () => {
if (loading) return;
setLoading(true);
setError(null);
try {
await onDone();
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to continue.");
} finally {
setLoading(false);
}
}, [loading, onDone]);
const stepIndex = step === "company" ? 0 : step === "goal" ? 1 : 2;
return (
<div className="grid min-h-[100dvh] grid-cols-1 bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_440px]">
<div className="grid min-h-[100dvh] grid-cols-1 bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_480px]">
{/* Left panel — branding */}
<div className="flex items-center px-8 py-12 md:px-16">
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, ease: [0.16, 1, 0.3, 1] }}
className="max-w-[420px]"
className="max-w-[440px]"
>
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">AgentCompany Desktop</div>
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
Welcome.
Build your<br />AI company.
</h1>
<p className="mt-4 text-[14px] leading-relaxed text-[color:var(--muted)]">
We detected the following coding agents on your system. You'll configure your organization structure, goals, and agents from the dashboard after setup.
<p className="mt-4 max-w-[380px] text-[14px] leading-relaxed text-[color:var(--muted)]">
Set up a fully autonomous AI-powered organization. Your agents will hire, delegate, execute, review, and coordinate — just like a real company.
</p>
<div className="mt-8">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Detected connectors</div>
{/* Step indicators */}
<div className="mt-10 flex items-center gap-3">
{[
{ label: "Create Company", icon: Buildings },
{ label: "Set Mission", icon: Target },
{ label: "Launch", icon: Rocket },
].map((s, i) => (
<div key={s.label} className="flex items-center gap-2">
{i > 0 ? <div className={`h-px w-6 ${i <= 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 ${
i < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: i === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}>
{i < stepIndex ? <CheckCircle size={14} weight="fill" /> : <s.icon size={14} />}
{s.label}
</div>
</div>
))}
</div>
{/* Detected connectors */}
<div className="mt-10">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Detected agents</div>
<div className="space-y-2.5">
{connectors.map((c) => {
const isOk = c.status === "ready" || c.status === "degraded";
@@ -122,56 +243,188 @@ export function CompanyOnboarding({
</motion.div>
</div>
{/* Right panel — form steps */}
<div className="flex items-center border-l border-[color:var(--line)] bg-[color:var(--bg-subtle)] px-6 py-10 md:px-10">
<motion.div
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.08, duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Getting started</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">Create your company</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input label="Company name" value={companyName} onChange={setCompanyName} placeholder="e.g. Acme Engineering" />
<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>
</label>
<div className="flex gap-3">
<Input
id="onboarding-workspace-path"
ariaLabel="Workspace folder"
value={workspacePath}
onChange={setWorkspacePath}
placeholder="/Users/you/project"
<AnimatePresence mode="wait">
{step === "company" ? (
<motion.div
key="company"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
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</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">Create your company</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input label="Company name" value={companyName} onChange={setCompanyName} placeholder="e.g. Acme AI Labs" />
<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>
</label>
<div className="flex gap-3">
<Input
id="onboarding-workspace-path"
ariaLabel="Workspace folder"
value={workspacePath}
onChange={setWorkspacePath}
placeholder="/Users/you/project"
/>
<ActionButton label="Choose" onClick={() => void pickDirectory()} icon={FolderOpen} />
</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
The directory where your agents will work. You can add more later.
</div>
</div>
{/* Auto-approve toggle */}
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={autoApproveHires}
onChange={(e) => setAutoApproveHires(e.target.checked)}
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)]">Full Autonomy Mode</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
Auto-approve agent hires and org strategies. Agents will build the team without waiting for Board approval.
</div>
</div>
</label>
</div>
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]" role="alert">
{error}
</div>
) : null}
<div className="mt-8 flex items-center justify-end gap-4">
<ActionButton
label={loading ? "Creating..." : "Continue"}
onClick={() => void createCompany()}
icon={Buildings}
tone="accent"
loading={loading}
disabled={!hasReadyConnector || !companyName.trim()}
/>
<ActionButton label="Choose" onClick={() => void pickDirectory()} icon={FolderOpen} />
</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
You can add more workspaces later from the dashboard.
</motion.div>
) : step === "goal" ? (
<motion.div
key="goal"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
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</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">Set your company's mission</div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">
What should your AI company accomplish? The CEO agent will use this to build a team and start working.
</div>
</div>
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]" role="alert">
{error}
</div>
) : null}
<div className="mt-8 flex items-center justify-end gap-4">
<ActionButton
label={loading ? "Creating..." : "Continue to dashboard"}
onClick={() => void submit()}
icon={Buildings}
tone="accent"
loading={loading}
disabled={!hasReadyConnector || !companyName.trim()}
/>
</div>
</motion.div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input
label="Goal"
value={goalTitle}
onChange={setGoalTitle}
placeholder="e.g. Build a SaaS landing page with marketing copy"
/>
<TextArea
label="Description (acceptance criteria)"
value={goalDescription}
onChange={setGoalDescription}
placeholder="Describe what 'done' looks like. The more specific, the better."
rows={4}
/>
</div>
{/* Preview: what will happen */}
<div className="mt-5 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">What happens next</div>
<div className="space-y-2 text-[12px] text-[color:var(--muted)]">
<div className="flex items-start gap-2">
<Robot size={14} className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
<span>A <strong className="text-[color:var(--text)]">CEO agent</strong> will be created using {bestConnector?.label ?? "your best connector"}</span>
</div>
<div className="flex items-start gap-2">
<Target size={14} className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
<span>The CEO will read your goal and <strong className="text-[color:var(--text)]">design an organization structure</strong></span>
</div>
<div className="flex items-start gap-2">
<Lightning size={14} className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
<span>Agents will be hired, tasks delegated, and work will begin <strong className="text-[color:var(--text)]">autonomously</strong></span>
</div>
</div>
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]" role="alert">
{error}
</div>
) : null}
<div className="mt-8 flex items-center justify-between">
<button
type="button"
onClick={() => void skipGoalAndFinish()}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
Skip for now
</button>
<ActionButton
label={loading ? "Launching..." : "Launch your company"}
onClick={() => void createGoalAndLaunch()}
icon={Rocket}
tone="accent"
loading={loading}
disabled={!goalTitle.trim()}
/>
</div>
</motion.div>
) : (
<motion.div
key="launch"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className="w-full text-center"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, duration: 0.6, type: "spring", bounce: 0.4 }}
className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-[color:var(--success-soft)]"
>
<Rocket size={36} weight="fill" className="text-[color:var(--success)]" />
</motion.div>
<h2 className="text-[24px] font-bold tracking-[-0.02em] text-[color:var(--text)]">
{companyName} is live!
</h2>
<p className="mx-auto mt-3 max-w-[340px] text-[14px] leading-relaxed text-[color:var(--muted)]">
Your CEO agent is starting up. Watch the dashboard as your company comes to life.
</p>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5 }}
className="mt-8"
>
<div className="flex items-center justify-center gap-2 text-[12px] text-[color:var(--muted)]">
<span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--success)]" />
CEO agent initializing...
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
+200 -18
View File
@@ -473,6 +473,53 @@ export function TaskInspector({
</>
)}
{/* Subtask tree */}
{(() => {
const subtasks = tasks.filter((t) => t.parentId === task.id);
if (subtasks.length === 0) return null;
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<ListChecks size={14} className="text-[color:var(--muted)]" />
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Subtasks ({subtasks.length})</span>
<span className="text-[10px] text-[color:var(--success)]">
{subtasks.filter((t) => t.status === "done").length}/{subtasks.length} done
</span>
</div>
{/* Progress bar */}
<div className="h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
<div
className="h-full rounded-full bg-[color:var(--success)] transition-all duration-300"
style={{ width: `${subtasks.length > 0 ? (subtasks.filter((t) => t.status === "done").length / subtasks.length) * 100 : 0}%` }}
/>
</div>
<div className="space-y-1">
{subtasks.map((sub) => {
const subAssignee = agents.find((a) => a.id === sub.assigneeAgentId);
return (
<div key={sub.id} className="flex items-center gap-2 rounded-[6px] px-3 py-2 text-[12px] bg-[color:var(--panel-soft)]">
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${
sub.status === "done" ? "bg-[color:var(--success)]" :
sub.status === "in_progress" ? "bg-[color:var(--accent)]" :
sub.status === "blocked" ? "bg-[color:var(--danger)]" :
sub.status === "in_review" ? "bg-[color:var(--warn)]" :
"bg-[color:var(--muted)]"
}`} />
<span className={`flex-1 truncate ${sub.status === "done" ? "line-through text-[color:var(--muted)]" : "text-[color:var(--text)]"}`}>
{sub.title}
</span>
{subAssignee ? (
<span className="shrink-0 text-[10px] text-[color:var(--muted)]">{subAssignee.name}</span>
) : null}
<StatusPill status={sub.status} />
</div>
);
})}
</div>
</div>
);
})()}
{/* Communication */}
<TaskComments taskId={task.id} companyId={task.companyId} agents={agents} />
@@ -481,17 +528,42 @@ export function TaskInspector({
<div className="space-y-2">
<div className="text-[11px] font-medium uppercase tracking-[0.08em] text-[color:var(--muted)]">Run history</div>
<div className="space-y-2">
{runs.slice(0, 5).map((run) => (
<div key={run.id} className="flex items-center justify-between rounded-md bg-[color:var(--panel-soft)] px-3 py-2">
<div>
<div className="text-[13px] text-[color:var(--text)]">{run.summary || "Run"}</div>
<div className="text-[11px] text-[color:var(--muted)]">{formatTime(run.startedAt)}{run.finishedAt ? ` · ${formatTime(run.finishedAt)}` : ""}</div>
{run.errorMessage ? <div className="mt-1 text-[11px] text-[color:var(--danger)]">{run.errorMessage}</div> : null}
{runs.slice(0, 8).map((run) => {
const runAgent = agents.find((a) => a.id === run.agentId);
const isActive = run.status === "running" || run.status === "queued";
return (
<div key={run.id} className={`rounded-[8px] border px-3 py-2.5 ${
isActive ? "border-[color:var(--success)] bg-[color:var(--success-soft)]" :
run.status === "failed" || run.status === "timed_out" ? "border-[color:var(--danger)] border-opacity-30 bg-[color:var(--danger-soft)]" :
"border-[color:var(--line)] bg-[color:var(--panel-soft)]"
}`}>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--success)]" /> : null}
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">
{run.summary || (isActive ? "Running..." : "Completed run")}
</span>
</div>
<div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-[color:var(--muted)]">
{runAgent ? <span>{runAgent.name}</span> : null}
<span>{run.connectorId.replace("_local", "")}</span>
{run.model ? <span className="mono">{run.model}</span> : null}
<span>{formatTime(run.startedAt)}</span>
{run.costUsd != null && run.costUsd > 0 ? <span className="mono">{formatMoney(run.costUsd)}</span> : null}
</div>
</div>
<StatusPill status={run.status} />
</div>
{run.errorMessage ? (
<div className="mt-1.5 rounded bg-[color:var(--danger-soft)] px-2 py-1 text-[11px] text-[color:var(--danger)]">
{run.errorMessage}
</div>
) : null}
</div>
<StatusPill status={run.status} />
</div>
))}
{runs.length > 5 && <div className="text-[11px] text-[color:var(--muted)] text-center">+{runs.length - 5} more runs</div>}
);
})}
{runs.length > 8 && <div className="text-[11px] text-[color:var(--muted)] text-center">+{runs.length - 8} more runs</div>}
</div>
</div>
)}
@@ -797,15 +869,87 @@ export function ApprovalInspector({
{actionError && !confirmDelete ? <InlineNotice message={actionError} /> : null}
<div className="border-y border-[color:var(--line)] py-4 text-[13px] leading-relaxed text-[color:var(--muted)]">
<div className="mb-3 text-[color:var(--text)]">{approval.payloadSummary}</div>
<div className="mb-3">{approval.impactSummary}</div>
<div className="grid gap-2 md:grid-cols-2">
<div>Requested by: <span className="text-[color:var(--text)]">{requestingAgent?.name ?? "Unknown agent"}</span></div>
<div>Related task: <span className="text-[color:var(--text)]">{relatedTask?.title ?? "None"}</span></div>
<div>Type: <span className="text-[color:var(--text)]">{approval.type.replaceAll("_", " ")}</span></div>
<div>State: <StatusPill status={approval.state} /></div>
{/* Approval context */}
<div className="border-y border-[color:var(--line)] py-4 space-y-4">
{/* Type badge + state */}
<div className="flex items-center gap-3">
<span className={`rounded-full px-3 py-1 text-[11px] font-bold uppercase tracking-[0.08em] ${
approval.type === "hire_agent" ? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" :
approval.type === "approve_ceo_strategy" ? "bg-[color:var(--warn-soft)] text-[color:var(--warn)]" :
approval.type === "secret_access" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" :
"bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}>
{approval.type.replaceAll("_", " ")}
</span>
<StatusPill status={approval.state} />
</div>
{/* Summary */}
<div className="text-[14px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
{approval.impactSummary ? (
<div className="text-[13px] text-[color:var(--muted)]">{approval.impactSummary}</div>
) : null}
{/* Metadata grid */}
<div className="grid gap-2 text-[13px] md:grid-cols-2">
<div className="flex items-center gap-2 text-[color:var(--muted)]">
<Robot size={14} />
Requested by: <span className="font-medium text-[color:var(--text)]">{requestingAgent?.name ?? "Unknown"}</span>
{requestingAgent?.role ? <span className="text-[10px] text-[color:var(--muted)]">({requestingAgent.role})</span> : null}
</div>
{relatedTask ? (
<div className="flex items-center gap-2 text-[color:var(--muted)]">
<Briefcase size={14} />
Task: <span className="font-medium text-[color:var(--text)]">{relatedTask.title}</span>
</div>
) : null}
<div className="text-[color:var(--muted)]">
Created: <span className="mono text-[color:var(--text)]">{formatTime(approval.createdAt)}</span>
</div>
{approval.updatedAt !== approval.createdAt ? (
<div className="text-[color:var(--muted)]">
Updated: <span className="mono text-[color:var(--text)]">{formatTime(approval.updatedAt)}</span>
</div>
) : null}
</div>
{/* Hire agent payload details */}
{approval.type === "hire_agent" ? (() => {
// Find the agent that was proposed for hiring (look for pending_approval agents)
const proposedAgent = agents.find(
(a) => a.status === "pending_approval" && a.reportsTo === approval.requestedByAgentId
) ?? agents.find(
(a) => a.name.toLowerCase().includes("pending") || a.status === "pending_approval"
);
if (!proposedAgent) return null;
return (
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-30 bg-[color:var(--accent-soft)] px-4 py-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--accent)]">Proposed Hire</div>
<div className="grid gap-1.5 text-[12px]">
<div><span className="text-[color:var(--muted)]">Name:</span> <span className="font-medium text-[color:var(--text)]">{proposedAgent.name}</span></div>
<div><span className="text-[color:var(--muted)]">Role:</span> <span className="text-[color:var(--text)]">{proposedAgent.role}</span></div>
{proposedAgent.title ? <div><span className="text-[color:var(--muted)]">Title:</span> <span className="text-[color:var(--text)]">{proposedAgent.title}</span></div> : null}
{proposedAgent.capabilities ? <div><span className="text-[color:var(--muted)]">Capabilities:</span> <span className="text-[color:var(--text)]">{proposedAgent.capabilities}</span></div> : null}
<div><span className="text-[color:var(--muted)]">Connector:</span> <span className="mono text-[color:var(--text)]">{proposedAgent.connectorId}</span></div>
<div><span className="text-[color:var(--muted)]">Budget:</span> <span className="mono text-[color:var(--text)]">{formatMoney(proposedAgent.budgetMonthlyUsd)}/mo</span></div>
{proposedAgent.reportsTo ? (() => {
const manager = agents.find((a) => a.id === proposedAgent.reportsTo);
return manager ? <div><span className="text-[color:var(--muted)]">Reports to:</span> <span className="text-[color:var(--text)]">{manager.name}</span></div> : null;
})() : null}
</div>
</div>
);
})() : null}
{/* CEO strategy payload details */}
{approval.type === "approve_ceo_strategy" ? (
<div className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--warn)]">Organization Strategy</div>
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">
{approval.payloadSummary}
</div>
</div>
) : null}
</div>
{/* Decision note input */}
@@ -1330,6 +1474,44 @@ export function SettingsPane({
</div>
</div>
{/* Company autonomy */}
{(() => {
const company = snapshot.companies.find((c) => c.id === companyId);
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="rounded-[8px] border border-[color:var(--line)] px-4 py-3">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={company.autoApproveHires}
onChange={async (e) => {
try {
unwrap(await window.agentCompany.saveCompany({
id: company.id,
name: company.name,
description: company.description,
status: company.status,
autoApproveHires: e.target.checked,
}));
await onSaved();
} catch { /* ignore */ }
}}
className="h-4 w-4 rounded accent-[color:var(--accent)]"
/>
<div>
<div className="text-[13px] font-medium text-[color:var(--text)]">Full Autonomy Mode</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
Auto-approve agent hires and CEO strategies. Agents will build the team and start operations without Board approval.
</div>
</div>
</label>
</div>
</div>
);
})()}
{/* Actions */}
<div>
<div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Actions</div>
+1
View File
@@ -52,6 +52,7 @@ export const companyInputSchema = z.object({
name: z.string().min(2).max(80),
description: z.string().max(500).default(""),
status: z.enum(["active", "paused", "archived"]).default("active"),
autoApproveHires: z.boolean().default(false),
});
export const connectorInputSchema = z.object({
+1
View File
@@ -63,6 +63,7 @@ export interface CompanyRecord {
name: string;
description: string;
status: CompanyStatus;
autoApproveHires: boolean;
createdAt: string;
updatedAt: string;
}