import { useEffect, useMemo, useState } from "react" import type { CommentRecord, ProfileSnapshot, RunRecord } from "@shared/types" import { useT } from "../../i18n" import { formatMoney, formatTime } from "../../lib/formatters" import { unwrap } from "../../lib/desktop" import { ActionButton, InlineNotice, StatusPill } from "../ui" import { MarkdownBody } from "../ui/MarkdownBody" import { ArrowRight } from "../ui/icons" import { describeActionError } from "./execution-utils" type ActivityFilter = "all" | "comments" | "runs" export function TaskInspectorActivity({ taskId, companyId, agents, comments, runs, onRefresh, }: { taskId: string companyId: string agents: ProfileSnapshot["agents"] comments: CommentRecord[] runs: RunRecord[] onRefresh: () => Promise }) { const t = useT() const [filter, setFilter] = useState("all") const [newComment, setNewComment] = useState("") const [submitting, setSubmitting] = useState(false) const [submitError, setSubmitError] = useState(null) const agentMap = useMemo(() => new Map(agents.map((a) => [a.id, a])), [agents]) useEffect(() => { const unsubscribe = window.agentCompany.subscribe((event) => { if (event.type === "agent-message" && event.taskId === taskId) void onRefresh() }) return unsubscribe }, [onRefresh, taskId]) const handleSubmit = async () => { if (!newComment.trim()) return setSubmitError(null); setSubmitting(true) try { unwrap(await window.agentCompany.addComment({ companyId, taskId, authorName: "Board", body: newComment.trim() })) setNewComment("") await onRefresh() } catch (e) { setSubmitError(describeActionError(e)) } finally { setSubmitting(false) } } type FeedItem = | { kind: "comment"; ts: number; data: CommentRecord } | { kind: "run"; ts: number; data: RunRecord } const feed: FeedItem[] = useMemo(() => { const items: FeedItem[] = [] const toMs = (value: string | null) => (value ? Date.parse(value) : 0) if (filter === "all" || filter === "comments") { for (const c of comments) items.push({ kind: "comment", ts: toMs(c.createdAt), data: c }) } if (filter === "all" || filter === "runs") { for (const r of runs) items.push({ kind: "run", ts: toMs(r.startedAt ?? r.createdAt), data: r }) } items.sort((a, b) => b.ts - a.ts) return items }, [comments, runs, filter]) const filterBtn = (key: ActivityFilter, label: string) => ( ) return (
{filterBtn("all", t("exec.activityAll", "All"))} {filterBtn("comments", t("exec.communication"))} {filterBtn("runs", t("exec.runHistory"))} {feed.length}
{feed.length === 0 ? (
{t("exec.noMessagesYet")}
) : (
{feed.map((item) => { if (item.kind === "comment") { const c = item.data const isAgent = Boolean(c.authorAgentId) const agent = isAgent ? agentMap.get(c.authorAgentId ?? "") : null return (
{c.authorName} {agent && {agent.role}} {formatTime(c.createdAt)}
) } const run = item.data const runAgent = agentMap.get(run.agentId) const isActive = run.status === "running" || run.status === "queued" const needsAttention = run.status === "failed" || run.status === "timed_out" // Two tiers only: resting (default border + muted bg) vs attention // (destructive border + muted bg). Active runs earn a pulsing dot, // not a third border color — less rhythm, more signal. return (
{isActive ? : null} {run.summary || (isActive ? t("exec.runningEllipsis") : t("exec.completedRun"))}
{runAgent ? {runAgent.name} : null} {run.connectorId.replace("_local", "")} {run.model ? {run.model} : null} {formatTime(run.startedAt)} {run.costUsd != null && run.costUsd > 0 ? {formatMoney(run.costUsd)} : null}
{run.errorMessage ? (
{run.errorMessage}
) : null}
) })}
)} {submitError ? : null}
setNewComment(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); void handleSubmit() } }} /> void handleSubmit()} icon={ArrowRight} loading={submitting} />
) }