This commit is contained in:
ZacharyZhang-NY committed 2026-03-18 17:49:10 -04:00
1 parent 48daca9ba0
commit 53e0f03c0c
13 files changed
+509 -125

No files matched your search

-1
View File
@@ -43,7 +43,6 @@ import type {
InboxItem,
OnboardingBootstrapResult,
ProfileSnapshot,
RunRecord,
SectionId,
SocialAccountRecord,
BrowserActionRecord,
+40 -24
View File
@@ -11,6 +11,7 @@ import {
WarningCircle,
XCircle,
} from "@phosphor-icons/react";
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
import type { OnboardingBootstrapResult, ProfileSnapshot } from "@shared/types";
import { unwrap } from "../lib/desktop";
import { ActionButton, Input, Select, TextArea } from "./ui";
@@ -39,23 +40,24 @@ export function CompanyOnboarding({
const [error, setError] = useState<string | null>(null);
const [launchSummary, setLaunchSummary] = useState<OnboardingBootstrapResult | null>(null);
const readyConnectors = useMemo(
() => connectors.filter((connector) => connector.status === "ready" || connector.status === "degraded"),
const executionReadyConnectors = useMemo(
() => connectors.filter((connector) => isConnectorExecutionReady(connector)),
[connectors],
);
const [selectedConnectorId, setSelectedConnectorId] = useState<string>(readyConnectors[0]?.id ?? "");
const hasRunnableConnector = readyConnectors.length > 0;
const selectedConnector = readyConnectors.find((connector) => connector.id === selectedConnectorId) ?? readyConnectors[0] ?? null;
const [selectedConnectorId, setSelectedConnectorId] = useState<string>(executionReadyConnectors[0]?.id ?? "");
const hasRunnableConnector = executionReadyConnectors.length > 0;
const selectedConnector =
executionReadyConnectors.find((connector) => connector.id === selectedConnectorId) ?? executionReadyConnectors[0] ?? null;
useEffect(() => {
if (!selectedConnectorId && readyConnectors[0]) {
setSelectedConnectorId(readyConnectors[0].id);
if (!selectedConnectorId && executionReadyConnectors[0]) {
setSelectedConnectorId(executionReadyConnectors[0].id);
return;
}
if (selectedConnectorId && !readyConnectors.some((connector) => connector.id === selectedConnectorId)) {
setSelectedConnectorId(readyConnectors[0]?.id ?? "");
if (selectedConnectorId && !executionReadyConnectors.some((connector) => connector.id === selectedConnectorId)) {
setSelectedConnectorId(executionReadyConnectors[0]?.id ?? "");
}
}, [readyConnectors, selectedConnectorId]);
}, [executionReadyConnectors, selectedConnectorId]);
const stepLabels = [
{ label: "Company", icon: Buildings },
@@ -105,7 +107,7 @@ export function CompanyOnboarding({
return;
}
if (!selectedConnector) {
setError("At least one ready connector is required before you can continue.");
setError("At least one execution-ready connector is required before you can continue.");
return;
}
setError(null);
@@ -160,8 +162,8 @@ export function CompanyOnboarding({
agent company.
</h1>
<p className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]">
This flow creates one company, binds a real connector, links an optional workspace, writes the first goal,
creates the operating project, and queues the first execution task.
This flow creates one company, binds one execution-ready connector, links an optional workspace, writes
the first goal, creates the operating project, and queues the first execution task.
</p>
<div className="mt-10 flex items-center gap-3">
@@ -192,10 +194,11 @@ export function CompanyOnboarding({
</div>
<div className="space-y-2.5">
{connectors.map((connector) => {
const isRunnable = connector.status === "ready" || connector.status === "degraded";
const issue = getConnectorExecutionReadinessIssue(connector);
const isRunnable = !issue;
const isMissing = connector.status === "not_installed";
return (
<div key={connector.id} className="flex items-center gap-2.5 text-[13px]">
<div key={connector.id} className="flex items-start gap-2.5 text-[13px]">
{isRunnable ? (
<CheckCircle size={16} weight="fill" className="shrink-0 text-[color:var(--success)]" />
) : isMissing ? (
@@ -203,19 +206,28 @@ export function CompanyOnboarding({
) : (
<WarningCircle size={16} weight="fill" className="shrink-0 text-[color:var(--warn)]" />
)}
<span className={isRunnable ? "font-medium text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
{connector.label}
</span>
<span className="text-[11px] text-[color:var(--muted)]">
{connector.status.replaceAll("_", " ")}
</span>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={isRunnable ? "font-medium text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
{connector.label}
</span>
<span className="text-[11px] text-[color:var(--muted)]">
{connector.status.replaceAll("_", " ")}
</span>
</div>
{issue ? (
<div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">
{issue}
</div>
) : null}
</div>
</div>
);
})}
</div>
{!hasRunnableConnector ? (
<div className="mt-4 rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
Install and authenticate at least one supported CLI runtime before creating the company.
Install, authenticate, and fully validate at least one supported CLI runtime before creating the company.
</div>
) : null}
</div>
@@ -270,11 +282,15 @@ export function CompanyOnboarding({
label="Primary connector"
value={selectedConnector?.id ?? ""}
onChange={setSelectedConnectorId}
options={readyConnectors.map((connector) => ({
options={executionReadyConnectors.map((connector) => ({
value: connector.id,
label: `${connector.label} (${connector.status.replaceAll("_", " ")})`,
label: connector.label,
}))}
/>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
Only connectors in `ready` state with session resume and structured transcripts are offered here.
Re-run connector health checks from Settings if the list is empty.
</div>
<div>
<label
htmlFor="onboarding-workspace-path"
+4 -3
View File
@@ -39,6 +39,7 @@ import type {
TaskRecord,
WorkflowPipelineRecord,
} from "@shared/types";
import { isConnectorExecutionReady } from "@shared/connector-policy";
import { formatMoney, getAgentInitials, timeAgo } from "../lib/formatters";
import { StatusPill } from "./ui";
@@ -76,7 +77,7 @@ function computeHealthScore(props: Pick<DashboardProps, "agents" | "tasks" | "ru
const errorAgents = agents.filter((a) => a.status === "error");
score -= Math.min(errorAgents.length * 10, 20);
const readyConnectors = connectors.filter((c) => c.status === "ready");
const readyConnectors = connectors.filter((connector) => isConnectorExecutionReady(connector));
if (readyConnectors.length === 0 && connectors.length > 0) score -= 15;
const overBudget = agents.filter((a) => a.budgetMonthlyUsd > 0 && a.spentMonthlyUsd >= a.budgetMonthlyUsd);
@@ -602,7 +603,7 @@ export function Dashboard(props: DashboardProps) {
const activeGoals = useMemo(() => goals.filter((g) => g.status === "active"), [goals]);
const totalSpend = useMemo(() => costs.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0), [costs]);
const activeAgents = useMemo(() => agents.filter((a) => a.status !== "terminated" && a.status !== "pending_approval"), [agents]);
const readyConnectors = useMemo(() => connectors.filter((c) => c.status === "ready"), [connectors]);
const readyConnectors = useMemo(() => connectors.filter((connector) => isConnectorExecutionReady(connector)), [connectors]);
const activeSprint = useMemo(() => sprints.find((s) => s.status === "active"), [sprints]);
const unreadMessages = useMemo(() => messages.filter((message) => !message.readAt), [messages]);
const urgentMessages = useMemo(() => messages.filter((message) => !message.readAt && (message.priority === "urgent" || message.channel === "incident")), [messages]);
@@ -812,7 +813,7 @@ export function Dashboard(props: DashboardProps) {
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Infrastructure</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
{connectors.map((connector) => {
const isReady = connector.status === "ready";
const isReady = isConnectorExecutionReady(connector);
const isError = connector.status === "error" || connector.status === "auth_required";
const agentsUsing = agents.filter((a) => a.connectorId === connector.id).length;
return (
+64 -5
View File
@@ -17,6 +17,7 @@ import type {
ProjectRecord,
WorkspaceRecord,
} from "@shared/types";
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
import { formatMoney, formatTime, getAgentInitials } from "../lib/formatters";
import { unwrap } from "../lib/desktop";
import {
@@ -137,10 +138,14 @@ export function AgentCreator({
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(connectors[0]?.id ?? "codex_local");
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 ?? "");
@@ -150,8 +155,14 @@ export function AgentCreator({
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) {
@@ -164,6 +175,16 @@ export function AgentCreator({
}
}, [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" />
@@ -173,8 +194,13 @@ export function AgentCreator({
label="Connector"
value={connectorId}
onChange={(value) => setConnectorId(value as ConnectorRecord["id"])}
options={connectors.map((connector) => ({ value: connector.id, label: connector.label }))}
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"
@@ -231,6 +257,10 @@ export function AgentCreator({
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({
@@ -261,6 +291,7 @@ export function AgentCreator({
})()
}
icon={Robot}
disabled={!selectedConnector}
/>
{requestError ? <p className="text-[12px] text-[color:var(--danger)]">{requestError}</p> : null}
</div>
@@ -301,6 +332,24 @@ export function AgentEditor({
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 () => {
@@ -352,8 +401,13 @@ export function AgentEditor({
value={connectorId}
onChange={(v) => setConnectorId(v as ConnectorRecord["id"])}
disabled={pendingApproval}
options={connectors.map((c) => ({ value: c.id, label: c.label }))}
options={connectorOptions}
/>
{selectedConnectorIssue ? (
<p className="text-[12px] text-[color:var(--warn)]">
{selectedConnectorIssue}
</p>
) : null}
{availableModels.length > 0 ? (
<Select
label="Model"
@@ -466,7 +520,7 @@ export function AgentEditor({
label="Wake now"
tone="accent"
icon={Lightning}
disabled={pendingApproval}
disabled={pendingApproval || !!persistedConnectorIssue}
onClick={() =>
void (async () => {
await window.agentCompany.triggerHeartbeat({
@@ -478,6 +532,11 @@ export function AgentEditor({
})()
}
/>
{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>
@@ -486,7 +545,7 @@ export function AgentEditor({
label="Save changes"
tone="accent"
icon={Pencil}
disabled={pendingApproval}
disabled={pendingApproval || !!selectedConnectorIssue}
onClick={() =>
void (async () => {
setSaveError(null);