update
This commit is contained in:
1 parent
9779c4d1ca
commit
1b578c111a
719 files changed
+191593
-98
No files matched your search
@@ -215,7 +215,7 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
|
||||
</div>
|
||||
{approval.type === "hire_agent" ? (() => { const payload = parseApprovalPayloadJson(approval); const payloadAgentId = typeof payload?.agentId === "string" ? payload.agentId : null; const reqSnap = payload?.requestedConfigurationSnapshot && typeof payload.requestedConfigurationSnapshot === "object" && !Array.isArray(payload.requestedConfigurationSnapshot) ? payload.requestedConfigurationSnapshot as Record<string, unknown> : null; const proposedAgent = (approval.relatedAgentId ? (agents.find((a) => a.id === approval.relatedAgentId) ?? null) : null) ?? (payloadAgentId ? (agents.find((a) => a.id === payloadAgentId) ?? null) : null); const mgrId = proposedAgent?.reportsTo ?? (typeof reqSnap?.reportsTo === "string" ? reqSnap.reportsTo : null); const mgr = mgrId ? (agents.find((a) => a.id === mgrId) ?? null) : null; if (!proposedAgent && !reqSnap) return null; return (
|
||||
<div className="rounded-lg border border-[color:var(--primary)] border-opacity-30 bg-accent px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Proposed Hire</div><div className="grid gap-1.5 text-[12px]">
|
||||
<div><span className="text-muted-foreground">Name:</span> <span className="font-medium text-foreground">{proposedAgent?.name ?? approval.payloadSummary.replace(/^Hire\s+/i, "").split(/\s+as\s+/i)[0] ?? "Pending hire"}</span></div>
|
||||
<div><span className="text-muted-foreground">Name:</span> <span className="font-medium text-foreground">{proposedAgent?.name ?? (typeof reqSnap?.name === "string" ? reqSnap.name : "Unspecified")}</span></div>
|
||||
<div><span className="text-muted-foreground">Role:</span> <span className="text-foreground">{proposedAgent?.role ?? (typeof reqSnap?.role === "string" ? reqSnap.role : "Unspecified")}</span></div>
|
||||
{(proposedAgent?.title || typeof reqSnap?.title === "string") ? <div><span className="text-muted-foreground">Title:</span> <span className="text-foreground">{proposedAgent?.title || reqSnap?.title as string}</span></div> : null}
|
||||
{(proposedAgent?.capabilities || typeof reqSnap?.capabilities === "string") ? <div><span className="text-muted-foreground">Capabilities:</span> <span className="text-foreground">{proposedAgent?.capabilities || reqSnap?.capabilities as string}</span></div> : null}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { BudgetPolicyRecord } from "@shared/types";
|
||||
import { ActionButton, Input, Modal, Select } from "../ui";
|
||||
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input";
|
||||
|
||||
interface PolicyFormModalProps {
|
||||
open: boolean;
|
||||
@@ -25,7 +26,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
|
||||
if (editingPolicy) {
|
||||
setScopeType(editingPolicy.scopeType);
|
||||
setScopeId(editingPolicy.scopeId);
|
||||
setAmountDollars((editingPolicy.amount / 100).toFixed(2));
|
||||
setAmountDollars(getBudgetPolicyAmountDollars(editingPolicy));
|
||||
setWarnPercent(String(editingPolicy.warnPercent));
|
||||
setHardStopEnabled(editingPolicy.hardStopEnabled);
|
||||
setNotifyEnabled(editingPolicy.notifyEnabled);
|
||||
@@ -52,24 +53,19 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const amountCents = Math.round(parseFloat(amountDollars) * 100);
|
||||
if (Number.isNaN(amountCents) || amountCents <= 0) return;
|
||||
|
||||
const wp = parseInt(warnPercent, 10);
|
||||
if (Number.isNaN(wp) || wp < 1 || wp > 100) return;
|
||||
|
||||
const result = await window.agentCompany.saveBudgetPolicy({
|
||||
const payload = buildBudgetPolicyInput({
|
||||
policyId: editingPolicy?.id ?? null,
|
||||
companyId,
|
||||
scopeType,
|
||||
scopeId,
|
||||
metric: "billed_cents",
|
||||
windowKind: "calendar_month_utc",
|
||||
amount: amountCents,
|
||||
warnPercent: wp,
|
||||
amountDollars,
|
||||
warnPercent,
|
||||
hardStopEnabled,
|
||||
notifyEnabled,
|
||||
isActive: true,
|
||||
});
|
||||
if (!payload) return;
|
||||
|
||||
const result = await window.agentCompany.saveBudgetPolicy(payload);
|
||||
if (result.ok) {
|
||||
onSaved();
|
||||
onClose();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Robot } from "@phosphor-icons/react";
|
||||
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
|
||||
import { DEPARTMENT_OPTIONS } from "@shared/constants";
|
||||
import type { AgentRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types";
|
||||
import { isConnectorExecutionReady } from "@shared/connector-policy";
|
||||
import { unwrap } from "../../lib/desktop";
|
||||
import { ActionButton, Input, Select, TextArea } from "../ui";
|
||||
@@ -27,6 +28,7 @@ export function AgentCreator({
|
||||
const [name, setName] = useState("");
|
||||
const [role, setRole] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [department, setDepartment] = useState<DepartmentId | "">("");
|
||||
const [connectorId, setConnectorId] = useState(executionReadyConnectors[0]?.id ?? "");
|
||||
const [model, setModel] = useState("");
|
||||
const [availableModels, setAvailableModels] = useState<Array<{ id: string; label: string }>>([]);
|
||||
@@ -69,6 +71,7 @@ export function AgentCreator({
|
||||
|
||||
const t = useT();
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const selectedDepartment: DepartmentId | null = department === "" ? null : department;
|
||||
|
||||
return (
|
||||
<div className="grid min-w-0 gap-4">
|
||||
@@ -114,6 +117,15 @@ export function AgentCreator({
|
||||
<AnimatePresence>
|
||||
{showAdvanced ? (
|
||||
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }} className="grid gap-4 overflow-hidden">
|
||||
<Select
|
||||
label={t("comm.department")}
|
||||
value={department}
|
||||
onChange={(value) => setDepartment(value as DepartmentId | "")}
|
||||
options={DEPARTMENT_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.value === "" ? t("comm.selectDepartment") : option.label,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
label={t("agent.workspace")}
|
||||
value={workspaceId}
|
||||
@@ -162,21 +174,22 @@ export function AgentCreator({
|
||||
unwrap(await window.agentCompany.requestHire({
|
||||
companyId,
|
||||
requestedByAgentId: null,
|
||||
name,
|
||||
role,
|
||||
title,
|
||||
department: null,
|
||||
name: name.trim(),
|
||||
role: role.trim(),
|
||||
title: title.trim(),
|
||||
department: selectedDepartment,
|
||||
connectorId: connectorId as ConnectorRecord["id"],
|
||||
workspaceId: workspaceId || null,
|
||||
reportsTo: reportsTo || null,
|
||||
model: model || null,
|
||||
metadataJson: JSON.stringify(meta),
|
||||
capabilities,
|
||||
capabilities: capabilities.trim(),
|
||||
budgetMonthlyUsd: Number(budget || 0),
|
||||
}));
|
||||
setName("");
|
||||
setRole("");
|
||||
setTitle("");
|
||||
setDepartment("");
|
||||
setCapabilities("");
|
||||
setReportsTo("");
|
||||
setRequestError(null);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Pencil, Trash } from "@phosphor-icons/react";
|
||||
import type { AgentRecord, BudgetPolicyRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
|
||||
import { DEPARTMENT_OPTIONS } from "@shared/constants";
|
||||
import type { AgentRecord, BudgetPolicyRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types";
|
||||
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
|
||||
import { unwrap } from "../../lib/desktop";
|
||||
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input";
|
||||
import { useAppStore } from "../../state/store";
|
||||
import { useT } from "../../i18n";
|
||||
import { ActionButton, Input, Select, TextArea } from "../ui";
|
||||
@@ -30,6 +32,7 @@ export function AgentEditor({
|
||||
const [name, setName] = useState(agent.name);
|
||||
const [role, setRole] = useState(agent.role);
|
||||
const [title, setTitle] = useState(agent.title);
|
||||
const [department, setDepartment] = useState<DepartmentId | "">(agent.department ?? "");
|
||||
const [status, setStatus] = useState<AgentRecord["status"]>(agent.status);
|
||||
const [connectorId, setConnectorId] = useState<ConnectorRecord["id"]>(agent.connectorId);
|
||||
const [model, setModel] = useState(agent.model ?? "");
|
||||
@@ -108,18 +111,22 @@ export function AgentEditor({
|
||||
void (async () => {
|
||||
const result = await window.agentCompany.listBudgetPolicies(companyId);
|
||||
if (result.ok) {
|
||||
const policy = result.data.find(
|
||||
(p) => p.scopeType === "agent" && p.scopeId === agent.id
|
||||
);
|
||||
const policy = result.data
|
||||
.filter((p) => p.scopeType === "agent" && p.scopeId === agent.id)
|
||||
.sort((left, right) => {
|
||||
if (left.isActive !== right.isActive) return left.isActive ? -1 : 1;
|
||||
return right.updatedAt.localeCompare(left.updatedAt);
|
||||
})[0];
|
||||
if (policy) {
|
||||
setAgentPolicy(policy);
|
||||
setBudget(String((policy.amount / 100).toFixed(2)));
|
||||
setBudget(getBudgetPolicyAmountDollars(policy));
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [companyId, agent.id]);
|
||||
|
||||
const managerCandidates = agents.filter((a) => a.id !== agent.id);
|
||||
const selectedDepartment: DepartmentId | null = department === "" ? null : department;
|
||||
|
||||
async function persistAgentDraft() {
|
||||
const meta = { ...existingMeta };
|
||||
@@ -130,9 +137,10 @@ export function AgentEditor({
|
||||
unwrap(await window.agentCompany.saveAgent({
|
||||
id: agent.id,
|
||||
companyId,
|
||||
name,
|
||||
role,
|
||||
title,
|
||||
name: name.trim(),
|
||||
role: role.trim(),
|
||||
title: title.trim(),
|
||||
department: selectedDepartment,
|
||||
status,
|
||||
connectorId,
|
||||
workspaceId: workspaceId || null,
|
||||
@@ -140,22 +148,24 @@ export function AgentEditor({
|
||||
model: model || null,
|
||||
metadataJson: JSON.stringify(meta),
|
||||
budgetMonthlyUsd: budgetDollars,
|
||||
capabilities,
|
||||
capabilities: capabilities.trim(),
|
||||
}));
|
||||
|
||||
if (budgetDollars > 0 && agent.id) {
|
||||
await window.agentCompany.saveBudgetPolicy({
|
||||
const payload = buildBudgetPolicyInput({
|
||||
policyId: agentPolicy?.id ?? null,
|
||||
companyId,
|
||||
scopeType: "agent",
|
||||
scopeId: agent.id,
|
||||
metric: "billed_cents",
|
||||
windowKind: "calendar_month_utc",
|
||||
amount: Math.round(budgetDollars * 100),
|
||||
warnPercent: agentPolicy?.warnPercent ?? 80,
|
||||
amountDollars: String(budgetDollars),
|
||||
warnPercent: String(agentPolicy?.warnPercent ?? 80),
|
||||
hardStopEnabled: agentPolicy?.hardStopEnabled ?? true,
|
||||
notifyEnabled: agentPolicy?.notifyEnabled ?? true,
|
||||
isActive: true,
|
||||
});
|
||||
if (!payload) {
|
||||
throw new Error("Agent budget policy is invalid.");
|
||||
}
|
||||
unwrap(await window.agentCompany.saveBudgetPolicy(payload));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +185,16 @@ export function AgentEditor({
|
||||
<Input label={t("agent.name")} value={name} onChange={setName} placeholder={t("agent.name")} readOnly={pendingApproval} disabled={pendingApproval} />
|
||||
<Input label={t("agent.role")} value={role} onChange={setRole} placeholder={t("agent.role")} readOnly={pendingApproval} disabled={pendingApproval} />
|
||||
<Input label={t("agent.title")} value={title} onChange={setTitle} placeholder={t("agent.title")} readOnly={pendingApproval} disabled={pendingApproval} />
|
||||
<Select
|
||||
label={t("comm.department")}
|
||||
value={department}
|
||||
onChange={(value) => setDepartment(value as DepartmentId | "")}
|
||||
disabled={pendingApproval}
|
||||
options={DEPARTMENT_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.value === "" ? t("comm.selectDepartment") : option.label,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
label={t("agent.status")}
|
||||
value={status}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { BudgetPolicyRecord } from "@shared/types";
|
||||
|
||||
interface BuildBudgetPolicyInputArgs {
|
||||
policyId?: string | null;
|
||||
companyId: string;
|
||||
scopeType: "company" | "agent" | "project";
|
||||
scopeId: string;
|
||||
amountDollars: string;
|
||||
warnPercent: string;
|
||||
hardStopEnabled: boolean;
|
||||
notifyEnabled: boolean;
|
||||
}
|
||||
|
||||
export function buildBudgetPolicyInput(args: BuildBudgetPolicyInputArgs) {
|
||||
const amount = Math.round(Number.parseFloat(args.amountDollars) * 100);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const warnPercent = Number.parseInt(args.warnPercent, 10);
|
||||
if (!Number.isFinite(warnPercent) || warnPercent < 1 || warnPercent > 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: args.policyId ?? null,
|
||||
companyId: args.companyId,
|
||||
scopeType: args.scopeType,
|
||||
scopeId: args.scopeId,
|
||||
metric: "billed_cents" as const,
|
||||
windowKind: "calendar_month_utc" as const,
|
||||
amount,
|
||||
warnPercent,
|
||||
hardStopEnabled: args.hardStopEnabled,
|
||||
notifyEnabled: args.notifyEnabled,
|
||||
isActive: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function getBudgetPolicyAmountDollars(policy: BudgetPolicyRecord | null): string {
|
||||
if (!policy) {
|
||||
return "";
|
||||
}
|
||||
return (policy.amount / 100).toFixed(2);
|
||||
}
|
||||
Reference in new issue
Block a user