refactor(execution): split ApprovalInspector 346-line god-component

Decompose monolithic src/renderer/components/execution/ApprovalInspector.tsx
into src/renderer/components/execution/approval-inspector/:
- ApprovalInspector.tsx (168 lines, orchestrator only)
- ApprovalSummary.tsx, ProposedHireCard.tsx, ApprovalDocumentsPanel.tsx
- DeliverableReviewPanel.tsx, SchedulePicker.tsx, DecisionForm.tsx
- TypeBadge.tsx, useApprovalPayload.ts, index.ts (barrel)

Hoist JSON.parse out of JSX IIFE into useApprovalPayload hook with typed
return. Replace roll-your-own role=radio spans with native
input[type=radio]. Extract getTypeBadgeClass switch into TypeBadge
component. Update execution.tsx barrel to re-export from new path.

Business logic preserved: approval decision flow, schedule validation,
deliverable review, document editing, delete confirmation.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 22:06:39 -04:00
1 parent cc02b45864
commit 0f1b313272
12 files changed
+763 -347

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@ export { TaskQueue } from "./execution/TaskQueue"
export { TaskInspector } from "./execution/TaskInspector"
export { TaskCreator } from "./execution/TaskCreator"
export { ApprovalList } from "./execution/ApprovalList"
export { ApprovalInspector } from "./execution/ApprovalInspector"
export { ApprovalInspector } from "./execution/approval-inspector"
export { ActivityList } from "./execution/ActivityList"
export { CostList } from "./execution/CostList"
export { BudgetSummary } from "./execution/BudgetSummary"
@@ -1,346 +0,0 @@
import { useMemo, useState } from "react"
import { Briefcase, CheckCircle, Clock, ClockCounterClockwise, Robot, Trash, XCircle } from "@phosphor-icons/react"
import { CaretDown, CaretUp } from "@phosphor-icons/react"
import type { AgentRecord, ApprovalRecord, DocumentRecord, TaskRecord } from "@shared/types"
import { extractApprovalDocumentReferences, type ApprovalDocumentReference } from "@shared/approval-documents"
import { useT } from "../../i18n"
import { formatMoney, formatTime } from "../../lib/formatters"
import { unwrap } from "../../lib/desktop"
import { ActionButton, ConfirmDialog, InlineNotice, Input, StatusPill, TextArea } from "../ui"
import { describeActionError, parseApprovalPayloadJson } from "./execution-utils"
import { SocialDraftPreview } from "./SocialDraftPreview"
import { useAppStore } from "../../state/store"
function getTypeBadgeClass(type: string): string {
if (type === "hire_agent") return "bg-accent text-primary"
if (type === "approve_ceo_strategy") return "bg-warning-muted text-warning"
if (type === "secret_access") return "bg-destructive-muted text-destructive"
if (type === "social_post") return "bg-accent text-primary"
if (type === "deliverable_review") return "bg-accent text-primary"
return "bg-muted text-muted-foreground"
}
function ApprovalDocumentEditor({
companyId,
document,
fallback,
}: {
companyId: string
document: DocumentRecord
fallback?: ApprovalDocumentReference
}) {
const addToast = useAppStore((s) => s.addToast)
const [title, setTitle] = useState(document.title)
const [content, setContent] = useState(document.content ?? "")
const [saving, setSaving] = useState(false)
const [saveError, setSaveError] = useState<string | null>(null)
async function handleSave() {
if (!title.trim()) {
setSaveError("Document title is required.")
return
}
setSaving(true)
setSaveError(null)
try {
unwrap(await window.agentCompany.saveDocument({
id: document.id,
companyId,
type: document.type,
title: title.trim(),
content,
authorAgentId: document.authorAgentId ?? null,
reviewerAgentId: document.reviewerAgentId ?? null,
projectId: document.projectId ?? null,
goalId: document.goalId ?? null,
parentDocId: document.parentDocId ?? null,
version: document.version,
status: document.status,
tagsJson: document.tagsJson,
}))
addToast(`Saved document: ${title.trim()}`, "success")
} catch (error) {
setSaveError(describeActionError(error))
} finally {
setSaving(false)
}
}
return (
<div className="space-y-3 rounded-lg border border-border px-4 py-3">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-[13px] font-medium text-foreground">{fallback?.title ?? document.title}</div>
<div className="text-[11px] text-muted-foreground">{(fallback?.type ?? document.type).replaceAll("_", " ")} · v{document.version} · {document.status}</div>
</div>
<ActionButton label={saving ? "Saving..." : "Save changes"} icon={CheckCircle} tone="accent" onClick={() => void handleSave()} loading={saving} />
</div>
{saveError ? <InlineNotice message={saveError} /> : null}
<Input label="Title" value={title} onChange={setTitle} placeholder="Document title" />
<TextArea label="Content" value={content} onChange={setContent} rows={12} placeholder="Document content" />
</div>
)
}
function ApprovalDocumentsPanel({
approval,
documents,
companyId,
}: {
approval: ApprovalRecord
documents?: DocumentRecord[]
companyId: string
}) {
const references = useMemo(
() => extractApprovalDocumentReferences(approval.payloadJson),
[approval.payloadJson],
)
if (references.length === 0) return null
const fullDocs = documents
? references
.map((reference) => ({
reference,
document: documents.find((entry) => entry.id === reference.id) ?? null,
}))
.filter((entry): entry is typeof entry & { document: NonNullable<typeof entry.document> } => entry.document != null)
: []
return (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Linked Documents</div>
{fullDocs.length > 0 ? fullDocs.map(({ reference, document }) => (
<ApprovalDocumentEditor
key={document.id}
companyId={companyId}
document={document}
fallback={reference}
/>
)) : references.map((reference) => (
<div key={reference.id} className="rounded-lg border border-border bg-muted px-4 py-3">
<div className="text-[13px] font-medium text-foreground">{reference.title ?? reference.id}</div>
<div className="text-[11px] text-muted-foreground">{reference.type?.replaceAll("_", " ") ?? "document"} · {reference.id}</div>
<div className="mt-2 text-[12px] text-muted-foreground">The linked document is not loaded in the current snapshot yet. Reopen the approval after the next refresh.</div>
</div>
))}
{approval.impactSummary ? <div className="text-[11px] text-muted-foreground">{approval.impactSummary}</div> : null}
</div>
)
}
export function ApprovalInspector({ approval, agents, tasks, documents, companyId, onDecide, onDeleted }: {
approval: ApprovalRecord; agents: AgentRecord[]; tasks: TaskRecord[]; documents?: DocumentRecord[]; companyId: string
onDecide: (state: "approved" | "rejected" | "revision_requested", note?: string) => Promise<void>; onDeleted: () => Promise<void>
}) {
const t = useT()
const addToast = useAppStore((s) => s.addToast)
const [decisionNote, setDecisionNote] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false)
const [actionError, setActionError] = useState<string | null>(null); const [acting, setActing] = useState<"approved"|"rejected"|"revision_requested"|"delete"|null>(null)
const [scheduleMode, setScheduleMode] = useState<"immediate" | "scheduled">("immediate")
const [scheduledAt, setScheduledAt] = useState("")
const [scheduleError, setScheduleError] = useState<string | null>(null)
const requestingAgent = agents.find((a) => a.id === approval.requestedByAgentId) ?? null
const relatedTask = tasks.find((t) => t.id === approval.relatedTaskId) ?? null
const isDecided = approval.state !== "pending"
const isSocialPost = approval.type === "social_post"
async function handleDelete() { setActionError(null); setActing("delete"); try { unwrap(await window.agentCompany.deleteApproval({ id: approval.id, companyId })); setConfirmDelete(false); await onDeleted() } catch (e) { setActionError(describeActionError(e)) } finally { setActing(null) } }
async function handleDecision(state: "approved"|"rejected"|"revision_requested") {
setActionError(null)
setScheduleError(null)
// Validate schedule if social_post and user picked "scheduled"
if (isSocialPost && state === "approved" && scheduleMode === "scheduled") {
if (!scheduledAt) {
setScheduleError("Please select a date and time for scheduling.")
return
}
const chosenTime = new Date(scheduledAt).getTime()
if (chosenTime <= Date.now()) {
setScheduleError("Scheduled time must be in the future.")
return
}
}
setActing(state)
try {
// For social_post approvals with a schedule, include the schedule info in the decision note
let noteForDecision = decisionNote || undefined
if (isSocialPost && state === "approved" && scheduleMode === "scheduled" && scheduledAt) {
const scheduleInfo = `[scheduled:${new Date(scheduledAt).toISOString()}]`
noteForDecision = noteForDecision ? `${noteForDecision} ${scheduleInfo}` : scheduleInfo
}
await onDecide(state, noteForDecision)
if (state === "approved") addToast("Approval granted — agent is moving forward", "success")
else if (state === "rejected") addToast("Approval rejected", "warn")
else if (state === "revision_requested") addToast("Revision requested — agent will revise and resubmit", "warn")
} catch (e) {
setActionError(describeActionError(e))
} finally {
setActing(null)
}
}
// Compute minimum datetime for schedule picker: now + 5 minutes
const minDatetime = (() => {
const d = new Date(Date.now() + 5 * 60 * 1000)
// Format as YYYY-MM-DDTHH:mm for datetime-local input
const pad = (n: number) => String(n).padStart(2, "0")
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
})()
const socialPlatform = (() => {
if (!isSocialPost || !approval.payloadJson) return "other"
try { return (JSON.parse(approval.payloadJson) as { platform?: string }).platform ?? "other" } catch { return "other" }
})()
return (
<div className="space-y-6">
<ConfirmDialog open={confirmDelete} title={t("exec.deleteApproval")} message={t("exec.deleteApprovalConfirm")} errorMessage={actionError} onConfirm={() => void handleDelete()} onCancel={() => { setActionError(null); setConfirmDelete(false) }} />
{actionError && !confirmDelete ? <InlineNotice message={actionError} /> : null}
<div className="border-y border-border py-4 space-y-4">
<div className="flex items-center gap-3">
<span className={`rounded-full px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] ${getTypeBadgeClass(approval.type)}`}>{approval.type.replaceAll("_", " ")}</span>
<StatusPill status={approval.state} />
</div>
<div className="text-[13px] font-medium text-foreground">{approval.payloadSummary}</div>
{approval.impactSummary ? <div className="text-[13px] text-muted-foreground">{approval.impactSummary}</div> : null}
<div className="grid gap-2 text-[13px] md:grid-cols-2">
<div className="flex items-center gap-2 text-muted-foreground"><Robot size={14} />Requested by: <span className="font-medium text-foreground">{requestingAgent?.name ?? "Unknown"}</span>{requestingAgent?.role ? <span className="text-[10px] text-muted-foreground">({requestingAgent.role})</span> : null}</div>
{relatedTask ? <div className="flex items-center gap-2 text-muted-foreground"><Briefcase size={14} />Task: <span className="font-medium text-foreground">{relatedTask.title}</span></div> : null}
<div className="text-muted-foreground">Created: <span className="font-mono text-foreground">{formatTime(approval.createdAt)}</span></div>
{approval.updatedAt !== approval.createdAt ? <div className="text-muted-foreground">Updated: <span className="font-mono text-foreground">{formatTime(approval.updatedAt)}</span></div> : null}
</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 ?? (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}
<div><span className="text-muted-foreground">Connector:</span> <span className="font-mono text-foreground">{proposedAgent?.connectorId ?? (typeof reqSnap?.connectorId === "string" ? reqSnap.connectorId : "Unknown")}</span></div>
<div><span className="text-muted-foreground">Budget:</span> <span className="font-mono text-foreground">{formatMoney(proposedAgent?.budgetMonthlyUsd ?? (typeof reqSnap?.budgetMonthlyUsd === "number" ? reqSnap.budgetMonthlyUsd : 0))}/mo</span></div>
{mgr ? <div><span className="text-muted-foreground">Reports to:</span> <span className="text-foreground">{mgr.name}</span></div> : null}
</div></div>) })() : null}
{approval.type === "approve_ceo_strategy" ? <div className="rounded-lg border border-[color:var(--destructive)] border-opacity-30 bg-warning-muted px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-warning">Organization Strategy</div><div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">{approval.payloadSummary}</div></div> : null}
<ApprovalDocumentsPanel approval={approval} documents={documents} companyId={companyId} />
{approval.type === "deliverable_review" && !extractApprovalDocumentReferences(approval.payloadJson).length ? <DeliverableReviewPanel approval={approval} documents={documents} agents={agents} /> : null}
{isSocialPost && approval.payloadJson ? (
<SocialDraftPreview
payloadJson={approval.payloadJson}
platform={socialPlatform}
isPending={approval.state === "pending"}
/>
) : null}
</div>
{/* Schedule picker for social_post approvals */}
{isSocialPost && !isDecided ? (
<div className="space-y-3">
<div role="radiogroup" aria-label="Post timing" className="space-y-2">
<label className="flex cursor-pointer items-center gap-2">
<span
role="radio"
aria-checked={scheduleMode === "immediate"}
tabIndex={0}
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 ${scheduleMode === "immediate" ? "border-[color:var(--primary)]" : "border-border"}`}
onClick={() => { setScheduleMode("immediate"); setScheduleError(null) }}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { setScheduleMode("immediate"); setScheduleError(null) } }}
>
{scheduleMode === "immediate" ? <span className="h-2 w-2 rounded-full bg-primary" aria-hidden="true" /> : null}
</span>
<span className="text-[13px] font-medium text-foreground">Post immediately</span>
</label>
<label className="flex cursor-pointer items-center gap-2">
<span
role="radio"
aria-checked={scheduleMode === "scheduled"}
tabIndex={0}
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 ${scheduleMode === "scheduled" ? "border-[color:var(--primary)]" : "border-border"}`}
onClick={() => setScheduleMode("scheduled")}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") setScheduleMode("scheduled") }}
>
{scheduleMode === "scheduled" ? <span className="h-2 w-2 rounded-full bg-primary" aria-hidden="true" /> : null}
</span>
<span className="text-[13px] font-medium text-foreground">Schedule for later</span>
</label>
</div>
{scheduleMode === "scheduled" ? (
<div className="flex items-center gap-2">
<Clock size={16} className="text-muted-foreground" />
<input
type="datetime-local"
value={scheduledAt}
min={minDatetime}
onChange={(e) => { setScheduledAt(e.target.value); setScheduleError(null) }}
className="rounded-lg border border-border bg-card px-3.5 py-2.5 text-[13px] text-foreground focus:border-[color:var(--primary)] focus:outline-none"
/>
</div>
) : null}
{scheduleError ? <InlineNotice message={scheduleError} /> : null}
</div>
) : null}
{!isDecided ? <div><label className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Decision note</label><TextArea value={decisionNote} onChange={setDecisionNote} placeholder="Add a note (optional)" rows={3} /></div> : null}
<div className="flex flex-wrap gap-3">
{!isDecided ? <>
<ActionButton
label={isSocialPost ? "Approve & Post" : t("exec.approve")}
tone="accent"
onClick={() => void handleDecision("approved")}
icon={CheckCircle}
loading={acting === "approved"}
/>
<ActionButton label={t("exec.requestRevision")} onClick={() => void handleDecision("revision_requested")} icon={ClockCounterClockwise} loading={acting === "revision_requested"} />
<ActionButton label={t("exec.reject")} tone="danger" onClick={() => void handleDecision("rejected")} icon={XCircle} loading={acting === "rejected"} />
</> : null}
{isDecided ? <ActionButton label="Delete" onClick={() => setConfirmDelete(true)} icon={Trash} tone="danger" loading={acting === "delete"} /> : null}
</div>
{approval.decisionNote ? <div className="border-t border-border pt-4 text-[13px] text-muted-foreground">Last note: {approval.decisionNote}</div> : null}
</div>
)
}
function DeliverableReviewPanel({ approval, documents, agents }: { approval: ApprovalRecord; documents?: DocumentRecord[]; agents: AgentRecord[] }) {
const payload = parseApprovalPayloadJson(approval)
const docRefs = Array.isArray(payload?.documents) ? payload.documents as Array<{ id: string; type: string; title: string }> : []
const docIds = Array.isArray(payload?.documentIds) ? payload.documentIds as string[] : docRefs.map((d) => d.id)
const fullDocs = documents ? docIds.map((id) => documents.find((d) => d.id === id)).filter(Boolean) as DocumentRecord[] : []
const [expandedDocId, setExpandedDocId] = useState<string | null>(fullDocs.length === 1 ? fullDocs[0]!.id : null)
return (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Deliverable Review</div>
{fullDocs.length > 0 ? fullDocs.map((doc) => {
const isExpanded = expandedDocId === doc.id
const author = doc.authorAgentId ? agents.find((a) => a.id === doc.authorAgentId) : null
return (
<div key={doc.id} className="rounded-lg border border-border overflow-hidden">
<button
type="button"
onClick={() => setExpandedDocId(isExpanded ? null : doc.id)}
className="flex w-full cursor-pointer items-center gap-3 bg-muted px-4 py-3 text-left transition hover:bg-card"
>
<span className="shrink-0 rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">{doc.type.replaceAll("_", " ")}</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-foreground">{doc.title}</span>
{isExpanded ? <CaretUp size={14} className="shrink-0 text-muted-foreground" /> : <CaretDown size={14} className="shrink-0 text-muted-foreground" />}
</button>
{isExpanded ? (
<div className="border-t border-border px-4 py-3">
{author ? <div className="mb-2 text-[11px] text-muted-foreground">Author: <span className="font-medium text-foreground">{author.name}</span> ({author.role})</div> : null}
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">{doc.content || "No content."}</div>
</div>
) : null}
</div>
)
}) : docRefs.length > 0 ? docRefs.map((doc) => (
<div key={doc.id} className="flex items-center gap-2 rounded-lg border border-border bg-muted px-4 py-3 text-[12px]">
<span className="rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">{doc.type.replaceAll("_", " ")}</span>
<span className="font-medium text-foreground">{doc.title}</span>
</div>
)) : <div className="text-[12px] text-muted-foreground">{approval.payloadSummary}</div>}
{approval.impactSummary ? <div className="text-[11px] text-muted-foreground">{approval.impactSummary}</div> : null}
</div>
)
}
@@ -0,0 +1,138 @@
import { useMemo, useState } from "react"
import { CheckCircle } from "@phosphor-icons/react"
import type { ApprovalRecord, DocumentRecord } from "@shared/types"
import { extractApprovalDocumentReferences, type ApprovalDocumentReference } from "@shared/approval-documents"
import { unwrap } from "../../../lib/desktop"
import { useAppStore } from "../../../state/store"
import { ActionButton, InlineNotice, Input, TextArea } from "../../ui"
import { describeActionError } from "../execution-utils"
function ApprovalDocumentEditor({
companyId,
document,
fallback,
}: {
companyId: string
document: DocumentRecord
fallback?: ApprovalDocumentReference
}) {
const addToast = useAppStore((s) => s.addToast)
const [title, setTitle] = useState(document.title)
const [content, setContent] = useState(document.content ?? "")
const [saving, setSaving] = useState(false)
const [saveError, setSaveError] = useState<string | null>(null)
async function handleSave() {
if (!title.trim()) {
setSaveError("Document title is required.")
return
}
setSaving(true)
setSaveError(null)
try {
unwrap(
await window.agentCompany.saveDocument({
id: document.id,
companyId,
type: document.type,
title: title.trim(),
content,
authorAgentId: document.authorAgentId ?? null,
reviewerAgentId: document.reviewerAgentId ?? null,
projectId: document.projectId ?? null,
goalId: document.goalId ?? null,
parentDocId: document.parentDocId ?? null,
version: document.version,
status: document.status,
tagsJson: document.tagsJson,
}),
)
addToast(`Saved document: ${title.trim()}`, "success")
} catch (error) {
setSaveError(describeActionError(error))
} finally {
setSaving(false)
}
}
return (
<div className="space-y-3 rounded-lg border border-border px-4 py-3">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-[13px] font-medium text-foreground">{fallback?.title ?? document.title}</div>
<div className="text-[11px] text-muted-foreground">
{(fallback?.type ?? document.type).replaceAll("_", " ")} · v{document.version} · {document.status}
</div>
</div>
<ActionButton
label={saving ? "Saving..." : "Save changes"}
icon={CheckCircle}
tone="accent"
onClick={() => void handleSave()}
loading={saving}
/>
</div>
{saveError ? <InlineNotice message={saveError} /> : null}
<Input label="Title" value={title} onChange={setTitle} placeholder="Document title" />
<TextArea label="Content" value={content} onChange={setContent} rows={12} placeholder="Document content" />
</div>
)
}
export function ApprovalDocumentsPanel({
approval,
documents,
companyId,
}: {
approval: ApprovalRecord
documents?: DocumentRecord[]
companyId: string
}) {
const references = useMemo(
() => extractApprovalDocumentReferences(approval.payloadJson),
[approval.payloadJson],
)
if (references.length === 0) return null
const fullDocs = documents
? references
.map((reference) => ({
reference,
document: documents.find((entry) => entry.id === reference.id) ?? null,
}))
.filter(
(entry): entry is typeof entry & { document: NonNullable<typeof entry.document> } =>
entry.document != null,
)
: []
return (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Linked Documents</div>
{fullDocs.length > 0
? fullDocs.map(({ reference, document }) => (
<ApprovalDocumentEditor
key={document.id}
companyId={companyId}
document={document}
fallback={reference}
/>
))
: references.map((reference) => (
<div key={reference.id} className="rounded-lg border border-border bg-muted px-4 py-3">
<div className="text-[13px] font-medium text-foreground">{reference.title ?? reference.id}</div>
<div className="text-[11px] text-muted-foreground">
{reference.type?.replaceAll("_", " ") ?? "document"} · {reference.id}
</div>
<div className="mt-2 text-[12px] text-muted-foreground">
The linked document is not loaded in the current snapshot yet. Reopen the approval after the next refresh.
</div>
</div>
))}
{approval.impactSummary ? (
<div className="text-[11px] text-muted-foreground">{approval.impactSummary}</div>
) : null}
</div>
)
}
@@ -0,0 +1,168 @@
import { useState } from "react"
import type { AgentRecord, ApprovalRecord, DocumentRecord, TaskRecord } from "@shared/types"
import { extractApprovalDocumentReferences } from "@shared/approval-documents"
import { useT } from "../../../i18n"
import { unwrap } from "../../../lib/desktop"
import { ConfirmDialog, InlineNotice } from "../../ui"
import { useAppStore } from "../../../state/store"
import { describeActionError } from "../execution-utils"
import { SocialDraftPreview } from "../SocialDraftPreview"
import { ApprovalSummary } from "./ApprovalSummary"
import { ProposedHireCard } from "./ProposedHireCard"
import { ApprovalDocumentsPanel } from "./ApprovalDocumentsPanel"
import { DeliverableReviewPanel } from "./DeliverableReviewPanel"
import { SchedulePicker, type ScheduleMode } from "./SchedulePicker"
import { DecisionForm, type ActingState, type DecisionState } from "./DecisionForm"
import { useSocialPlatform } from "./useApprovalPayload"
export function ApprovalInspector({
approval,
agents,
tasks,
documents,
companyId,
onDecide,
onDeleted,
}: {
approval: ApprovalRecord
agents: AgentRecord[]
tasks: TaskRecord[]
documents?: DocumentRecord[]
companyId: string
onDecide: (state: DecisionState, note?: string) => Promise<void>
onDeleted: () => Promise<void>
}) {
const t = useT()
const addToast = useAppStore((s) => s.addToast)
const [decisionNote, setDecisionNote] = useState("")
const [confirmDelete, setConfirmDelete] = useState(false)
const [actionError, setActionError] = useState<string | null>(null)
const [acting, setActing] = useState<ActingState>(null)
const [scheduleMode, setScheduleMode] = useState<ScheduleMode>("immediate")
const [scheduledAt, setScheduledAt] = useState("")
const [scheduleError, setScheduleError] = useState<string | null>(null)
const requestingAgent = agents.find((a) => a.id === approval.requestedByAgentId) ?? null
const relatedTask = tasks.find((task) => task.id === approval.relatedTaskId) ?? null
const isDecided = approval.state !== "pending"
const isSocialPost = approval.type === "social_post"
const socialPlatform = useSocialPlatform(approval)
const hasDocumentReferences = extractApprovalDocumentReferences(approval.payloadJson).length > 0
async function handleDelete() {
setActionError(null)
setActing("delete")
try {
unwrap(await window.agentCompany.deleteApproval({ id: approval.id, companyId }))
setConfirmDelete(false)
await onDeleted()
} catch (error) {
setActionError(describeActionError(error))
} finally {
setActing(null)
}
}
async function handleDecision(state: DecisionState) {
setActionError(null)
setScheduleError(null)
if (isSocialPost && state === "approved" && scheduleMode === "scheduled") {
if (!scheduledAt) {
setScheduleError("Please select a date and time for scheduling.")
return
}
const chosenTime = new Date(scheduledAt).getTime()
if (chosenTime <= Date.now()) {
setScheduleError("Scheduled time must be in the future.")
return
}
}
setActing(state)
try {
let noteForDecision = decisionNote || undefined
if (isSocialPost && state === "approved" && scheduleMode === "scheduled" && scheduledAt) {
const scheduleInfo = `[scheduled:${new Date(scheduledAt).toISOString()}]`
noteForDecision = noteForDecision ? `${noteForDecision} ${scheduleInfo}` : scheduleInfo
}
await onDecide(state, noteForDecision)
if (state === "approved") addToast("Approval granted — agent is moving forward", "success")
else if (state === "rejected") addToast("Approval rejected", "warn")
else if (state === "revision_requested") addToast("Revision requested — agent will revise and resubmit", "warn")
} catch (error) {
setActionError(describeActionError(error))
} finally {
setActing(null)
}
}
return (
<div className="space-y-6">
<ConfirmDialog
open={confirmDelete}
title={t("exec.deleteApproval")}
message={t("exec.deleteApprovalConfirm")}
errorMessage={actionError}
onConfirm={() => void handleDelete()}
onCancel={() => {
setActionError(null)
setConfirmDelete(false)
}}
/>
{actionError && !confirmDelete ? <InlineNotice message={actionError} /> : null}
<div className="border-y border-border py-4 space-y-4">
<ApprovalSummary approval={approval} requestingAgent={requestingAgent} relatedTask={relatedTask} />
{approval.type === "hire_agent" ? <ProposedHireCard approval={approval} agents={agents} /> : null}
{approval.type === "approve_ceo_strategy" ? (
<div className="rounded-lg border border-[color:var(--destructive)] border-opacity-30 bg-warning-muted px-4 py-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-warning">
Organization Strategy
</div>
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">
{approval.payloadSummary}
</div>
</div>
) : null}
<ApprovalDocumentsPanel approval={approval} documents={documents} companyId={companyId} />
{approval.type === "deliverable_review" && !hasDocumentReferences ? (
<DeliverableReviewPanel approval={approval} documents={documents} agents={agents} />
) : null}
{isSocialPost && approval.payloadJson ? (
<SocialDraftPreview
payloadJson={approval.payloadJson}
platform={socialPlatform}
isPending={approval.state === "pending"}
/>
) : null}
</div>
{isSocialPost && !isDecided ? (
<SchedulePicker
mode={scheduleMode}
scheduledAt={scheduledAt}
error={scheduleError}
onModeChange={setScheduleMode}
onScheduledAtChange={setScheduledAt}
onClearError={() => setScheduleError(null)}
/>
) : null}
<DecisionForm
isDecided={isDecided}
isSocialPost={isSocialPost}
decisionNote={decisionNote}
acting={acting}
onDecisionNoteChange={setDecisionNote}
onDecide={(state) => void handleDecision(state)}
onRequestDelete={() => setConfirmDelete(true)}
/>
{approval.decisionNote ? (
<div className="border-t border-border pt-4 text-[13px] text-muted-foreground">
Last note: {approval.decisionNote}
</div>
) : null}
</div>
)
}
@@ -0,0 +1,52 @@
import { Briefcase, Robot } from "@phosphor-icons/react"
import type { AgentRecord, ApprovalRecord, TaskRecord } from "@shared/types"
import { formatTime } from "../../../lib/formatters"
import { StatusPill } from "../../ui"
import { TypeBadge } from "./TypeBadge"
export function ApprovalSummary({
approval,
requestingAgent,
relatedTask,
}: {
approval: ApprovalRecord
requestingAgent: AgentRecord | null
relatedTask: TaskRecord | null
}) {
return (
<>
<div className="flex items-center gap-3">
<TypeBadge type={approval.type} />
<StatusPill status={approval.state} />
</div>
<div className="text-[13px] font-medium text-foreground">{approval.payloadSummary}</div>
{approval.impactSummary ? (
<div className="text-[13px] text-muted-foreground">{approval.impactSummary}</div>
) : null}
<div className="grid gap-2 text-[13px] md:grid-cols-2">
<div className="flex items-center gap-2 text-muted-foreground">
<Robot size={14} />
Requested by:{" "}
<span className="font-medium text-foreground">{requestingAgent?.name ?? "Unknown"}</span>
{requestingAgent?.role ? (
<span className="text-[10px] text-muted-foreground">({requestingAgent.role})</span>
) : null}
</div>
{relatedTask ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Briefcase size={14} />
Task: <span className="font-medium text-foreground">{relatedTask.title}</span>
</div>
) : null}
<div className="text-muted-foreground">
Created: <span className="font-mono text-foreground">{formatTime(approval.createdAt)}</span>
</div>
{approval.updatedAt !== approval.createdAt ? (
<div className="text-muted-foreground">
Updated: <span className="font-mono text-foreground">{formatTime(approval.updatedAt)}</span>
</div>
) : null}
</div>
</>
)
}
@@ -0,0 +1,79 @@
import { CheckCircle, ClockCounterClockwise, Trash, XCircle } from "@phosphor-icons/react"
import { ActionButton, TextArea } from "../../ui"
import { useT } from "../../../i18n"
export type DecisionState = "approved" | "rejected" | "revision_requested"
export type ActingState = DecisionState | "delete" | null
export function DecisionForm({
isDecided,
isSocialPost,
decisionNote,
acting,
onDecisionNoteChange,
onDecide,
onRequestDelete,
}: {
isDecided: boolean
isSocialPost: boolean
decisionNote: string
acting: ActingState
onDecisionNoteChange: (value: string) => void
onDecide: (state: DecisionState) => void
onRequestDelete: () => void
}) {
const t = useT()
return (
<>
{!isDecided ? (
<div>
<label className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
Decision note
</label>
<TextArea
value={decisionNote}
onChange={onDecisionNoteChange}
placeholder="Add a note (optional)"
rows={3}
/>
</div>
) : null}
<div className="flex flex-wrap gap-3">
{!isDecided ? (
<>
<ActionButton
label={isSocialPost ? "Approve & Post" : t("exec.approve")}
tone="accent"
onClick={() => onDecide("approved")}
icon={CheckCircle}
loading={acting === "approved"}
/>
<ActionButton
label={t("exec.requestRevision")}
onClick={() => onDecide("revision_requested")}
icon={ClockCounterClockwise}
loading={acting === "revision_requested"}
/>
<ActionButton
label={t("exec.reject")}
tone="danger"
onClick={() => onDecide("rejected")}
icon={XCircle}
loading={acting === "rejected"}
/>
</>
) : null}
{isDecided ? (
<ActionButton
label="Delete"
onClick={onRequestDelete}
icon={Trash}
tone="danger"
loading={acting === "delete"}
/>
) : null}
</div>
</>
)
}
@@ -0,0 +1,112 @@
import { useMemo, useState } from "react"
import { CaretDown, CaretUp } from "@phosphor-icons/react"
import type { AgentRecord, ApprovalRecord, DocumentRecord } from "@shared/types"
import { useApprovalPayload } from "./useApprovalPayload"
interface DocRef {
id: string
type: string
title: string
}
function toDocRefs(value: unknown): DocRef[] {
if (!Array.isArray(value)) return []
return value.filter(
(entry): entry is DocRef =>
!!entry && typeof entry === "object" &&
typeof (entry as DocRef).id === "string" &&
typeof (entry as DocRef).type === "string" &&
typeof (entry as DocRef).title === "string",
)
}
function toDocIds(value: unknown, fallback: DocRef[]): string[] {
if (Array.isArray(value)) {
return value.filter((entry): entry is string => typeof entry === "string")
}
return fallback.map((d) => d.id)
}
export function DeliverableReviewPanel({
approval,
documents,
agents,
}: {
approval: ApprovalRecord
documents?: DocumentRecord[]
agents: AgentRecord[]
}) {
const payload = useApprovalPayload(approval)
const docRefs = useMemo(() => toDocRefs(payload?.documents), [payload])
const docIds = useMemo(() => toDocIds(payload?.documentIds, docRefs), [payload, docRefs])
const fullDocs = useMemo(() => {
if (!documents) return [] as DocumentRecord[]
return docIds
.map((id) => documents.find((d) => d.id === id))
.filter((entry): entry is DocumentRecord => entry != null)
}, [documents, docIds])
const [expandedDocId, setExpandedDocId] = useState<string | null>(
fullDocs.length === 1 ? fullDocs[0]!.id : null,
)
return (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Deliverable Review</div>
{fullDocs.length > 0 ? (
fullDocs.map((doc) => {
const isExpanded = expandedDocId === doc.id
const author = doc.authorAgentId ? agents.find((a) => a.id === doc.authorAgentId) : null
return (
<div key={doc.id} className="rounded-lg border border-border overflow-hidden">
<button
type="button"
onClick={() => setExpandedDocId(isExpanded ? null : doc.id)}
className="flex w-full cursor-pointer items-center gap-3 bg-muted px-4 py-3 text-left transition hover:bg-card"
>
<span className="shrink-0 rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">
{doc.type.replaceAll("_", " ")}
</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-foreground">{doc.title}</span>
{isExpanded ? (
<CaretUp size={14} className="shrink-0 text-muted-foreground" />
) : (
<CaretDown size={14} className="shrink-0 text-muted-foreground" />
)}
</button>
{isExpanded ? (
<div className="border-t border-border px-4 py-3">
{author ? (
<div className="mb-2 text-[11px] text-muted-foreground">
Author: <span className="font-medium text-foreground">{author.name}</span> ({author.role})
</div>
) : null}
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">
{doc.content || "No content."}
</div>
</div>
) : null}
</div>
)
})
) : docRefs.length > 0 ? (
docRefs.map((doc) => (
<div
key={doc.id}
className="flex items-center gap-2 rounded-lg border border-border bg-muted px-4 py-3 text-[12px]"
>
<span className="rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">
{doc.type.replaceAll("_", " ")}
</span>
<span className="font-medium text-foreground">{doc.title}</span>
</div>
))
) : (
<div className="text-[12px] text-muted-foreground">{approval.payloadSummary}</div>
)}
{approval.impactSummary ? (
<div className="text-[11px] text-muted-foreground">{approval.impactSummary}</div>
) : null}
</div>
)
}
@@ -0,0 +1,93 @@
import { useMemo } from "react"
import type { AgentRecord, ApprovalRecord } from "@shared/types"
import { formatMoney } from "../../../lib/formatters"
import { useApprovalPayload } from "./useApprovalPayload"
interface HireSnapshot {
name?: string
role?: string
title?: string
capabilities?: string
connectorId?: string
budgetMonthlyUsd?: number
reportsTo?: string
}
function toHireSnapshot(value: unknown): HireSnapshot | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null
return value as HireSnapshot
}
export function ProposedHireCard({
approval,
agents,
}: {
approval: ApprovalRecord
agents: AgentRecord[]
}) {
const payload = useApprovalPayload(approval)
const { proposedAgent, reqSnap, mgr } = useMemo(() => {
const payloadAgentId = typeof payload?.agentId === "string" ? payload.agentId : null
const snapshot = toHireSnapshot(payload?.requestedConfigurationSnapshot)
const relatedAgent = approval.relatedAgentId
? agents.find((a) => a.id === approval.relatedAgentId) ?? null
: null
const fallbackAgent = payloadAgentId ? agents.find((a) => a.id === payloadAgentId) ?? null : null
const proposed = relatedAgent ?? fallbackAgent
const mgrId = proposed?.reportsTo ?? snapshot?.reportsTo ?? null
const manager = mgrId ? agents.find((a) => a.id === mgrId) ?? null : null
return { proposedAgent: proposed, reqSnap: snapshot, mgr: manager }
}, [payload, approval.relatedAgentId, agents])
if (!proposedAgent && !reqSnap) return null
const name = proposedAgent?.name ?? reqSnap?.name ?? "Unspecified"
const role = proposedAgent?.role ?? reqSnap?.role ?? "Unspecified"
const title = proposedAgent?.title || reqSnap?.title
const capabilities = proposedAgent?.capabilities || reqSnap?.capabilities
const connectorId = proposedAgent?.connectorId ?? reqSnap?.connectorId ?? "Unknown"
const budget = proposedAgent?.budgetMonthlyUsd ?? reqSnap?.budgetMonthlyUsd ?? 0
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">{name}</span>
</div>
<div>
<span className="text-muted-foreground">Role:</span>{" "}
<span className="text-foreground">{role}</span>
</div>
{title ? (
<div>
<span className="text-muted-foreground">Title:</span>{" "}
<span className="text-foreground">{title}</span>
</div>
) : null}
{capabilities ? (
<div>
<span className="text-muted-foreground">Capabilities:</span>{" "}
<span className="text-foreground">{capabilities}</span>
</div>
) : null}
<div>
<span className="text-muted-foreground">Connector:</span>{" "}
<span className="font-mono text-foreground">{connectorId}</span>
</div>
<div>
<span className="text-muted-foreground">Budget:</span>{" "}
<span className="font-mono text-foreground">{formatMoney(budget)}/mo</span>
</div>
{mgr ? (
<div>
<span className="text-muted-foreground">Reports to:</span>{" "}
<span className="text-foreground">{mgr.name}</span>
</div>
) : null}
</div>
</div>
)
}
@@ -0,0 +1,77 @@
import { useMemo } from "react"
import { Clock } from "@phosphor-icons/react"
import { InlineNotice } from "../../ui"
export type ScheduleMode = "immediate" | "scheduled"
function computeMinDatetime(): string {
const d = new Date(Date.now() + 5 * 60 * 1000)
const pad = (n: number) => String(n).padStart(2, "0")
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
}
export function SchedulePicker({
mode,
scheduledAt,
error,
onModeChange,
onScheduledAtChange,
onClearError,
}: {
mode: ScheduleMode
scheduledAt: string
error: string | null
onModeChange: (mode: ScheduleMode) => void
onScheduledAtChange: (value: string) => void
onClearError: () => void
}) {
const minDatetime = useMemo(computeMinDatetime, [])
return (
<div className="space-y-3">
<div role="radiogroup" aria-label="Post timing" className="space-y-2">
<label className="flex cursor-pointer items-center gap-2">
<input
type="radio"
name="schedule-mode"
value="immediate"
checked={mode === "immediate"}
onChange={() => {
onModeChange("immediate")
onClearError()
}}
className="h-4 w-4 shrink-0 cursor-pointer accent-[color:var(--primary)]"
/>
<span className="text-[13px] font-medium text-foreground">Post immediately</span>
</label>
<label className="flex cursor-pointer items-center gap-2">
<input
type="radio"
name="schedule-mode"
value="scheduled"
checked={mode === "scheduled"}
onChange={() => onModeChange("scheduled")}
className="h-4 w-4 shrink-0 cursor-pointer accent-[color:var(--primary)]"
/>
<span className="text-[13px] font-medium text-foreground">Schedule for later</span>
</label>
</div>
{mode === "scheduled" ? (
<div className="flex items-center gap-2">
<Clock size={16} className="text-muted-foreground" />
<input
type="datetime-local"
value={scheduledAt}
min={minDatetime}
onChange={(e) => {
onScheduledAtChange(e.target.value)
onClearError()
}}
className="rounded-lg border border-border bg-card px-3.5 py-2.5 text-[13px] text-foreground focus:border-[color:var(--primary)] focus:outline-none"
/>
</div>
) : null}
{error ? <InlineNotice message={error} /> : null}
</div>
)
}
@@ -0,0 +1,16 @@
const TYPE_BADGE_CLASSES: Record<string, string> = {
hire_agent: "bg-accent text-primary",
approve_ceo_strategy: "bg-warning-muted text-warning",
secret_access: "bg-destructive-muted text-destructive",
social_post: "bg-accent text-primary",
deliverable_review: "bg-accent text-primary",
}
export function TypeBadge({ type }: { type: string }) {
const className = TYPE_BADGE_CLASSES[type] ?? "bg-muted text-muted-foreground"
return (
<span className={`rounded-full px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] ${className}`}>
{type.replaceAll("_", " ")}
</span>
)
}
@@ -0,0 +1 @@
export { ApprovalInspector } from "./ApprovalInspector"
@@ -0,0 +1,26 @@
import { useMemo } from "react"
import type { ApprovalRecord } from "@shared/types"
export function useApprovalPayload(approval: ApprovalRecord): Record<string, unknown> | null {
return useMemo(() => {
if (!approval.payloadJson) return null
try {
const parsed = JSON.parse(approval.payloadJson)
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>
}
return null
} catch {
return null
}
}, [approval.payloadJson])
}
export function useSocialPlatform(approval: ApprovalRecord): string {
const payload = useApprovalPayload(approval)
return useMemo(() => {
if (approval.type !== "social_post") return "other"
const platform = payload?.platform
return typeof platform === "string" ? platform : "other"
}, [approval.type, payload])
}