update
This commit is contained in:
1 parent
48daca9ba0
commit
53e0f03c0c
13 files changed
+509
-125
No files matched your search
@@ -43,7 +43,6 @@ import type {
|
||||
InboxItem,
|
||||
OnboardingBootstrapResult,
|
||||
ProfileSnapshot,
|
||||
RunRecord,
|
||||
SectionId,
|
||||
SocialAccountRecord,
|
||||
BrowserActionRecord,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user