diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index ef6f0dc1..a400db48 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -67,6 +67,8 @@ export function registerIpcHandlers(deps: IpcDeps): void { }); ipcMain.handle(IPC_CHANNELS.createOrganization, (_event, input) => mutate(() => organization.createOrganization(input))); + ipcMain.handle(IPC_CHANNELS.setActiveOrganization, (_event, input) => mutate(() => organization.setActiveOrganization(input.orgId))); + ipcMain.handle(IPC_CHANNELS.deleteOrganization, (_event, input) => mutate(() => organization.deleteOrganization(input.orgId))); ipcMain.handle(IPC_CHANNELS.createAgent, (_event, input) => mutate(() => organization.createAgent(input))); ipcMain.handle(IPC_CHANNELS.createTask, (_event, input) => mutate(() => organization.createTask(input))); ipcMain.handle(IPC_CHANNELS.updateTaskStatus, (_event, input) => mutate(() => organization.updateTaskStatus(input.orgId, input.taskId, input.status))); diff --git a/AgentCompany-New/src/main/database/migrations/006-app-state.ts b/AgentCompany-New/src/main/database/migrations/006-app-state.ts new file mode 100644 index 00000000..53820389 --- /dev/null +++ b/AgentCompany-New/src/main/database/migrations/006-app-state.ts @@ -0,0 +1,14 @@ +export const appStateSchema = ` + create table if not exists app_state ( + id integer primary key check (id = 1), + active_org_id text references organizations(id) on delete set null, + updated_at text not null + ); + + insert into app_state(id, active_org_id, updated_at) + select 1, (select id from organizations order by created_at asc limit 1), datetime('now') + where not exists (select 1 from app_state where id = 1); + + insert into schema_migrations(version, name, applied_at) + values (6, 'app_state', datetime('now')); +`; diff --git a/AgentCompany-New/src/main/database/schema.ts b/AgentCompany-New/src/main/database/schema.ts index e081b591..42da029e 100644 --- a/AgentCompany-New/src/main/database/schema.ts +++ b/AgentCompany-New/src/main/database/schema.ts @@ -4,6 +4,7 @@ import { collaborationProviderPolicySchema } from "./migrations/002-collaboratio import { onboardingExecutionSchema } from "./migrations/003-onboarding"; import { filesPinnedAndScheduledPosts } from "./migrations/004-files-posts"; import { chatReadState } from "./migrations/005-chat-read-state"; +import { appStateSchema } from "./migrations/006-app-state"; const MIGRATIONS: Array<{ version: number; sql: string }> = [ { version: 1, sql: initialEnterpriseSchema }, @@ -11,6 +12,7 @@ const MIGRATIONS: Array<{ version: number; sql: string }> = [ { version: 3, sql: onboardingExecutionSchema }, { version: 4, sql: filesPinnedAndScheduledPosts }, { version: 5, sql: chatReadState }, + { version: 6, sql: appStateSchema }, ]; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; diff --git a/AgentCompany-New/src/main/services/organization-service.ts b/AgentCompany-New/src/main/services/organization-service.ts index ee131783..8b395001 100644 --- a/AgentCompany-New/src/main/services/organization-service.ts +++ b/AgentCompany-New/src/main/services/organization-service.ts @@ -74,6 +74,11 @@ export class OrganizationService { } this.policy.seedOrganizationPolicies(orgId, now); + this.db.prepare(` + insert into app_state(id, active_org_id, updated_at) + values (1, ?, ?) + on conflict(id) do update set active_org_id = excluded.active_org_id, updated_at = excluded.updated_at + `).run(orgId, now); this.audit.record({ orgId, entityType: "organization", @@ -86,6 +91,41 @@ export class OrganizationService { return orgId; } + setActiveOrganization(orgId: string): void { + const row = this.db.prepare("select id from organizations where id = ?").get(orgId) as { id: string } | undefined; + if (!row) throw new Error("Organization was not found."); + const now = new Date().toISOString(); + this.db.prepare(` + insert into app_state(id, active_org_id, updated_at) + values (1, ?, ?) + on conflict(id) do update set active_org_id = excluded.active_org_id, updated_at = excluded.updated_at + `).run(orgId, now); + this.audit.record({ + orgId, + entityType: "app_state", + entityId: "active_org", + actorType: "employee", + actorId: null, + action: "switched", + payload: { orgId }, + }); + } + + deleteOrganization(orgId: string): void { + const row = this.db.prepare("select id from organizations where id = ?") + .get(orgId) as { id: string } | undefined; + if (!row) throw new Error("Organization was not found."); + this.db.prepare("delete from organizations where id = ?").run(orgId); + const fallback = this.db.prepare("select id from organizations order by created_at asc limit 1") + .get() as { id: string } | undefined; + const now = new Date().toISOString(); + this.db.prepare(` + insert into app_state(id, active_org_id, updated_at) + values (1, ?, ?) + on conflict(id) do update set active_org_id = excluded.active_org_id, updated_at = excluded.updated_at + `).run(fallback?.id ?? null, now); + } + createAgent(input: CreateAgentInput): string { const id = createId("agt"); const now = new Date().toISOString(); diff --git a/AgentCompany-New/src/main/services/snapshot-service.ts b/AgentCompany-New/src/main/services/snapshot-service.ts index 16048d74..58951d92 100644 --- a/AgentCompany-New/src/main/services/snapshot-service.ts +++ b/AgentCompany-New/src/main/services/snapshot-service.ts @@ -34,7 +34,7 @@ export class SnapshotService { autoApproveHires: toBool(org.autoApproveHires), reviewDeliverables: toBool(org.reviewDeliverables), })); - const activeOrgId = organizations[0]?.id ?? null; + const activeOrgId = this.resolveActiveOrgId(organizations.map((org) => org.id)); const agents = list(this.db, "agents", "created_at asc") .map((agent) => ({ ...agent, heartbeatEnabled: toBool(agent.heartbeatEnabled) })); const tasks = list(this.db, "tasks", "created_at asc") @@ -82,4 +82,12 @@ export class SnapshotService { auditEvents: list(this.db, "audit_events", "created_at desc").slice(0, 200), }; } + + private resolveActiveOrgId(known: string[]): string | null { + const row = this.db.prepare("select active_org_id from app_state where id = 1") + .get() as { active_org_id: string | null } | undefined; + const stored = row?.active_org_id ?? null; + if (stored && known.includes(stored)) return stored; + return known[0] ?? null; + } } diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index bd1c260c..80615297 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -10,6 +10,8 @@ const api: DesktopApi = { loadSnapshot: () => invoke(IPC_CHANNELS.loadSnapshot), bootstrapOnboarding: (input) => invoke(IPC_CHANNELS.bootstrapOnboarding, input), createOrganization: (input) => invoke(IPC_CHANNELS.createOrganization, input), + setActiveOrganization: (input) => invoke(IPC_CHANNELS.setActiveOrganization, input), + deleteOrganization: (input) => invoke(IPC_CHANNELS.deleteOrganization, input), createAgent: (input) => invoke(IPC_CHANNELS.createAgent, input), createTask: (input) => invoke(IPC_CHANNELS.createTask, input), updateTaskStatus: (input) => invoke(IPC_CHANNELS.updateTaskStatus, input), diff --git a/AgentCompany-New/src/renderer/src/app/App.tsx b/AgentCompany-New/src/renderer/src/app/App.tsx index 77117009..2cf6da73 100644 --- a/AgentCompany-New/src/renderer/src/app/App.tsx +++ b/AgentCompany-New/src/renderer/src/app/App.tsx @@ -14,12 +14,20 @@ import { SettingsPage } from "../pages/SettingsPage"; export function App() { const { snapshot, actions } = useSnapshot(); const [route, setRoute] = useState("home"); + const [creatingOrg, setCreatingOrg] = useState(false); if (!snapshot) { return
; } - if (snapshot.organizations.length === 0) { - return ; + if (snapshot.organizations.length === 0 || creatingOrg) { + return ( + setCreatingOrg(false) : null} + onCompleted={() => setCreatingOrg(false)} + /> + ); } const pageProps = { snapshot, actions }; @@ -35,7 +43,14 @@ export function App() { }[route]; return ( - + void actions.run(() => window.agentCompany.setActiveOrganization({ orgId }))} + onDeleteOrg={(orgId) => void actions.run(() => window.agentCompany.deleteOrganization({ orgId }))} + onNewOrg={() => setCreatingOrg(true)} + > {page} ); diff --git a/AgentCompany-New/src/renderer/src/app/Onboarding.tsx b/AgentCompany-New/src/renderer/src/app/Onboarding.tsx index 99744222..eb289213 100644 --- a/AgentCompany-New/src/renderer/src/app/Onboarding.tsx +++ b/AgentCompany-New/src/renderer/src/app/Onboarding.tsx @@ -30,7 +30,17 @@ const STEPS: Array<{ id: Step; label: string }> = [ { id: "mission", label: "Goal" }, ]; -export function Onboarding({ snapshot, actions }: { snapshot: AppSnapshot; actions: SnapshotActions }) { +export function Onboarding({ + snapshot, + actions, + onCancel, + onCompleted, +}: { + snapshot: AppSnapshot; + actions: SnapshotActions; + onCancel: (() => void) | null; + onCompleted: () => void; +}) { const apiProviders = useMemo( () => snapshot.providerCatalog.filter((entry) => entry.authType === PROVIDER_AUTH_TYPE.API_KEY), [snapshot.providerCatalog], @@ -102,6 +112,7 @@ export function Onboarding({ snapshot, actions }: { snapshot: AppSnapshot; actio return; } await actions.reload(); + onCompleted(); }; return ( @@ -111,6 +122,11 @@ export function Onboarding({ snapshot, actions }: { snapshot: AppSnapshot; actio
A
AgentCompany
+ {onCancel && ( + + )}

Start your AI company.

diff --git a/AgentCompany-New/src/renderer/src/shared/OrgSwitcher.tsx b/AgentCompany-New/src/renderer/src/shared/OrgSwitcher.tsx new file mode 100644 index 00000000..0d81ab6b --- /dev/null +++ b/AgentCompany-New/src/renderer/src/shared/OrgSwitcher.tsx @@ -0,0 +1,155 @@ +import { useEffect, useRef, useState } from "react"; +import type { AppSnapshot, OrganizationRecord } from "../../../shared/domain"; +import { Icon } from "./Icon"; + +interface OrgSwitcherProps { + snapshot: AppSnapshot; + activeOrgId: string; + onSelect: (orgId: string) => void; + onNew: () => void; + onDelete: (orgId: string) => void; +} + +export function OrgSwitcher({ snapshot, activeOrgId, onSelect, onNew, onDelete }: OrgSwitcherProps) { + const [open, setOpen] = useState(false); + const [confirmingDelete, setConfirmingDelete] = useState(null); + const containerRef = useRef(null); + const active = snapshot.organizations.find((org) => org.id === activeOrgId); + if (!active) throw new Error("Active organization missing from snapshot."); + + useEffect(() => { + if (!open) return; + function onDocClick(event: MouseEvent) { + if (!containerRef.current?.contains(event.target as Node)) { + setOpen(false); + setConfirmingDelete(null); + } + } + document.addEventListener("mousedown", onDocClick); + return () => document.removeEventListener("mousedown", onDocClick); + }, [open]); + + const initial = active.name.trim().charAt(0).toUpperCase() || "A"; + const orgs = snapshot.organizations; + + const handleSelect = (orgId: string) => { + setOpen(false); + setConfirmingDelete(null); + if (orgId !== activeOrgId) onSelect(orgId); + }; + + const handleDelete = (orgId: string) => { + setOpen(false); + setConfirmingDelete(null); + onDelete(orgId); + }; + + return ( +

+ + {open && ( +
+
Organizations
+ {orgs.map((org) => ( + handleSelect(org.id)} + confirming={confirmingDelete === org.id} + onAskDelete={() => setConfirmingDelete(org.id)} + onCancelDelete={() => setConfirmingDelete(null)} + onConfirmDelete={() => handleDelete(org.id)} + canDelete={orgs.length > 1} + /> + ))} +
+ +
+ )} +
+ ); +} + +function OrgRow({ + org, + active, + onSelect, + confirming, + onAskDelete, + onCancelDelete, + onConfirmDelete, + canDelete, +}: { + org: OrganizationRecord; + active: boolean; + onSelect: () => void; + confirming: boolean; + onAskDelete: () => void; + onCancelDelete: () => void; + onConfirmDelete: () => void; + canDelete: boolean; +}) { + const initial = org.name.trim().charAt(0).toUpperCase() || "A"; + if (confirming) { + return ( +
+ {initial} +
+
Delete {org.name}?
+
All agents, work, chats, files removed.
+
+ + +
+ ); + } + return ( + + ); +} diff --git a/AgentCompany-New/src/renderer/src/shared/Shell.tsx b/AgentCompany-New/src/renderer/src/shared/Shell.tsx index 4d76d9b6..9e5cb25b 100644 --- a/AgentCompany-New/src/renderer/src/shared/Shell.tsx +++ b/AgentCompany-New/src/renderer/src/shared/Shell.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from "react"; import type { AppSnapshot } from "../../../shared/domain"; import { Avatar, Icon, IconButton } from "./ui"; import type { IconName } from "./Icon"; +import { OrgSwitcher } from "./OrgSwitcher"; export type RouteId = | "home" @@ -34,12 +35,18 @@ export function Shell({ route, setRoute, actions, + onSwitchOrg, + onDeleteOrg, + onNewOrg, children, }: { snapshot: AppSnapshot; route: RouteId; setRoute: (route: RouteId) => void; actions?: ReactNode; + onSwitchOrg: (orgId: string) => void; + onDeleteOrg: (orgId: string) => void; + onNewOrg: () => void; children: ReactNode; }) { const organization = snapshot.organizations.find((item) => item.id === snapshot.activeOrgId); @@ -48,21 +55,17 @@ export function Shell({ if (!employee) throw new Error("Active employee is missing."); const badges = computeBadges(snapshot, organization.id); const activeLabel = labelFor(route); - const orgInitial = organization.name.trim().charAt(0).toUpperCase() || "A"; return (