update
This commit is contained in:
1 parent
d067589276
commit
41eafc7c44
6 files changed
+555
-85
No files matched your search
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface CompanyRecord {
|
||||
name: string;
|
||||
description: string;
|
||||
status: CompanyStatus;
|
||||
autoApproveHires: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user