From 41eafc7c44fd64e34179e714274b87d8c0ed6569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 16 Mar 2026 00:22:21 -0400 Subject: [PATCH] update --- src/main/api-server.ts | 26 ++ src/main/database.ts | 21 +- src/renderer/components/CompanyOnboarding.tsx | 373 +++++++++++++++--- src/renderer/components/execution.tsx | 218 +++++++++- src/shared/contracts.ts | 1 + src/shared/types.ts | 1 + 6 files changed, 555 insertions(+), 85 deletions(-) diff --git a/src/main/api-server.ts b/src/main/api-server.ts index 3932db7d..9ea3cc71 100644 --- a/src/main/api-server.ts +++ b/src/main/api-server.ts @@ -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 }); }); diff --git a/src/main/database.ts b/src/main/database.ts index 90a5304e..310f0032 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -423,8 +423,8 @@ export class AppDatabase { secretBackend: this.vault.getBackendLabel(), }, companies: this.readAll( - "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( "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 ( diff --git a/src/renderer/components/CompanyOnboarding.tsx b/src/renderer/components/CompanyOnboarding.tsx index bac63af6..afcb00f7 100644 --- a/src/renderer/components/CompanyOnboarding.tsx +++ b/src/renderer/components/CompanyOnboarding.tsx @@ -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; }) { + const [step, setStep] = useState("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(null); + const [createdCompanyId, setCreatedCompanyId] = useState(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 ( -
+
+ {/* Left panel — branding */}
AgentCompany Desktop

- Welcome. + Build your
AI company.

-

- We detected the following coding agents on your system. You'll configure your organization structure, goals, and agents from the dashboard after setup. +

+ Set up a fully autonomous AI-powered organization. Your agents will hire, delegate, execute, review, and coordinate — just like a real company.

-
-
Detected connectors
+ + {/* Step indicators */} +
+ {[ + { label: "Create Company", icon: Buildings }, + { label: "Set Mission", icon: Target }, + { label: "Launch", icon: Rocket }, + ].map((s, i) => ( +
+ {i > 0 ?
: null} +
+ {i < stepIndex ? : } + {s.label} +
+
+ ))} +
+ + {/* Detected connectors */} +
+
Detected agents
{connectors.map((c) => { const isOk = c.status === "ready" || c.status === "degraded"; @@ -122,56 +243,188 @@ export function CompanyOnboarding({
+ {/* Right panel — form steps */}
- -
Getting started
-
Create your company
-
-
- -
- -
- + {step === "company" ? ( + +
Step 1 of 3
+
Create your company
+
+
+ +
+ +
+ + void pickDirectory()} icon={FolderOpen} /> +
+
+ The directory where your agents will work. You can add more later. +
+
+ + {/* Auto-approve toggle */} +
+ +
+
+ {error ? ( +
+ {error} +
+ ) : null} +
+ void createCompany()} + icon={Buildings} + tone="accent" + loading={loading} + disabled={!hasReadyConnector || !companyName.trim()} /> - void pickDirectory()} icon={FolderOpen} />
-
- You can add more workspaces later from the dashboard. + + ) : step === "goal" ? ( + +
Step 2 of 3
+
Set your company's mission
+
+ What should your AI company accomplish? The CEO agent will use this to build a team and start working.
-
-
- {error ? ( -
- {error} -
- ) : null} -
- void submit()} - icon={Buildings} - tone="accent" - loading={loading} - disabled={!hasReadyConnector || !companyName.trim()} - /> -
-
+
+
+ +