update
This commit is contained in:
1 parent
1ba3147ed2
commit
0cd65521b8
32 files changed
+1853
-342
No files matched your search
@@ -27,6 +27,7 @@ import type {
|
||||
DepartmentId,
|
||||
MessageChannel,
|
||||
MessagePriority,
|
||||
ProjectRecord,
|
||||
TaskRecord,
|
||||
} from "@shared/types"
|
||||
import { getAgentInitials, formatMessageTime } from "../lib/formatters"
|
||||
@@ -64,6 +65,8 @@ interface ChannelDescriptor {
|
||||
targetId: string | null
|
||||
/** Agent ID for DM channels */
|
||||
agentId: string | null
|
||||
/** Participant IDs for DM channels */
|
||||
participantIds: string[]
|
||||
}
|
||||
|
||||
interface TaskDiscussionDescriptor {
|
||||
@@ -76,9 +79,11 @@ interface TaskDiscussionDescriptor {
|
||||
function deriveChannels(
|
||||
messages: AgentMessageRecord[],
|
||||
agents: AgentRecord[],
|
||||
projects?: ProjectRecord[],
|
||||
): ChannelDescriptor[] {
|
||||
const channelMap = new Map<string, ChannelDescriptor>()
|
||||
const agentMap = new Map(agents.map((a) => [a.id, a]))
|
||||
const projectMap = new Map((projects ?? []).map((project) => [project.id, project]))
|
||||
|
||||
// Always create company-wide channel
|
||||
channelMap.set("company-all", {
|
||||
@@ -88,6 +93,7 @@ function deriveChannels(
|
||||
label: "#company-wide",
|
||||
targetId: null,
|
||||
agentId: null,
|
||||
participantIds: [],
|
||||
})
|
||||
|
||||
// Create permanent department channels for all departments that have agents
|
||||
@@ -107,6 +113,7 @@ function deriveChannels(
|
||||
label: `#${deptLabel}`,
|
||||
targetId: dept,
|
||||
agentId: null,
|
||||
participantIds: [],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,12 +129,14 @@ function deriveChannels(
|
||||
if (!otherId) {
|
||||
key = `direct-broadcast`
|
||||
label = "Broadcast DMs"
|
||||
agentId = null
|
||||
} else {
|
||||
const ids = [msg.fromAgentId, otherId].sort()
|
||||
key = `direct-${ids.join("-")}`
|
||||
const other = agentMap.get(otherId)
|
||||
label = other ? other.name : "Unknown Agent"
|
||||
agentId = otherId
|
||||
const first = agentMap.get(ids[0] ?? "")
|
||||
const second = agentMap.get(ids[1] ?? "")
|
||||
label = `${first?.name ?? (ids[0] ? ids[0].slice(0, 8) : "Unknown")} ↔ ${second?.name ?? (ids[1] ? ids[1].slice(0, 8) : "Unknown")}`
|
||||
agentId = ids[1] ?? null
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -145,13 +154,13 @@ function deriveChannels(
|
||||
case "project": {
|
||||
const projId = msg.channelTargetId ?? "general"
|
||||
key = `project-${projId}`
|
||||
label = `#project-${projId.slice(0, 8)}`
|
||||
label = projectMap.get(projId)?.name ?? `#project-${projId.slice(0, 8)}`
|
||||
break
|
||||
}
|
||||
case "incident": {
|
||||
const incId = msg.channelTargetId ?? "general"
|
||||
const incId = msg.channelTargetId ?? "room"
|
||||
key = `incident-${incId}`
|
||||
label = `#incident-${incId.slice(0, 8)}`
|
||||
label = incId === "room" ? "#incident-room" : `#incident-${incId.slice(0, 8)}`
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -164,6 +173,9 @@ function deriveChannels(
|
||||
label,
|
||||
targetId: msg.channelTargetId,
|
||||
agentId,
|
||||
participantIds: msg.channel === "direct" && msg.toAgentId
|
||||
? [msg.fromAgentId, msg.toAgentId].sort()
|
||||
: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -180,12 +192,17 @@ function deriveChannels(
|
||||
function deriveTaskDiscussionChannels(
|
||||
comments: CommentRecord[],
|
||||
tasks: TaskRecord[],
|
||||
forcedTaskId?: string | null,
|
||||
): TaskDiscussionDescriptor[] {
|
||||
const taskMap = new Map(tasks.map((task) => [task.id, task]))
|
||||
const activeTaskIds = tasks
|
||||
.filter((task) => ["todo", "in_progress", "in_review", "blocked"].includes(task.status))
|
||||
.map((task) => task.id)
|
||||
const discussionTaskIds = new Set([...comments.map((comment) => comment.taskId), ...activeTaskIds])
|
||||
const discussionTaskIds = new Set([
|
||||
...comments.map((comment) => comment.taskId),
|
||||
...activeTaskIds,
|
||||
...(forcedTaskId ? [forcedTaskId] : []),
|
||||
])
|
||||
|
||||
return Array.from(discussionTaskIds)
|
||||
.map((taskId) => {
|
||||
@@ -227,9 +244,9 @@ function getChannelMessages(
|
||||
}
|
||||
if (channel.channel === "direct" && msg.channel === "direct") {
|
||||
if (channel.key === "direct-broadcast") return !msg.toAgentId
|
||||
if (!channel.agentId) return false
|
||||
const ids = [msg.fromAgentId, msg.toAgentId ?? ""].sort()
|
||||
return channel.key === `direct-${ids.join("-")}`
|
||||
if (!msg.toAgentId || channel.participantIds.length < 2) return false
|
||||
const ids = [msg.fromAgentId, msg.toAgentId].sort()
|
||||
return ids[0] === channel.participantIds[0] && ids[1] === channel.participantIds[1]
|
||||
}
|
||||
return false
|
||||
})
|
||||
@@ -355,22 +372,12 @@ function MessageBubble({
|
||||
isThread ? "ml-11" : ""
|
||||
} ${
|
||||
isUnread
|
||||
? "border-l-[3px] bg-[color:var(--panel)]"
|
||||
: "border-l-[3px] border-l-transparent hover:bg-[color:var(--panel-soft)]/40"
|
||||
? "bg-[color:var(--panel)]"
|
||||
: "hover:bg-[color:var(--panel-soft)]/40"
|
||||
} ${
|
||||
isUrgent
|
||||
? "border-l-[color:var(--danger)]"
|
||||
: isUnread
|
||||
? `border-l-[${TERRACOTTA}]`
|
||||
: ""
|
||||
isUrgent ? "border-l-[3px]" : ""
|
||||
}`}
|
||||
style={
|
||||
isUnread && !isUrgent
|
||||
? { borderLeftColor: TERRACOTTA }
|
||||
: isUrgent
|
||||
? { borderLeftColor: "var(--danger)" }
|
||||
: undefined
|
||||
}
|
||||
style={isUrgent ? { borderLeftColor: "var(--danger)" } : undefined}
|
||||
>
|
||||
<AgentAvatar agent={agent} />
|
||||
|
||||
@@ -610,6 +617,8 @@ interface MessageComposerProps {
|
||||
/** Pre-fill channel context */
|
||||
channel: MessageChannel
|
||||
channelTargetId: string | null
|
||||
/** Participant IDs for a direct channel */
|
||||
directParticipantIds?: string[]
|
||||
/** Pre-fill as a reply to a message */
|
||||
replyTo: AgentMessageRecord | null
|
||||
onSent: () => void
|
||||
@@ -621,6 +630,7 @@ function MessageComposer({
|
||||
companyId,
|
||||
channel,
|
||||
channelTargetId,
|
||||
directParticipantIds = [],
|
||||
replyTo,
|
||||
onSent,
|
||||
onCancelReply,
|
||||
@@ -638,15 +648,41 @@ function MessageComposer({
|
||||
}
|
||||
}, [replyTo])
|
||||
|
||||
useEffect(() => {
|
||||
if (agents.length === 0) {
|
||||
if (fromAgentId) setFromAgentId("")
|
||||
return
|
||||
}
|
||||
if (!agents.some((agent) => agent.id === fromAgentId)) {
|
||||
setFromAgentId(agents[0]?.id ?? "")
|
||||
}
|
||||
}, [agents, fromAgentId])
|
||||
|
||||
const directRecipientId = useMemo(() => {
|
||||
if (channel !== "direct") return null
|
||||
if (replyTo?.fromAgentId) return replyTo.fromAgentId
|
||||
if (directParticipantIds.length === 0) return null
|
||||
if (directParticipantIds.length === 1) return directParticipantIds[0] ?? null
|
||||
if (fromAgentId && directParticipantIds.includes(fromAgentId)) {
|
||||
return directParticipantIds.find((agentId) => agentId !== fromAgentId) ?? null
|
||||
}
|
||||
return directParticipantIds[0] ?? null
|
||||
}, [channel, replyTo, directParticipantIds, fromAgentId])
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (!body.trim() || !fromAgentId) return
|
||||
if (channel === "direct" && !directRecipientId) return
|
||||
if (!agents.some((agent) => agent.id === fromAgentId)) {
|
||||
setSendError("Select a valid sender from the current company before sending.")
|
||||
return
|
||||
}
|
||||
setSending(true)
|
||||
setSendError(null)
|
||||
try {
|
||||
unwrap(await window.agentCompany.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId,
|
||||
toAgentId: replyTo ? replyTo.fromAgentId : null,
|
||||
toAgentId: channel === "direct" ? directRecipientId : null,
|
||||
channel,
|
||||
channelTargetId,
|
||||
subject: subject.trim() || (replyTo ? `Re: ${replyTo.subject}` : ""),
|
||||
@@ -663,7 +699,7 @@ function MessageComposer({
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}, [body, fromAgentId, companyId, channel, channelTargetId, replyTo, subject, onSent])
|
||||
}, [agents, body, channel, channelTargetId, companyId, directRecipientId, fromAgentId, onSent, replyTo, subject])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
@@ -680,6 +716,11 @@ function MessageComposer({
|
||||
return (
|
||||
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
|
||||
{sendError ? <div className="mb-3"><InlineNotice message={sendError} /></div> : null}
|
||||
{channel === "direct" && !directRecipientId ? (
|
||||
<div className="mb-2">
|
||||
<InlineNotice message="Select a direct conversation partner before sending a DM." />
|
||||
</div>
|
||||
) : null}
|
||||
<AnimatePresence>
|
||||
{replyTo ? (
|
||||
<motion.div
|
||||
@@ -742,7 +783,7 @@ function MessageComposer({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleSend()}
|
||||
disabled={!body.trim() || !fromAgentId || sending}
|
||||
disabled={!body.trim() || !fromAgentId || sending || (channel === "direct" && !directRecipientId)}
|
||||
className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-[8px] text-white transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none"
|
||||
style={{ backgroundColor: TERRACOTTA }}
|
||||
aria-label="Send message"
|
||||
@@ -768,11 +809,12 @@ interface ComposeModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
agents: AgentRecord[]
|
||||
projects?: ProjectRecord[]
|
||||
companyId: string
|
||||
onSent: () => void
|
||||
}
|
||||
|
||||
function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModalProps) {
|
||||
function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: ComposeModalProps) {
|
||||
const [fromAgentId, setFromAgentId] = useState(agents[0]?.id ?? "")
|
||||
const [toAgentId, setToAgentId] = useState("")
|
||||
const [channel, setChannel] = useState<MessageChannel>("company")
|
||||
@@ -791,8 +833,38 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
|
||||
return Array.from(depts).sort()
|
||||
}, [agents])
|
||||
|
||||
useEffect(() => {
|
||||
if (channel !== "direct") {
|
||||
setToAgentId("")
|
||||
}
|
||||
if (channel !== "department" && channel !== "project") {
|
||||
setChannelTargetId("")
|
||||
}
|
||||
}, [channel])
|
||||
|
||||
useEffect(() => {
|
||||
if (agents.length === 0) {
|
||||
if (fromAgentId) setFromAgentId("")
|
||||
return
|
||||
}
|
||||
if (!agents.some((agent) => agent.id === fromAgentId)) {
|
||||
setFromAgentId(agents[0]?.id ?? "")
|
||||
}
|
||||
}, [agents, fromAgentId])
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (!body.trim() || !fromAgentId || !subject.trim() || (channel === "direct" && !toAgentId)) return
|
||||
const requiresTarget = channel === "department" || channel === "project"
|
||||
if (
|
||||
!body.trim()
|
||||
|| !fromAgentId
|
||||
|| !subject.trim()
|
||||
|| (channel === "direct" && !toAgentId)
|
||||
|| (requiresTarget && !channelTargetId)
|
||||
) return
|
||||
if (!agents.some((agent) => agent.id === fromAgentId)) {
|
||||
setSendError("Select a valid sender from the current company before sending.")
|
||||
return
|
||||
}
|
||||
setSending(true)
|
||||
setSendError(null)
|
||||
try {
|
||||
@@ -801,7 +873,7 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
|
||||
fromAgentId,
|
||||
toAgentId: channel === "direct" && toAgentId ? toAgentId : null,
|
||||
channel,
|
||||
channelTargetId: channelTargetId || null,
|
||||
channelTargetId: channel === "department" || channel === "project" ? channelTargetId || null : null,
|
||||
subject: subject.trim(),
|
||||
body: body.trim(),
|
||||
priority,
|
||||
@@ -818,7 +890,7 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}, [body, fromAgentId, toAgentId, channel, channelTargetId, subject, priority, companyId, onSent, onClose])
|
||||
}, [agents, body, channel, channelTargetId, companyId, fromAgentId, onClose, onSent, priority, subject, toAgentId])
|
||||
|
||||
return (
|
||||
<Modal open={open} title="Compose Message" onClose={onClose} width="560px">
|
||||
@@ -875,12 +947,18 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
) : channel === "project" || channel === "incident" ? (
|
||||
<Input
|
||||
label={channel === "project" ? "Project ID" : "Incident ID"}
|
||||
) : channel === "project" ? (
|
||||
<Select
|
||||
label="Project"
|
||||
value={channelTargetId}
|
||||
onChange={setChannelTargetId}
|
||||
placeholder={`Enter ${channel} identifier`}
|
||||
options={[
|
||||
{ value: "", label: "Select project..." },
|
||||
...(projects ?? []).map((p) => ({
|
||||
value: p.id,
|
||||
label: p.name,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -921,7 +999,14 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleSend()}
|
||||
disabled={!body.trim() || !subject.trim() || !fromAgentId || (channel === "direct" && !toAgentId) || sending}
|
||||
disabled={
|
||||
!body.trim()
|
||||
|| !subject.trim()
|
||||
|| !fromAgentId
|
||||
|| (channel === "direct" && !toAgentId)
|
||||
|| ((channel === "department" || channel === "project") && !channelTargetId)
|
||||
|| sending
|
||||
}
|
||||
className="inline-flex items-center gap-2 rounded-[8px] px-4 py-2 text-[12px] font-semibold text-white transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none"
|
||||
style={{ backgroundColor: TERRACOTTA }}
|
||||
>
|
||||
@@ -1084,6 +1169,7 @@ interface CommunicationHubProps {
|
||||
comments: CommentRecord[]
|
||||
tasks: TaskRecord[]
|
||||
agents: AgentRecord[]
|
||||
projects?: ProjectRecord[]
|
||||
companyId: string
|
||||
selectedEntityId?: string | null
|
||||
onEntityConsumed?: () => void
|
||||
@@ -1096,6 +1182,7 @@ export function CommunicationHub({
|
||||
comments,
|
||||
tasks,
|
||||
agents,
|
||||
projects,
|
||||
companyId,
|
||||
selectedEntityId,
|
||||
onEntityConsumed,
|
||||
@@ -1136,8 +1223,15 @@ export function CommunicationHub({
|
||||
}, [effectiveMessages])
|
||||
|
||||
// Derive channels from messages
|
||||
const messageChannels = useMemo(() => deriveChannels(effectiveMessages, agents), [effectiveMessages, agents])
|
||||
const taskDiscussionChannels = useMemo(() => deriveTaskDiscussionChannels(comments, tasks), [comments, tasks])
|
||||
const messageChannels = useMemo(() => deriveChannels(effectiveMessages, agents, projects), [effectiveMessages, agents, projects])
|
||||
const forcedTaskDiscussionId = useMemo(
|
||||
() => (selectedEntityId && tasks.some((task) => task.id === selectedEntityId) ? selectedEntityId : null),
|
||||
[selectedEntityId, tasks],
|
||||
)
|
||||
const taskDiscussionChannels = useMemo(
|
||||
() => deriveTaskDiscussionChannels(comments, tasks, forcedTaskDiscussionId),
|
||||
[comments, forcedTaskDiscussionId, tasks],
|
||||
)
|
||||
const channels = useMemo(() => [...messageChannels, ...taskDiscussionChannels], [messageChannels, taskDiscussionChannels])
|
||||
|
||||
// Auto-select first valid channel
|
||||
@@ -1633,6 +1727,7 @@ export function CommunicationHub({
|
||||
companyId={companyId}
|
||||
channel={activeChannel.channel}
|
||||
channelTargetId={activeChannel.channel === "direct" ? null : activeChannel.targetId}
|
||||
directParticipantIds={activeChannel.channel === "direct" ? activeChannel.participantIds : []}
|
||||
replyTo={replyTo}
|
||||
onSent={() => {
|
||||
setReplyTo(null)
|
||||
@@ -1662,6 +1757,7 @@ export function CommunicationHub({
|
||||
open={showCompose}
|
||||
onClose={() => setShowCompose(false)}
|
||||
agents={agents}
|
||||
projects={projects}
|
||||
companyId={companyId}
|
||||
onSent={onRefresh}
|
||||
/>
|
||||
|
||||
@@ -641,7 +641,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
value={liveRuns.length}
|
||||
icon={Heartbeat}
|
||||
tone={liveRuns.length > 0 ? "success" : "default"}
|
||||
onClick={() => onNavigate("tasks")}
|
||||
onClick={() => onNavigate("runs")}
|
||||
subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} running` : "All quiet"}
|
||||
/>
|
||||
<MetricCard
|
||||
@@ -665,7 +665,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
value={failedRuns.length}
|
||||
icon={Warning}
|
||||
tone={failedRuns.length > 0 ? "danger" : "default"}
|
||||
onClick={() => onNavigate("tasks")}
|
||||
onClick={() => onNavigate("runs")}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Total Spend"
|
||||
|
||||
@@ -1486,8 +1486,8 @@ export function SettingsPane({
|
||||
setActionNotice(null);
|
||||
setLoadingAction("check");
|
||||
try {
|
||||
unwrap(await window.agentCompany.checkForUpdates());
|
||||
setActionNotice("Update check started.");
|
||||
const started = unwrap(await window.agentCompany.checkForUpdates());
|
||||
setActionNotice(started ? "Update check started." : "Update check skipped because no release update feed is configured.");
|
||||
} catch (error) {
|
||||
setActionError(describeActionError(error));
|
||||
} finally {
|
||||
|
||||
@@ -148,6 +148,7 @@ export function AgentCreator({
|
||||
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";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -220,19 +221,25 @@ export function AgentCreator({
|
||||
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="Create agent"
|
||||
label="Submit hire request"
|
||||
tone="accent"
|
||||
onClick={() =>
|
||||
void (async () => {
|
||||
try {
|
||||
setRequestError(null);
|
||||
const meta: Record<string, string> = {};
|
||||
if (thinkingEffort) meta.thinkingEffort = thinkingEffort;
|
||||
await window.agentCompany.saveAgent({
|
||||
unwrap(await window.agentCompany.requestHire({
|
||||
companyId,
|
||||
requestedByAgentId: null,
|
||||
name,
|
||||
role,
|
||||
title,
|
||||
department: null,
|
||||
connectorId: connectorId as ConnectorRecord["id"],
|
||||
workspaceId: workspaceId || null,
|
||||
reportsTo: reportsTo || null,
|
||||
@@ -240,21 +247,22 @@ export function AgentCreator({
|
||||
metadataJson: JSON.stringify(meta),
|
||||
capabilities,
|
||||
budgetMonthlyUsd: Number(budget || 0),
|
||||
status: "idle",
|
||||
});
|
||||
}));
|
||||
setName("");
|
||||
setRole("");
|
||||
setTitle("");
|
||||
setCapabilities("");
|
||||
setReportsTo("");
|
||||
setRequestError(null);
|
||||
await onSaved();
|
||||
} catch (error) {
|
||||
void error;
|
||||
setRequestError(error instanceof Error ? error.message : "Hire request failed.");
|
||||
}
|
||||
})()
|
||||
}
|
||||
icon={Robot}
|
||||
/>
|
||||
{requestError ? <p className="text-[12px] text-[color:var(--danger)]">{requestError}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -292,6 +300,7 @@ export function AgentEditor({
|
||||
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";
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
@@ -314,27 +323,35 @@ export function AgentEditor({
|
||||
|
||||
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="Role" />
|
||||
<Input label="Title" value={title} onChange={setTitle} placeholder="Title" />
|
||||
<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"])}
|
||||
options={[
|
||||
{ value: "idle", label: "Idle" },
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "running", label: "Running" },
|
||||
{ value: "paused", label: "Paused" },
|
||||
{ value: "error", label: "Error" },
|
||||
{ value: "pending_approval", label: "Pending Approval" },
|
||||
{ value: "terminated", label: "Terminated" },
|
||||
]}
|
||||
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={connectors.map((c) => ({ value: c.id, label: c.label }))}
|
||||
/>
|
||||
{availableModels.length > 0 ? (
|
||||
@@ -342,6 +359,7 @@ export function AgentEditor({
|
||||
label="Model"
|
||||
value={model}
|
||||
onChange={setModel}
|
||||
disabled={pendingApproval}
|
||||
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
|
||||
/>
|
||||
) : null}
|
||||
@@ -350,6 +368,7 @@ export function AgentEditor({
|
||||
label="Thinking effort"
|
||||
value={thinkingEffort}
|
||||
onChange={setThinkingEffort}
|
||||
disabled={pendingApproval}
|
||||
options={[
|
||||
{ value: "", label: "Default" },
|
||||
{ value: "low", label: "Low" },
|
||||
@@ -362,6 +381,7 @@ export function AgentEditor({
|
||||
label="Workspace"
|
||||
value={workspaceId}
|
||||
onChange={setWorkspaceId}
|
||||
disabled={pendingApproval}
|
||||
options={[
|
||||
{ value: "", label: "No workspace" },
|
||||
...workspaces.map((w) => ({ value: w.id, label: w.name })),
|
||||
@@ -371,13 +391,14 @@ export function AgentEditor({
|
||||
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" />
|
||||
<TextArea label="Capabilities" value={capabilities} onChange={setCapabilities} placeholder="Capabilities" />
|
||||
<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} />
|
||||
|
||||
{/* Heartbeat / Continuous Execution */}
|
||||
<div className="rounded-lg border border-[color:var(--line)] p-3">
|
||||
@@ -390,6 +411,7 @@ export function AgentEditor({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agent.heartbeatEnabled}
|
||||
disabled={pendingApproval}
|
||||
onChange={(e) =>
|
||||
void (async () => {
|
||||
await window.agentCompany.setHeartbeat({
|
||||
@@ -427,6 +449,7 @@ export function AgentEditor({
|
||||
await onSaved();
|
||||
})()
|
||||
}
|
||||
disabled={pendingApproval}
|
||||
options={[
|
||||
{ value: "30", label: "30 seconds" },
|
||||
{ value: "60", label: "1 minute" },
|
||||
@@ -443,6 +466,7 @@ export function AgentEditor({
|
||||
label="Wake now"
|
||||
tone="accent"
|
||||
icon={Lightning}
|
||||
disabled={pendingApproval}
|
||||
onClick={() =>
|
||||
void (async () => {
|
||||
await window.agentCompany.triggerHeartbeat({
|
||||
@@ -462,6 +486,7 @@ export function AgentEditor({
|
||||
label="Save changes"
|
||||
tone="accent"
|
||||
icon={Pencil}
|
||||
disabled={pendingApproval}
|
||||
onClick={() =>
|
||||
void (async () => {
|
||||
setSaveError(null);
|
||||
@@ -507,10 +532,16 @@ export function AgentEditor({
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -208,19 +208,25 @@ export function TextArea({
|
||||
placeholder,
|
||||
rows = 4,
|
||||
label,
|
||||
readOnly = false,
|
||||
disabled = false,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
label?: string;
|
||||
readOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const textarea = (
|
||||
<textarea
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
value={value}
|
||||
rows={rows}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
);
|
||||
@@ -241,12 +247,14 @@ export function Select({
|
||||
options,
|
||||
compact = false,
|
||||
label,
|
||||
disabled = false,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: Array<{ value: string; label: string }>;
|
||||
compact?: boolean;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const select = (
|
||||
<select
|
||||
@@ -254,8 +262,9 @@ export function Select({
|
||||
compact
|
||||
? "rounded-[6px] border-[color:var(--line)] px-2.5 py-1.5 text-[13px] font-medium"
|
||||
: "rounded-[8px] border-[color:var(--line)] px-3.5 py-2.5 text-[14px]"
|
||||
}`}
|
||||
} disabled:cursor-not-allowed disabled:opacity-60`}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
|
||||
Reference in new issue
Block a user