feat(01-03): decompose organization.tsx and dashboard.tsx into focused sub-components

- Split organization.tsx (1395 lines) into 12 files: AgentRoster, AgentCreator, AgentEditor, GoalList, GoalCreator, GoalEditor, ProjectList, ProjectCreator, ProjectEditor, OrgChart, helpers, orgTreeLayout
- Split dashboard.tsx (1053 lines) into 11 files: Dashboard, HealthCard, MetricsGrid, ActivityFeed, QuickActions, DepartmentOverview, TaskPipeline, SprintProgress, GoalProgress, KanbanBoard, NeedsAttention
- All sub-component files under 300 lines (max: 239 lines)
- Original files replaced with backward-compatible re-exports
- TypeScript compilation passes
This commit is contained in:
ZacharyZhang-NY committed 2026-03-22 01:54:20 -04:00
1 parent ccfdbe73a3
commit 16d10c0814
25 files changed
+2438 -2448

No files matched your search

@@ -0,0 +1,179 @@
import { useEffect, useMemo, useState } from "react";
import { Robot } from "@phosphor-icons/react";
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
import { isConnectorExecutionReady } from "@shared/connector-policy";
import { unwrap } from "../../lib/desktop";
import { ActionButton, Input, Select, TextArea } from "../ui";
export function AgentCreator({
companyId,
connectors,
workspaces,
agents,
onSaved,
}: {
companyId: string;
connectors: ConnectorRecord[];
workspaces: WorkspaceRecord[];
agents: AgentRecord[];
onSaved: () => Promise<void>;
}) {
const executionReadyConnectors = useMemo(
() => connectors.filter((connector) => isConnectorExecutionReady(connector)),
[connectors],
);
const [name, setName] = useState("");
const [role, setRole] = useState("");
const [title, setTitle] = useState("");
const [connectorId, setConnectorId] = useState(executionReadyConnectors[0]?.id ?? "");
const [model, setModel] = useState("");
const [availableModels, setAvailableModels] = useState<Array<{ id: string; label: string }>>([]);
const [workspaceId, setWorkspaceId] = useState(workspaces[0]?.id ?? "");
const [budget, setBudget] = useState("120");
const [capabilities, setCapabilities] = useState("");
const [reportsTo, setReportsTo] = useState("");
const [thinkingEffort, setThinkingEffort] = useState("");
const [requestError, setRequestError] = useState<string | null>(null);
const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local";
const selectedConnector = executionReadyConnectors.find((connector) => connector.id === connectorId) ?? null;
useEffect(() => {
if (!connectorId) {
setAvailableModels([]);
setModel("");
return;
}
void (async () => {
const result = await window.agentCompany.listConnectorModels(connectorId);
if (result.ok) {
setAvailableModels(result.data);
setModel(result.data[0]?.id ?? "");
}
})();
if (connectorId !== "claude_local" && connectorId !== "codex_local") {
setThinkingEffort("");
}
}, [connectorId]);
useEffect(() => {
if (!connectorId && executionReadyConnectors[0]) {
setConnectorId(executionReadyConnectors[0].id);
return;
}
if (connectorId && !executionReadyConnectors.some((connector) => connector.id === connectorId)) {
setConnectorId(executionReadyConnectors[0]?.id ?? "");
}
}, [executionReadyConnectors, connectorId]);
return (
<div className="grid gap-4">
<Input label="Agent name" value={name} onChange={setName} placeholder="Agent name" />
<Input label="Role" value={role} onChange={setRole} placeholder="e.g. Engineer, PM, CEO" />
<Input label="Title" value={title} onChange={setTitle} placeholder="Title" />
<Select
label="Connector"
value={connectorId}
onChange={(value) => setConnectorId(value as ConnectorRecord["id"])}
options={executionReadyConnectors.map((connector) => ({ value: connector.id, label: connector.label }))}
/>
{executionReadyConnectors.length === 0 ? (
<p className="text-[12px] text-[color:var(--warn)]">
No execution-ready connector is available. Fix connector health before requesting hires.
</p>
) : null}
{availableModels.length > 0 ? (
<Select
label="Model"
value={model}
onChange={setModel}
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
/>
) : null}
{supportsEffort ? (
<Select
label="Thinking effort"
value={thinkingEffort}
onChange={setThinkingEffort}
options={[
{ value: "", label: "Default" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
]}
/>
) : null}
<Select
label="Workspace"
value={workspaceId}
onChange={setWorkspaceId}
options={[
{ value: "", label: "No workspace" },
...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name })),
]}
/>
<Select
label="Reports to"
value={reportsTo}
onChange={setReportsTo}
options={[
{ value: "", label: "No manager" },
...agents.map((a) => ({ value: a.id, label: a.name })),
]}
/>
<Input label="Monthly budget (USD)" value={budget} onChange={setBudget} placeholder="120" type="number" />
<TextArea
label="Capabilities"
value={capabilities}
onChange={setCapabilities}
placeholder="What this agent is expected to do and when it becomes relevant."
/>
<p className="text-[12px] text-[color:var(--muted)]">
New agents enter the hiring pipeline first. Approval activates the agent and keeps onboarding, budgets, and org history linked to the same record.
</p>
<ActionButton
label="Submit hire request"
tone="accent"
onClick={() =>
void (async () => {
try {
setRequestError(null);
if (!selectedConnector) {
setRequestError("Select an execution-ready connector before requesting a hire.");
return;
}
const meta: Record<string, string> = {};
if (thinkingEffort) meta.thinkingEffort = thinkingEffort;
unwrap(await window.agentCompany.requestHire({
companyId,
requestedByAgentId: null,
name,
role,
title,
department: null,
connectorId: connectorId as ConnectorRecord["id"],
workspaceId: workspaceId || null,
reportsTo: reportsTo || null,
model: model || null,
metadataJson: JSON.stringify(meta),
capabilities,
budgetMonthlyUsd: Number(budget || 0),
}));
setName("");
setRole("");
setTitle("");
setCapabilities("");
setReportsTo("");
setRequestError(null);
await onSaved();
} catch (error) {
setRequestError(error instanceof Error ? error.message : "Hire request failed.");
}
})()
}
icon={Robot}
disabled={!selectedConnector}
/>
{requestError ? <p className="text-[12px] text-[color:var(--danger)]">{requestError}</p> : null}
</div>
);
}
@@ -0,0 +1,229 @@
import { useEffect, useMemo, useState } from "react";
import { Pencil, Trash } from "@phosphor-icons/react";
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
import { unwrap } from "../../lib/desktop";
import { ActionButton, Input, Select, TextArea } from "../ui";
import { ConfirmDelete, HeartbeatSection } from "./helpers";
export function AgentEditor({
agent,
connectors,
workspaces,
agents,
companyId,
onSaved,
onDeleted,
}: {
agent: AgentRecord;
connectors: ConnectorRecord[];
workspaces: WorkspaceRecord[];
agents: AgentRecord[];
companyId: string;
onSaved: () => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const [name, setName] = useState(agent.name);
const [role, setRole] = useState(agent.role);
const [title, setTitle] = useState(agent.title);
const [status, setStatus] = useState<AgentRecord["status"]>(agent.status);
const [connectorId, setConnectorId] = useState<ConnectorRecord["id"]>(agent.connectorId);
const [model, setModel] = useState(agent.model ?? "");
const [availableModels, setAvailableModels] = useState<Array<{ id: string; label: string }>>([]);
const [workspaceId, setWorkspaceId] = useState(agent.workspaceId ?? "");
const [budget, setBudget] = useState(String(agent.budgetMonthlyUsd));
const [capabilities, setCapabilities] = useState(agent.capabilities);
const [reportsTo, setReportsTo] = useState(agent.reportsTo ?? "");
const [confirmingDelete, setConfirmingDelete] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const existingMeta = useMemo(() => { try { return JSON.parse(agent.metadataJson || "{}"); } catch { return {}; } }, [agent.metadataJson]);
const [thinkingEffort, setThinkingEffort] = useState<string>(existingMeta.thinkingEffort ?? "");
const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local";
const pendingApproval = agent.status === "pending_approval";
const executionReadyConnectors = useMemo(
() => connectors.filter((connector) => isConnectorExecutionReady(connector)),
[connectors],
);
const selectedConnectorRecord = connectors.find((connector) => connector.id === connectorId) ?? null;
const selectedConnectorIssue = selectedConnectorRecord ? getConnectorExecutionReadinessIssue(selectedConnectorRecord) : "Select a connector.";
const persistedConnectorRecord = connectors.find((connector) => connector.id === agent.connectorId) ?? null;
const persistedConnectorIssue = persistedConnectorRecord ? getConnectorExecutionReadinessIssue(persistedConnectorRecord) : "Current connector is unavailable.";
const connectorOptions = useMemo(() => {
const readyOptions = executionReadyConnectors.map((connector) => ({ value: connector.id, label: connector.label }));
if (!selectedConnectorRecord || isConnectorExecutionReady(selectedConnectorRecord)) {
return readyOptions;
}
return [
{ value: selectedConnectorRecord.id, label: `${selectedConnectorRecord.label} (current, blocked)` },
...readyOptions.filter((option) => option.value !== selectedConnectorRecord.id),
];
}, [executionReadyConnectors, selectedConnectorRecord]);
useEffect(() => {
void (async () => {
const result = await window.agentCompany.listConnectorModels(connectorId);
if (result.ok) {
setAvailableModels(result.data);
if (connectorId !== agent.connectorId) {
setModel(result.data[0]?.id ?? "");
}
}
})();
if (connectorId !== "claude_local" && connectorId !== "codex_local") {
setThinkingEffort("");
}
}, [connectorId, agent.connectorId]);
const managerCandidates = agents.filter((a) => a.id !== agent.id);
return (
<div className="grid gap-4">
<Input label="Agent name" value={name} onChange={setName} placeholder="Agent name" readOnly={pendingApproval} disabled={pendingApproval} />
<Input label="Role" value={role} onChange={setRole} placeholder="Role" readOnly={pendingApproval} disabled={pendingApproval} />
<Input label="Title" value={title} onChange={setTitle} placeholder="Title" readOnly={pendingApproval} disabled={pendingApproval} />
<Select
label="Status"
value={status}
onChange={(v) => setStatus(v as AgentRecord["status"])}
disabled={pendingApproval}
options={pendingApproval
? [{ value: "pending_approval", label: "Pending Approval" }]
: [
{ value: "idle", label: "Idle" },
{ value: "active", label: "Active" },
{ value: "running", label: "Running" },
{ value: "paused", label: "Paused" },
{ value: "error", label: "Error" },
{ value: "terminated", label: "Terminated" },
]}
/>
{pendingApproval ? (
<p className="text-[12px] text-[color:var(--muted)]">
Pending hires must be approved or rejected from Hiring or Approvals. Status changes are intentionally locked here.
</p>
) : null}
<Select
label="Connector"
value={connectorId}
onChange={(v) => setConnectorId(v as ConnectorRecord["id"])}
disabled={pendingApproval}
options={connectorOptions}
/>
{selectedConnectorIssue ? (
<p className="text-[12px] text-[color:var(--warn)]">
{selectedConnectorIssue}
</p>
) : null}
{availableModels.length > 0 ? (
<Select
label="Model"
value={model}
onChange={setModel}
disabled={pendingApproval}
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
/>
) : null}
{supportsEffort ? (
<Select
label="Thinking effort"
value={thinkingEffort}
onChange={setThinkingEffort}
disabled={pendingApproval}
options={[
{ value: "", label: "Default" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
]}
/>
) : null}
<Select
label="Workspace"
value={workspaceId}
onChange={setWorkspaceId}
disabled={pendingApproval}
options={[
{ value: "", label: "No workspace" },
...workspaces.map((w) => ({ value: w.id, label: w.name })),
]}
/>
<Select
label="Reports to"
value={reportsTo}
onChange={setReportsTo}
disabled={pendingApproval}
options={[
{ value: "", label: "No manager" },
...managerCandidates.map((a) => ({ value: a.id, label: a.name })),
]}
/>
<Input label="Monthly budget (USD)" value={budget} onChange={setBudget} placeholder="120" type="number" readOnly={pendingApproval} disabled={pendingApproval} />
<TextArea label="Capabilities" value={capabilities} onChange={setCapabilities} placeholder="Capabilities" readOnly={pendingApproval} disabled={pendingApproval} />
<HeartbeatSection agent={agent} companyId={companyId} pendingApproval={pendingApproval} persistedConnectorIssue={persistedConnectorIssue} onSaved={onSaved} />
<div className="flex items-center gap-3">
<ActionButton
label="Save changes"
tone="accent"
icon={Pencil}
disabled={pendingApproval || !!selectedConnectorIssue}
onClick={() =>
void (async () => {
setSaveError(null);
const meta = { ...existingMeta };
if (thinkingEffort) meta.thinkingEffort = thinkingEffort;
else delete meta.thinkingEffort;
try {
unwrap(await window.agentCompany.saveAgent({
id: agent.id,
companyId,
name,
role,
title,
status,
connectorId,
workspaceId: workspaceId || null,
reportsTo: reportsTo || null,
model: model || null,
metadataJson: JSON.stringify(meta),
budgetMonthlyUsd: Number(budget || 0),
capabilities,
}));
await onSaved();
} catch (error) {
setSaveError(error instanceof Error ? error.message : "Failed to save agent.");
}
})()
}
/>
{confirmingDelete ? (
<ConfirmDelete
entityLabel={agent.name}
onConfirm={() =>
void (async () => {
await window.agentCompany.deleteAgent({ id: agent.id, companyId });
await onDeleted();
})()
}
onCancel={() => setConfirmingDelete(false)}
/>
) : (
<ActionButton
label="Delete"
tone="danger"
icon={Trash}
disabled={pendingApproval}
onClick={() => setConfirmingDelete(true)}
/>
)}
</div>
{pendingApproval ? (
<p className="text-[12px] text-[color:var(--muted)]">
Pending hires are read-only here. Use Hiring or Approvals to approve or reject the request before editing the agent profile.
</p>
) : null}
{saveError ? <p className="text-[12px] text-[color:var(--danger)]">{saveError}</p> : null}
</div>
);
}
@@ -0,0 +1,64 @@
import { CheckCircle } from "@phosphor-icons/react";
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
import { EmptyState, ListFrame, ListRow, StatusPill } from "../ui";
import { useT } from "../../i18n";
import { BudgetBar } from "./helpers";
export function AgentRoster({
agents,
connectors,
workspaces,
selectedAgentId,
onSelect,
}: {
agents: AgentRecord[];
connectors: ConnectorRecord[];
workspaces: WorkspaceRecord[];
selectedAgentId: string | null;
onSelect: (agentId: string) => void;
}) {
const t = useT();
if (agents.length === 0) {
return <EmptyState title={t("org.noAgentsYet")} />;
}
return (
<ListFrame>
{agents.map((agent) => {
const connector = connectors.find((c) => c.id === agent.connectorId);
const workspace = agent.workspaceId
? workspaces.find((w) => w.id === agent.workspaceId)
: null;
const isSelected = agent.id === selectedAgentId;
return (
<ListRow
key={agent.id}
onClick={() => onSelect(agent.id)}
className={`cursor-pointer transition-colors duration-100 ${isSelected ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}`}
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
{isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
)}
<div>
<div className="text-[15px] text-[color:var(--text)]">{agent.name}</div>
<div className="text-[12px] text-[color:var(--muted)]">
{agent.role} · {agent.title || "No title"}
</div>
</div>
</div>
<StatusPill status={agent.status} />
</div>
<div className="flex items-center justify-between text-[12px] text-[color:var(--muted)]">
<span>Connector: {connector?.label ?? agent.connectorId}</span>
{workspace && <span>Workspace: {workspace.name}</span>}
</div>
<div className="mt-1.5">
<BudgetBar spent={agent.spentMonthlyUsd} budget={agent.budgetMonthlyUsd} />
</div>
</ListRow>
);
})}
</ListFrame>
);
}
@@ -0,0 +1,74 @@
import { useState } from "react";
import { Target } from "@phosphor-icons/react";
import type { AgentRecord, GoalRecord } from "@shared/types";
import { ActionButton, Input, Select, TextArea } from "../ui";
export function GoalCreator({
companyId,
agents,
goals,
onSaved,
}: {
companyId: string;
agents: AgentRecord[];
goals: GoalRecord[];
onSaved: () => Promise<void>;
}) {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [ownerAgentId, setOwnerAgentId] = useState(agents[0]?.id ?? "");
const [parentId, setParentId] = useState("");
return (
<div className="grid gap-4">
<Input label="Goal title" value={title} onChange={setTitle} placeholder="Goal title" />
<TextArea
label="Success criteria"
value={description}
onChange={setDescription}
placeholder="What must become true for this goal to count as achieved?"
/>
<Select
label="Owner agent"
value={ownerAgentId}
onChange={setOwnerAgentId}
options={[
{ value: "", label: "No owner" },
...agents.map((agent) => ({ value: agent.id, label: agent.name })),
]}
/>
<Select
label="Parent goal"
value={parentId}
onChange={setParentId}
options={[
{ value: "", label: "No parent goal" },
...goals.map((g) => ({ value: g.id, label: g.title })),
]}
/>
<ActionButton
label="Create goal"
tone="accent"
onClick={() =>
void (async () => {
try {
await window.agentCompany.saveGoal({
companyId,
title,
description,
ownerAgentId: ownerAgentId || null,
parentId: parentId || null,
status: "active",
});
setTitle("");
setDescription("");
await onSaved();
} catch (error) {
void error;
}
})()
}
icon={Target}
/>
</div>
);
}
@@ -0,0 +1,111 @@
import { useState } from "react";
import { Pencil, Trash } from "@phosphor-icons/react";
import type { AgentRecord, GoalRecord } from "@shared/types";
import { ActionButton, Input, Select, TextArea } from "../ui";
import { ConfirmDelete } from "./helpers";
export function GoalEditor({
goal,
goals,
agents,
companyId,
onSaved,
onDeleted,
}: {
goal: GoalRecord;
goals: GoalRecord[];
agents: AgentRecord[];
companyId: string;
onSaved: () => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const [title, setTitle] = useState(goal.title);
const [description, setDescription] = useState(goal.description);
const [status, setStatus] = useState<GoalRecord["status"]>(goal.status);
const [parentId, setParentId] = useState(goal.parentId ?? "");
const [ownerAgentId, setOwnerAgentId] = useState(goal.ownerAgentId ?? "");
const [confirmingDelete, setConfirmingDelete] = useState(false);
const parentCandidates = goals.filter((g) => g.id !== goal.id);
return (
<div className="grid gap-4">
<Input label="Goal title" value={title} onChange={setTitle} placeholder="Goal title" />
<TextArea label="Description" value={description} onChange={setDescription} placeholder="Description" />
<Select
label="Status"
value={status}
onChange={(v) => setStatus(v as GoalRecord["status"])}
options={[
{ value: "planned", label: "Planned" },
{ value: "active", label: "Active" },
{ value: "achieved", label: "Achieved" },
{ value: "cancelled", label: "Cancelled" },
]}
/>
<Select
label="Parent goal"
value={parentId}
onChange={setParentId}
options={[
{ value: "", label: "No parent goal" },
...parentCandidates.map((g) => ({ value: g.id, label: g.title })),
]}
/>
<Select
label="Owner agent"
value={ownerAgentId}
onChange={setOwnerAgentId}
options={[
{ value: "", label: "No owner" },
...agents.map((a) => ({ value: a.id, label: a.name })),
]}
/>
<div className="flex items-center gap-3 flex-wrap">
<ActionButton
label="Save changes"
tone="accent"
icon={Pencil}
onClick={() =>
void (async () => {
try {
await window.agentCompany.saveGoal({
id: goal.id,
companyId,
title,
description,
status,
parentId: parentId || null,
ownerAgentId: ownerAgentId || null,
});
await onSaved();
} catch (error) {
void error;
}
})()
}
/>
{confirmingDelete ? (
<ConfirmDelete
entityLabel={goal.title}
onConfirm={() =>
void (async () => {
await window.agentCompany.deleteGoal({ id: goal.id, companyId });
await onDeleted();
})()
}
onCancel={() => setConfirmingDelete(false)}
/>
) : (
<ActionButton
label="Delete"
tone="danger"
icon={Trash}
onClick={() => setConfirmingDelete(true)}
/>
)}
</div>
</div>
);
}
@@ -0,0 +1,54 @@
import { CheckCircle } from "@phosphor-icons/react";
import type { AgentRecord, GoalRecord } from "@shared/types";
import { EmptyState, ListFrame, ListRow, StatusPill } from "../ui";
import { useT } from "../../i18n";
export function GoalList({
goals,
agents,
selectedGoalId,
onSelect,
}: {
goals: GoalRecord[];
agents: AgentRecord[];
selectedGoalId: string | null;
onSelect: (goalId: string) => void;
}) {
const t = useT();
if (goals.length === 0) {
return <EmptyState title={t("org.noGoalsYet")} />;
}
return (
<ListFrame>
{goals.map((goal) => {
const isSelected = goal.id === selectedGoalId;
const parentGoal = goal.parentId ? goals.find((g) => g.id === goal.parentId) : null;
const ownerAgent = goal.ownerAgentId
? agents.find((a) => a.id === goal.ownerAgentId)
: null;
return (
<ListRow
key={goal.id}
onClick={() => onSelect(goal.id)}
className={`cursor-pointer transition-colors duration-100 ${isSelected ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}`}
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
{isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
)}
<div className="text-[15px] text-[color:var(--text)]">{goal.title}</div>
</div>
<StatusPill status={goal.status} />
</div>
<div className="text-[13px] text-[color:var(--muted)]">{goal.description}</div>
<div className="mt-1 flex gap-4 text-[12px] text-[color:var(--muted)]">
{parentGoal && <span>Parent: {parentGoal.title}</span>}
{ownerAgent && <span>Owner: {ownerAgent.name}</span>}
</div>
</ListRow>
);
})}
</ListFrame>
);
}
@@ -0,0 +1,132 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Heartbeat } from "@phosphor-icons/react";
import type { AgentRecord } from "@shared/types";
import { getAgentInitials } from "../../lib/formatters";
import { EmptyState } from "../ui";
import { useT } from "../../i18n";
import { buildOrgTree, layoutForest, flattenLayout, collectEdges, statusDotColor, adapterLabels, CARD_W, CARD_H, CHART_PADDING } from "./orgTreeLayout";
export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent?: (agentId: string) => void }) {
const t = useT();
const containerRef = useRef<HTMLDivElement>(null);
const [pan, setPan] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [dragging, setDragging] = useState(false);
const dragStart = useRef({ x: 0, y: 0, panX: 0, panY: 0 });
const orgTree = useMemo(() => buildOrgTree(agents), [agents]);
const layout = useMemo(() => layoutForest(orgTree), [orgTree]);
const allNodes = useMemo(() => flattenLayout(layout), [layout]);
const edges = useMemo(() => collectEdges(layout), [layout]);
const bounds = useMemo(() => {
if (allNodes.length === 0) return { width: 800, height: 600 };
let maxX = 0, maxY = 0;
for (const n of allNodes) { maxX = Math.max(maxX, n.x + CARD_W); maxY = Math.max(maxY, n.y + CARD_H); }
return { width: maxX + CHART_PADDING, height: maxY + CHART_PADDING };
}, [allNodes]);
const hasInitialized = useRef(false);
useEffect(() => {
if (hasInitialized.current || allNodes.length === 0 || !containerRef.current) return;
hasInitialized.current = true;
const cW = containerRef.current.clientWidth;
const cH = containerRef.current.clientHeight;
const fitZoom = Math.min((cW - 40) / bounds.width, (cH - 40) / bounds.height, 1);
setZoom(fitZoom);
setPan({ x: (cW - bounds.width * fitZoom) / 2, y: (cH - bounds.height * fitZoom) / 2 });
}, [allNodes, bounds]);
const fitToScreen = useCallback(() => {
if (!containerRef.current) return;
const cW = containerRef.current.clientWidth;
const cH = containerRef.current.clientHeight;
const fitZoom = Math.min((cW - 40) / bounds.width, (cH - 40) / bounds.height, 1);
setZoom(fitZoom);
setPan({ x: (cW - bounds.width * fitZoom) / 2, y: (cH - bounds.height * fitZoom) / 2 });
}, [bounds]);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0 || (e.target as HTMLElement).closest("[data-org-card]")) return;
setDragging(true);
dragStart.current = { x: e.clientX, y: e.clientY, panX: pan.x, panY: pan.y };
}, [pan]);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!dragging) return;
setPan({ x: dragStart.current.panX + (e.clientX - dragStart.current.x), y: dragStart.current.panY + (e.clientY - dragStart.current.y) });
}, [dragging]);
const handleMouseUp = useCallback(() => setDragging(false), []);
const handleWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault();
const container = containerRef.current;
if (!container) return;
const rect = container.getBoundingClientRect();
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
const nz = Math.min(Math.max(zoom * (e.deltaY < 0 ? 1.1 : 0.9), 0.2), 2);
const s = nz / zoom;
setPan({ x: mx - s * (mx - pan.x), y: my - s * (my - pan.y) });
setZoom(nz);
}, [zoom, pan]);
if (agents.length === 0) return <EmptyState title={t("org.noAgentsToDisplay")} />;
const zoomIn = () => { const nz = Math.min(zoom * 1.2, 2); const c = containerRef.current; if (c) { const cx = c.clientWidth / 2; const cy = c.clientHeight / 2; const s = nz / zoom; setPan({ x: cx - s * (cx - pan.x), y: cy - s * (cy - pan.y) }); } setZoom(nz); };
const zoomOut = () => { const nz = Math.max(zoom * 0.8, 0.2); const c = containerRef.current; if (c) { const cx = c.clientWidth / 2; const cy = c.clientHeight / 2; const s = nz / zoom; setPan({ x: cx - s * (cx - pan.x), y: cy - s * (cy - pan.y) }); } setZoom(nz); };
return (
<div ref={containerRef} className="relative h-[480px] w-full overflow-hidden rounded-[10px] border border-[color:var(--line)] bg-[color:var(--bg)]" style={{ cursor: dragging ? "grabbing" : "grab" }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onWheel={handleWheel}>
<div className="absolute right-3 top-3 z-10 flex flex-col gap-1">
{[{ label: "+", ariaLabel: "Zoom in", action: zoomIn }, { label: "\u2212", ariaLabel: "Zoom out", action: zoomOut }, { label: "Fit", ariaLabel: "Fit chart to screen", action: fitToScreen }].map((btn) => (
<button key={btn.ariaLabel} className="flex h-7 w-7 items-center justify-center rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] text-[12px] text-[color:var(--text)] transition hover:bg-[color:var(--panel-soft)]" onClick={btn.action} aria-label={btn.ariaLabel}>{btn.label}</button>
))}
</div>
<svg className="pointer-events-none absolute inset-0" style={{ width: "100%", height: "100%" }}>
<g transform={`translate(${pan.x}, ${pan.y}) scale(${zoom})`}>
{edges.map(({ parent, child }) => {
const x1 = parent.x + CARD_W / 2, y1 = parent.y + CARD_H, x2 = child.x + CARD_W / 2, y2 = child.y;
const midY = (y1 + y2) / 2;
const isChildRunning = child.agent.status === "running";
return <path key={`${parent.agent.id}-${child.agent.id}`} d={`M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`} fill="none" stroke={isChildRunning ? "var(--success)" : "var(--line-strong)"} strokeWidth={isChildRunning ? 2 : 1.5} strokeDasharray={isChildRunning ? "6 3" : "none"} opacity={isChildRunning ? 0.8 : 0.5} />;
})}
</g>
</svg>
<div className="absolute inset-0" style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, transformOrigin: "0 0" }}>
{allNodes.map((node) => {
const dotColor = statusDotColor[node.agent.status] ?? "#a3a3a3";
const connectorLabel = adapterLabels[node.agent.connectorId] ?? node.agent.connectorId;
const initials = getAgentInitials(node.agent.name);
const isRunning = node.agent.status === "running";
const isActive = node.agent.status === "active" || isRunning;
const isError = node.agent.status === "error";
const isPending = node.agent.status === "pending_approval";
const isCEO = node.children.length > 0 && !node.agent.reportsTo;
return (
<div key={node.agent.id} data-org-card className={`absolute cursor-pointer select-none rounded-[10px] border bg-[color:var(--panel)] shadow-sm transition-all duration-200 hover:shadow-lg ${isRunning ? "border-[color:var(--success)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" : isError ? "border-[color:var(--danger)] border-opacity-50" : isPending ? "border-[color:var(--warn)] border-dashed" : "border-[color:var(--line)] hover:border-[color:var(--line-strong)]"}`} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} onClick={() => onSelectAgent?.(node.agent.id)}>
<div className={`h-[3px] rounded-t-[9px] ${isCEO ? "bg-[color:var(--accent)]" : node.children.length > 0 ? "bg-[color:var(--warn)]" : "bg-[color:var(--muted)] opacity-30"}`} />
<div className="flex items-center gap-3 px-4 py-3">
<div className="relative shrink-0">
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[13px] font-bold ${isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : isError ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>{initials}</div>
{isRunning ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" style={{ backgroundColor: dotColor }} />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
</span>
) : (
<span className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
)}
</div>
<div className="flex min-w-0 flex-1 flex-col items-start">
<span className="text-[13px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span>
<span className="mt-0.5 text-[11px] leading-tight text-[color:var(--muted)]">{node.agent.title || node.agent.role}</span>
<div className="mt-1.5 flex items-center gap-2">
<span className="rounded bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{connectorLabel}</span>
{node.agent.heartbeatEnabled ? <Heartbeat size={10} className="text-[color:var(--success)]" /> : null}
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,78 @@
import { useState } from "react";
import { Kanban } from "@phosphor-icons/react";
import type { AgentRecord, GoalRecord } from "@shared/types";
import { ActionButton, Input, Select, TextArea } from "../ui";
export function ProjectCreator({
companyId,
goals,
agents,
onSaved,
}: {
companyId: string;
goals: GoalRecord[];
agents: AgentRecord[];
onSaved: () => Promise<void>;
}) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [goalId, setGoalId] = useState(goals[0]?.id ?? "");
const [leadAgentId, setLeadAgentId] = useState(agents[0]?.id ?? "");
const [targetDate, setTargetDate] = useState("");
return (
<div className="grid gap-4">
<Input label="Project name" value={name} onChange={setName} placeholder="Project name" />
<TextArea
label="Description"
value={description}
onChange={setDescription}
placeholder="What delivery lane does this project own?"
/>
<Select
label="Linked goal"
value={goalId}
onChange={setGoalId}
options={[
{ value: "", label: "No linked goal" },
...goals.map((goal) => ({ value: goal.id, label: goal.title })),
]}
/>
<Select
label="Lead agent"
value={leadAgentId}
onChange={setLeadAgentId}
options={[
{ value: "", label: "No lead" },
...agents.map((agent) => ({ value: agent.id, label: agent.name })),
]}
/>
<Input label="Target date" value={targetDate} onChange={setTargetDate} placeholder="YYYY-MM-DD" />
<ActionButton
label="Create project"
tone="accent"
onClick={() =>
void (async () => {
try {
await window.agentCompany.saveProject({
companyId,
goalId: goalId || null,
name,
description,
leadAgentId: leadAgentId || null,
targetDate: targetDate || null,
status: "planned",
});
setName("");
setDescription("");
setTargetDate("");
await onSaved();
} catch (error) {
void error;
}
})()
}
icon={Kanban}
/>
</div>
);
}
@@ -0,0 +1,113 @@
import { useState } from "react";
import { Pencil, Trash } from "@phosphor-icons/react";
import type { AgentRecord, GoalRecord, ProjectRecord } from "@shared/types";
import { ActionButton, Input, Select, TextArea } from "../ui";
import { ConfirmDelete } from "./helpers";
export function ProjectEditor({
project,
goals,
agents,
companyId,
onSaved,
onDeleted,
}: {
project: ProjectRecord;
goals: GoalRecord[];
agents: AgentRecord[];
companyId: string;
onSaved: () => Promise<void>;
onDeleted: () => Promise<void>;
}) {
const [name, setName] = useState(project.name);
const [description, setDescription] = useState(project.description);
const [status, setStatus] = useState<ProjectRecord["status"]>(project.status);
const [goalId, setGoalId] = useState(project.goalId ?? "");
const [leadAgentId, setLeadAgentId] = useState(project.leadAgentId ?? "");
const [targetDate, setTargetDate] = useState(project.targetDate ?? "");
const [confirmingDelete, setConfirmingDelete] = useState(false);
return (
<div className="grid gap-4">
<Input label="Project name" value={name} onChange={setName} placeholder="Project name" />
<TextArea label="Description" value={description} onChange={setDescription} placeholder="Description" />
<Select
label="Status"
value={status}
onChange={(v) => setStatus(v as ProjectRecord["status"])}
options={[
{ value: "backlog", label: "Backlog" },
{ value: "planned", label: "Planned" },
{ value: "in_progress", label: "In Progress" },
{ value: "completed", label: "Completed" },
{ value: "cancelled", label: "Cancelled" },
]}
/>
<Select
label="Linked goal"
value={goalId}
onChange={setGoalId}
options={[
{ value: "", label: "No linked goal" },
...goals.map((g) => ({ value: g.id, label: g.title })),
]}
/>
<Select
label="Lead agent"
value={leadAgentId}
onChange={setLeadAgentId}
options={[
{ value: "", label: "No lead" },
...agents.map((a) => ({ value: a.id, label: a.name })),
]}
/>
<Input label="Target date" value={targetDate} onChange={setTargetDate} placeholder="YYYY-MM-DD" />
<div className="flex items-center gap-3">
<ActionButton
label="Save changes"
tone="accent"
icon={Pencil}
onClick={() =>
void (async () => {
try {
await window.agentCompany.saveProject({
id: project.id,
companyId,
name,
description,
status,
goalId: goalId || null,
leadAgentId: leadAgentId || null,
targetDate: targetDate || null,
});
await onSaved();
} catch (error) {
void error;
}
})()
}
/>
{confirmingDelete ? (
<ConfirmDelete
entityLabel={project.name}
onConfirm={() =>
void (async () => {
await window.agentCompany.deleteProject({ id: project.id, companyId });
await onDeleted();
})()
}
onCancel={() => setConfirmingDelete(false)}
/>
) : (
<ActionButton
label="Delete"
tone="danger"
icon={Trash}
onClick={() => setConfirmingDelete(true)}
/>
)}
</div>
</div>
);
}
@@ -0,0 +1,65 @@
import { CheckCircle } from "@phosphor-icons/react";
import type { AgentRecord, GoalRecord, ProjectRecord, WorkspaceRecord } from "@shared/types";
import { EmptyState, ListFrame, ListRow, StatusPill } from "../ui";
import { useT } from "../../i18n";
export function ProjectList({
projects,
goals,
agents,
workspaces,
selectedProjectId,
onSelect,
}: {
projects: ProjectRecord[];
goals: GoalRecord[];
agents: AgentRecord[];
workspaces: WorkspaceRecord[];
selectedProjectId: string | null;
onSelect: (projectId: string) => void;
}) {
const t = useT();
if (projects.length === 0) {
return <EmptyState title={t("org.noProjectsYet")} />;
}
return (
<ListFrame>
{projects.map((project) => {
const isSelected = project.id === selectedProjectId;
const wsCount = workspaces.filter((w) => w.projectId === project.id).length;
return (
<ListRow
key={project.id}
onClick={() => onSelect(project.id)}
className={`cursor-pointer transition-colors duration-100 ${isSelected ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}`}
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
{isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
)}
<div className="text-[15px] text-[color:var(--text)]">{project.name}</div>
</div>
<StatusPill status={project.status} />
</div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">{project.description}</div>
<div className="flex flex-wrap gap-4 text-[12px] text-[color:var(--muted)]">
<span>
Goal: {goals.find((g) => g.id === project.goalId)?.title ?? "None"}
</span>
<span>
Lead: {agents.find((a) => a.id === project.leadAgentId)?.name ?? "Unassigned"}
</span>
{project.targetDate && <span>Target: {project.targetDate}</span>}
{wsCount > 0 && (
<span>
{wsCount} workspace{wsCount !== 1 ? "s" : ""}
</span>
)}
</div>
</ListRow>
);
})}
</ListFrame>
);
}
@@ -0,0 +1,128 @@
import { Heartbeat, Lightning, Trash, XCircle } from "@phosphor-icons/react";
import type { AgentRecord } from "@shared/types";
import { ActionButton, Select } from "../ui";
import { formatMoney, formatTime } from "../../lib/formatters";
export function BudgetBar({ spent, budget }: { spent: number; budget: number }) {
const pct = budget > 0 ? Math.min((spent / budget) * 100, 100) : 0;
const overBudget = spent > budget && budget > 0;
return (
<div className="flex items-center gap-2">
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-[color:var(--line)]">
<div
className={`h-full rounded-full transition-all ${overBudget ? "bg-[color:var(--danger)]" : "bg-[color:var(--success)]"}`}
style={{ width: `${pct}%` }}
/>
</div>
<span className={`text-[11px] ${overBudget ? "text-[color:var(--danger)]" : "text-[color:var(--muted)]"}`}>
{formatMoney(spent)} / {formatMoney(budget)}
</span>
</div>
);
}
export function ConfirmDelete({
entityLabel,
onConfirm,
onCancel,
}: {
entityLabel: string;
onConfirm: () => void;
onCancel: () => void;
}) {
return (
<div className="flex items-center gap-3 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3">
<span className="text-[13px] text-[color:var(--danger)]">Delete {entityLabel}?</span>
<ActionButton label="Yes, delete" tone="danger" onClick={onConfirm} icon={Trash} />
<ActionButton label="Cancel" onClick={onCancel} icon={XCircle} />
</div>
);
}
const HEARTBEAT_INTERVALS = [
{ value: "30", label: "30 seconds" },
{ value: "60", label: "1 minute" },
{ value: "120", label: "2 minutes" },
{ value: "300", label: "5 minutes" },
{ value: "600", label: "10 minutes" },
{ value: "1800", label: "30 minutes" },
{ value: "3600", label: "1 hour" },
];
export function HeartbeatSection({
agent,
companyId,
pendingApproval,
persistedConnectorIssue,
onSaved,
}: {
agent: AgentRecord;
companyId: string;
pendingApproval: boolean;
persistedConnectorIssue: string | null;
onSaved: () => Promise<void>;
}) {
return (
<div className="rounded-lg border border-[color:var(--line)] p-3">
<div className="mb-2 flex items-center gap-2">
<Heartbeat size={16} className="text-[color:var(--accent)]" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">Continuous Execution</span>
</div>
<div className="mb-2 flex items-center gap-3">
<label className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={agent.heartbeatEnabled}
disabled={pendingApproval}
onChange={(e) =>
void (async () => {
await window.agentCompany.setHeartbeat({ agentId: agent.id, companyId, enabled: e.target.checked, intervalSec: agent.heartbeatIntervalSec || 120 });
await onSaved();
})()
}
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
/>
<span className="text-[13px] text-[color:var(--text)]">Enable heartbeat</span>
</label>
{agent.heartbeatEnabled && (
<span className="text-[11px] text-[color:var(--muted)]">
Every {agent.heartbeatIntervalSec}s{agent.lastHeartbeatAt ? ` · Last: ${formatTime(agent.lastHeartbeatAt)}` : ""}
</span>
)}
</div>
{agent.heartbeatEnabled && (
<Select
label="Interval"
value={String(agent.heartbeatIntervalSec)}
onChange={(v) =>
void (async () => {
await window.agentCompany.setHeartbeat({ agentId: agent.id, companyId, enabled: true, intervalSec: Number(v) });
await onSaved();
})()
}
disabled={pendingApproval}
options={HEARTBEAT_INTERVALS}
/>
)}
<div className="mt-2">
<ActionButton
label="Wake now"
tone="accent"
icon={Lightning}
disabled={pendingApproval || !!persistedConnectorIssue}
onClick={() =>
void (async () => {
await window.agentCompany.triggerHeartbeat({ agentId: agent.id, companyId, trigger: "manual" });
await onSaved();
})()
}
/>
{persistedConnectorIssue ? (
<p className="mt-2 text-[12px] text-[color:var(--warn)]">
{persistedConnectorIssue} Save a ready connector before waking this agent again.
</p>
) : null}
</div>
</div>
);
}
@@ -0,0 +1,101 @@
import type { AgentRecord } from "@shared/types";
export const CARD_W = 220;
export const CARD_H = 110;
export const GAP_X = 32;
export const GAP_Y = 80;
export const CHART_PADDING = 60;
export interface OrgTreeNode {
agent: AgentRecord;
children: OrgTreeNode[];
}
export interface LayoutNode {
agent: AgentRecord;
x: number;
y: number;
children: LayoutNode[];
}
export function buildOrgTree(agents: AgentRecord[]): OrgTreeNode[] {
const agentIds = new Set(agents.map((a) => a.id));
const childrenMap = new Map<string, AgentRecord[]>();
const roots: AgentRecord[] = [];
for (const agent of agents) {
if (!agent.reportsTo || !agentIds.has(agent.reportsTo)) {
roots.push(agent);
} else {
const list = childrenMap.get(agent.reportsTo) ?? [];
list.push(agent);
childrenMap.set(agent.reportsTo, list);
}
}
function toNode(agent: AgentRecord): OrgTreeNode {
const kids = childrenMap.get(agent.id) ?? [];
return { agent, children: kids.map(toNode) };
}
return roots.map(toNode);
}
export function subtreeWidth(node: OrgTreeNode): number {
if (node.children.length === 0) return CARD_W;
const childrenW = node.children.reduce((sum, c) => sum + subtreeWidth(c), 0);
const gaps = (node.children.length - 1) * GAP_X;
return Math.max(CARD_W, childrenW + gaps);
}
function layoutTree(node: OrgTreeNode, x: number, y: number): LayoutNode {
const totalW = subtreeWidth(node);
const layoutChildren: LayoutNode[] = [];
if (node.children.length > 0) {
const childrenW = node.children.reduce((sum, c) => sum + subtreeWidth(c), 0);
const gaps = (node.children.length - 1) * GAP_X;
let cx = x + (totalW - childrenW - gaps) / 2;
for (const child of node.children) {
const cw = subtreeWidth(child);
layoutChildren.push(layoutTree(child, cx, y + CARD_H + GAP_Y));
cx += cw + GAP_X;
}
}
return { agent: node.agent, x: x + (totalW - CARD_W) / 2, y, children: layoutChildren };
}
export function layoutForest(roots: OrgTreeNode[]): LayoutNode[] {
let x = CHART_PADDING;
return roots.map((root) => {
const w = subtreeWidth(root);
const node = layoutTree(root, x, CHART_PADDING);
x += w + GAP_X;
return node;
});
}
export function flattenLayout(nodes: LayoutNode[]): LayoutNode[] {
const result: LayoutNode[] = [];
function walk(n: LayoutNode) { result.push(n); n.children.forEach(walk); }
nodes.forEach(walk);
return result;
}
export function collectEdges(nodes: LayoutNode[]): Array<{ parent: LayoutNode; child: LayoutNode }> {
const edges: Array<{ parent: LayoutNode; child: LayoutNode }> = [];
function walk(n: LayoutNode) { for (const c of n.children) { edges.push({ parent: n, child: c }); walk(c); } }
nodes.forEach(walk);
return edges;
}
export const statusDotColor: Record<string, string> = {
running: "#22d3ee",
active: "#4ade80",
paused: "#facc15",
idle: "#facc15",
error: "#f87171",
terminated: "#a3a3a3",
};
export const adapterLabels: Record<string, string> = {
claude_local: "Claude",
codex_local: "Codex",
gemini_local: "Gemini",
};