2026-02-18 13:02:23 -06:00
|
|
|
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
2026-02-19 14:39:48 -06:00
|
|
|
import { useParams, useNavigate, Link, useBeforeUnload } from "react-router-dom";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
2026-02-23 12:25:13 -06:00
|
|
|
import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { heartbeatsApi } from "../api/heartbeats";
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
import { activityApi } from "../api/activity";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { issuesApi } from "../api/issues";
|
|
|
|
|
import { usePanel } from "../context/PanelContext";
|
2026-02-20 10:33:18 -06:00
|
|
|
import { useSidebar } from "../context/SidebarContext";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { useCompany } from "../context/CompanyContext";
|
2026-02-17 20:07:30 -06:00
|
|
|
import { useDialog } from "../context/DialogContext";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
2026-02-17 20:07:30 -06:00
|
|
|
import { AgentConfigForm } from "../components/AgentConfigForm";
|
|
|
|
|
import { adapterLabels, roleLabels } from "../components/agent-config-primitives";
|
2026-02-18 13:53:03 -06:00
|
|
|
import { getUIAdapter, buildTranscript } from "../adapters";
|
|
|
|
|
import type { TranscriptEntry } from "../adapters";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { StatusBadge } from "../components/StatusBadge";
|
2026-02-18 15:29:29 -06:00
|
|
|
import { CopyText } from "../components/CopyText";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { EntityRow } from "../components/EntityRow";
|
2026-02-19 14:39:48 -06:00
|
|
|
import { Identity } from "../components/Identity";
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
import { formatCents, formatDate, relativeTime, formatTokens } from "../lib/utils";
|
|
|
|
|
import { cn } from "../lib/utils";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { Button } from "@/components/ui/button";
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
import {
|
|
|
|
|
Popover,
|
|
|
|
|
PopoverContent,
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
} from "@/components/ui/popover";
|
|
|
|
|
import {
|
|
|
|
|
MoreHorizontal,
|
|
|
|
|
Play,
|
|
|
|
|
Pause,
|
|
|
|
|
CheckCircle2,
|
|
|
|
|
XCircle,
|
|
|
|
|
Clock,
|
|
|
|
|
Timer,
|
|
|
|
|
Loader2,
|
|
|
|
|
Slash,
|
|
|
|
|
RotateCcw,
|
|
|
|
|
Trash2,
|
2026-02-17 20:07:30 -06:00
|
|
|
Plus,
|
2026-02-17 20:46:12 -06:00
|
|
|
Key,
|
|
|
|
|
Eye,
|
|
|
|
|
EyeOff,
|
|
|
|
|
Copy,
|
2026-02-18 15:29:29 -06:00
|
|
|
ChevronRight,
|
2026-02-23 14:41:21 -06:00
|
|
|
ChevronDown,
|
2026-02-20 10:33:18 -06:00
|
|
|
ArrowLeft,
|
2026-02-23 14:41:21 -06:00
|
|
|
Settings,
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
} from "lucide-react";
|
2026-02-17 20:46:12 -06:00
|
|
|
import { Input } from "@/components/ui/input";
|
2026-02-23 12:25:13 -06:00
|
|
|
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
2026-02-19 14:39:48 -06:00
|
|
|
import type { Agent, HeartbeatRun, HeartbeatRunEvent, AgentRuntimeState } from "@paperclip/shared";
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
|
|
|
|
const runStatusIcons: Record<string, { icon: typeof CheckCircle2; color: string }> = {
|
|
|
|
|
succeeded: { icon: CheckCircle2, color: "text-green-400" },
|
|
|
|
|
failed: { icon: XCircle, color: "text-red-400" },
|
|
|
|
|
running: { icon: Loader2, color: "text-cyan-400" },
|
|
|
|
|
queued: { icon: Clock, color: "text-yellow-400" },
|
|
|
|
|
timed_out: { icon: Timer, color: "text-orange-400" },
|
|
|
|
|
cancelled: { icon: Slash, color: "text-neutral-400" },
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-18 16:46:55 -06:00
|
|
|
const REDACTED_ENV_VALUE = "***REDACTED***";
|
|
|
|
|
const SECRET_ENV_KEY_RE =
|
|
|
|
|
/(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i;
|
|
|
|
|
const JWT_VALUE_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?$/;
|
|
|
|
|
|
|
|
|
|
function shouldRedactSecretValue(key: string, value: unknown): boolean {
|
|
|
|
|
if (SECRET_ENV_KEY_RE.test(key)) return true;
|
|
|
|
|
if (typeof value !== "string") return false;
|
|
|
|
|
return JWT_VALUE_RE.test(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function redactEnvValue(key: string, value: unknown): string {
|
2026-02-19 15:44:05 -06:00
|
|
|
if (
|
|
|
|
|
typeof value === "object" &&
|
|
|
|
|
value !== null &&
|
|
|
|
|
!Array.isArray(value) &&
|
|
|
|
|
(value as { type?: unknown }).type === "secret_ref"
|
|
|
|
|
) {
|
|
|
|
|
return "***SECRET_REF***";
|
|
|
|
|
}
|
2026-02-18 16:46:55 -06:00
|
|
|
if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE;
|
|
|
|
|
if (value === null || value === undefined) return "";
|
|
|
|
|
if (typeof value === "string") return value;
|
|
|
|
|
try {
|
|
|
|
|
return JSON.stringify(value);
|
|
|
|
|
} catch {
|
|
|
|
|
return String(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatEnvForDisplay(envValue: unknown): string {
|
|
|
|
|
const env = asRecord(envValue);
|
|
|
|
|
if (!env) return "<unable-to-parse>";
|
|
|
|
|
|
|
|
|
|
const keys = Object.keys(env);
|
|
|
|
|
if (keys.length === 0) return "<empty>";
|
|
|
|
|
|
|
|
|
|
return keys
|
|
|
|
|
.sort()
|
|
|
|
|
.map((key) => `${key}=${redactEnvValue(key, env[key])}`)
|
|
|
|
|
.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const sourceLabels: Record<string, string> = {
|
|
|
|
|
timer: "Timer",
|
|
|
|
|
assignment: "Assignment",
|
|
|
|
|
on_demand: "On-demand",
|
|
|
|
|
automation: "Automation",
|
|
|
|
|
};
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
const LIVE_SCROLL_BOTTOM_TOLERANCE_PX = 32;
|
|
|
|
|
type ScrollContainer = Window | HTMLElement;
|
|
|
|
|
|
|
|
|
|
function isWindowContainer(container: ScrollContainer): container is Window {
|
|
|
|
|
return container === window;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isElementScrollContainer(element: HTMLElement): boolean {
|
|
|
|
|
const overflowY = window.getComputedStyle(element).overflowY;
|
|
|
|
|
return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findScrollContainer(anchor: HTMLElement | null): ScrollContainer {
|
|
|
|
|
let parent = anchor?.parentElement ?? null;
|
|
|
|
|
while (parent) {
|
|
|
|
|
if (isElementScrollContainer(parent)) return parent;
|
|
|
|
|
parent = parent.parentElement;
|
|
|
|
|
}
|
|
|
|
|
return window;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readScrollMetrics(container: ScrollContainer): { scrollHeight: number; distanceFromBottom: number } {
|
|
|
|
|
if (isWindowContainer(container)) {
|
|
|
|
|
const pageHeight = Math.max(
|
|
|
|
|
document.documentElement.scrollHeight,
|
|
|
|
|
document.body.scrollHeight,
|
|
|
|
|
);
|
|
|
|
|
const viewportBottom = window.scrollY + window.innerHeight;
|
|
|
|
|
return {
|
|
|
|
|
scrollHeight: pageHeight,
|
|
|
|
|
distanceFromBottom: Math.max(0, pageHeight - viewportBottom),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const viewportBottom = container.scrollTop + container.clientHeight;
|
|
|
|
|
return {
|
|
|
|
|
scrollHeight: container.scrollHeight,
|
|
|
|
|
distanceFromBottom: Math.max(0, container.scrollHeight - viewportBottom),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBehavior = "auto") {
|
|
|
|
|
if (isWindowContainer(container)) {
|
|
|
|
|
const pageHeight = Math.max(
|
|
|
|
|
document.documentElement.scrollHeight,
|
|
|
|
|
document.body.scrollHeight,
|
|
|
|
|
);
|
|
|
|
|
window.scrollTo({ top: pageHeight, behavior });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.scrollTo({ top: container.scrollHeight, behavior });
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
type AgentDetailView = "overview" | "configure" | "runs";
|
2026-02-18 13:02:23 -06:00
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
function parseAgentDetailView(value: string | null): AgentDetailView {
|
|
|
|
|
if (value === "configure" || value === "configuration") return "configure";
|
2026-02-18 13:02:23 -06:00
|
|
|
if (value === "runs") return value;
|
|
|
|
|
return "overview";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function usageNumber(usage: Record<string, unknown> | null, ...keys: string[]) {
|
|
|
|
|
if (!usage) return 0;
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
const value = usage[key];
|
|
|
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runMetrics(run: HeartbeatRun) {
|
|
|
|
|
const usage = (run.usageJson ?? null) as Record<string, unknown> | null;
|
|
|
|
|
const result = (run.resultJson ?? null) as Record<string, unknown> | null;
|
|
|
|
|
const input = usageNumber(usage, "inputTokens", "input_tokens");
|
|
|
|
|
const output = usageNumber(usage, "outputTokens", "output_tokens");
|
|
|
|
|
const cached = usageNumber(
|
|
|
|
|
usage,
|
|
|
|
|
"cachedInputTokens",
|
|
|
|
|
"cached_input_tokens",
|
|
|
|
|
"cache_read_input_tokens",
|
|
|
|
|
);
|
|
|
|
|
const cost =
|
|
|
|
|
usageNumber(usage, "costUsd", "cost_usd", "total_cost_usd") ||
|
|
|
|
|
usageNumber(result, "total_cost_usd", "cost_usd", "costUsd");
|
|
|
|
|
return {
|
|
|
|
|
input,
|
|
|
|
|
output,
|
|
|
|
|
cached,
|
|
|
|
|
cost,
|
|
|
|
|
totalTokens: input + output,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type RunLogChunk = { ts: string; stream: "stdout" | "stderr" | "system"; chunk: string };
|
|
|
|
|
|
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
|
|
|
return value as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
export function AgentDetail() {
|
2026-02-19 14:39:48 -06:00
|
|
|
const { agentId, tab: urlTab, runId: urlRunId } = useParams<{ agentId: string; tab?: string; runId?: string }>();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const { selectedCompanyId } = useCompany();
|
2026-02-18 13:02:23 -06:00
|
|
|
const { closePanel } = usePanel();
|
2026-02-17 20:07:30 -06:00
|
|
|
const { openNewIssue } = useDialog();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
2026-02-17 12:24:48 -06:00
|
|
|
const queryClient = useQueryClient();
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const navigate = useNavigate();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const [actionError, setActionError] = useState<string | null>(null);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const [moreOpen, setMoreOpen] = useState(false);
|
2026-02-23 14:41:21 -06:00
|
|
|
const activeView = urlRunId ? "runs" as AgentDetailView : parseAgentDetailView(urlTab ?? null);
|
2026-02-18 13:02:23 -06:00
|
|
|
const [configDirty, setConfigDirty] = useState(false);
|
|
|
|
|
const [configSaving, setConfigSaving] = useState(false);
|
|
|
|
|
const saveConfigActionRef = useRef<(() => void) | null>(null);
|
|
|
|
|
const cancelConfigActionRef = useRef<(() => void) | null>(null);
|
2026-02-20 11:29:13 -06:00
|
|
|
const { isMobile } = useSidebar();
|
2026-02-18 13:02:23 -06:00
|
|
|
const setSaveConfigAction = useCallback((fn: (() => void) | null) => { saveConfigActionRef.current = fn; }, []);
|
|
|
|
|
const setCancelConfigAction = useCallback((fn: (() => void) | null) => { cancelConfigActionRef.current = fn; }, []);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const { data: agent, isLoading, error } = useQuery({
|
|
|
|
|
queryKey: queryKeys.agents.detail(agentId!),
|
|
|
|
|
queryFn: () => agentsApi.get(agentId!),
|
|
|
|
|
enabled: !!agentId,
|
|
|
|
|
});
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const { data: runtimeState } = useQuery({
|
|
|
|
|
queryKey: queryKeys.agents.runtimeState(agentId!),
|
|
|
|
|
queryFn: () => agentsApi.runtimeState(agentId!),
|
|
|
|
|
enabled: !!agentId,
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const { data: heartbeats } = useQuery({
|
|
|
|
|
queryKey: queryKeys.heartbeats(selectedCompanyId!, agentId),
|
|
|
|
|
queryFn: () => heartbeatsApi.list(selectedCompanyId!, agentId),
|
|
|
|
|
enabled: !!selectedCompanyId && !!agentId,
|
|
|
|
|
});
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const { data: allIssues } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => issuesApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const { data: allAgents } = useQuery({
|
|
|
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const assignedIssues = (allIssues ?? []).filter((i) => i.assigneeAgentId === agentId);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo);
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agentId && a.status !== "terminated");
|
2026-02-20 11:29:13 -06:00
|
|
|
const mobileLiveRun = useMemo(
|
|
|
|
|
() => (heartbeats ?? []).find((r) => r.status === "running" || r.status === "queued") ?? null,
|
|
|
|
|
[heartbeats],
|
|
|
|
|
);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const agentAction = useMutation({
|
2026-02-19 14:02:29 -06:00
|
|
|
mutationFn: async (action: "invoke" | "pause" | "resume" | "terminate") => {
|
2026-02-17 12:24:48 -06:00
|
|
|
if (!agentId) return Promise.reject(new Error("No agent ID"));
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
switch (action) {
|
|
|
|
|
case "invoke": return agentsApi.invoke(agentId);
|
|
|
|
|
case "pause": return agentsApi.pause(agentId);
|
|
|
|
|
case "resume": return agentsApi.resume(agentId);
|
|
|
|
|
case "terminate": return agentsApi.terminate(agentId);
|
2026-02-17 12:24:48 -06:00
|
|
|
}
|
|
|
|
|
},
|
2026-02-20 12:21:03 -06:00
|
|
|
onSuccess: (data, action) => {
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
setActionError(null);
|
2026-02-17 12:24:48 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId!) });
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agentId!) });
|
2026-02-19 14:02:29 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agentId!) });
|
2026-02-17 12:24:48 -06:00
|
|
|
if (selectedCompanyId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId) });
|
|
|
|
|
}
|
2026-02-20 12:21:03 -06:00
|
|
|
if (action === "invoke" && data && typeof data === "object" && "id" in data) {
|
|
|
|
|
navigate(`/agents/${agentId}/runs/${(data as HeartbeatRun).id}`);
|
|
|
|
|
}
|
2026-02-17 12:24:48 -06:00
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
setActionError(err instanceof Error ? err.message : "Action failed");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-23 12:25:13 -06:00
|
|
|
const updateIcon = useMutation({
|
|
|
|
|
mutationFn: (icon: string) => agentsApi.update(agentId!, { icon }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId!) });
|
|
|
|
|
if (selectedCompanyId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId) });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-19 14:02:29 -06:00
|
|
|
const resetTaskSession = useMutation({
|
|
|
|
|
mutationFn: (taskKey: string | null) => agentsApi.resetSession(agentId!, taskKey),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setActionError(null);
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agentId!) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agentId!) });
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
setActionError(err instanceof Error ? err.message : "Failed to reset session");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
const updatePermissions = useMutation({
|
|
|
|
|
mutationFn: (canCreateAgents: boolean) =>
|
|
|
|
|
agentsApi.updatePermissions(agentId!, { canCreateAgents }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setActionError(null);
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId!) });
|
|
|
|
|
if (selectedCompanyId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId) });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
setActionError(err instanceof Error ? err.message : "Failed to update permissions");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
useEffect(() => {
|
2026-02-23 14:41:21 -06:00
|
|
|
const crumbs: { label: string; href?: string }[] = [
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
{ label: "Agents", href: "/agents" },
|
2026-02-23 14:41:21 -06:00
|
|
|
];
|
|
|
|
|
const agentName = agent?.name ?? agentId ?? "Agent";
|
|
|
|
|
if (activeView === "overview" && !urlRunId) {
|
|
|
|
|
crumbs.push({ label: agentName });
|
|
|
|
|
} else {
|
|
|
|
|
crumbs.push({ label: agentName, href: `/agents/${agentId}` });
|
|
|
|
|
if (urlRunId) {
|
|
|
|
|
crumbs.push({ label: "Runs", href: `/agents/${agentId}/runs` });
|
|
|
|
|
crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` });
|
|
|
|
|
} else if (activeView === "configure") {
|
|
|
|
|
crumbs.push({ label: "Configure" });
|
|
|
|
|
} else if (activeView === "runs") {
|
|
|
|
|
crumbs.push({ label: "Runs" });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setBreadcrumbs(crumbs);
|
|
|
|
|
}, [setBreadcrumbs, agent, agentId, activeView, urlRunId]);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-02-18 13:02:23 -06:00
|
|
|
closePanel();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
return () => closePanel();
|
2026-02-18 13:02:23 -06:00
|
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
|
|
useBeforeUnload(
|
|
|
|
|
useCallback((event) => {
|
|
|
|
|
if (!configDirty) return;
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.returnValue = "";
|
|
|
|
|
}, [configDirty]),
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
|
|
|
|
if (!agent) return null;
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
const isPendingApproval = agent.status === "pending_approval";
|
2026-02-23 14:41:21 -06:00
|
|
|
const showConfigActionBar = activeView === "configure" && configDirty;
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
|
|
|
|
return (
|
2026-02-20 11:29:13 -06:00
|
|
|
<div className={cn("space-y-6", isMobile && showConfigActionBar && "pb-24")}>
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
{/* Header */}
|
2026-02-20 10:32:32 -06:00
|
|
|
<div className="flex items-center justify-between gap-2">
|
2026-02-23 12:25:13 -06:00
|
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
|
|
|
<AgentIconPicker
|
|
|
|
|
value={agent.icon}
|
|
|
|
|
onChange={(icon) => updateIcon.mutate(icon)}
|
|
|
|
|
>
|
2026-02-23 14:41:21 -06:00
|
|
|
<button className="shrink-0 flex items-center justify-center h-12 w-12 rounded-lg bg-accent hover:bg-accent/80 transition-colors">
|
|
|
|
|
<AgentIcon icon={agent.icon} className="h-6 w-6" />
|
2026-02-23 12:25:13 -06:00
|
|
|
</button>
|
|
|
|
|
</AgentIconPicker>
|
|
|
|
|
<div className="min-w-0">
|
2026-02-23 14:41:21 -06:00
|
|
|
<h2 className="text-2xl font-bold truncate">{agent.name}</h2>
|
2026-02-23 12:25:13 -06:00
|
|
|
<p className="text-sm text-muted-foreground truncate">
|
|
|
|
|
{roleLabels[agent.role] ?? agent.role}
|
|
|
|
|
{agent.title ? ` - ${agent.title}` : ""}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
2026-02-20 10:32:32 -06:00
|
|
|
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2026-02-18 13:02:23 -06:00
|
|
|
onClick={() => openNewIssue({ assigneeAgentId: agentId })}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
>
|
2026-02-20 10:32:32 -06:00
|
|
|
<Plus className="h-3.5 w-3.5 sm:mr-1" />
|
|
|
|
|
<span className="hidden sm:inline">Assign Task</span>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</Button>
|
2026-02-17 20:07:30 -06:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2026-02-18 13:02:23 -06:00
|
|
|
onClick={() => agentAction.mutate("invoke")}
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
disabled={agentAction.isPending || isPendingApproval}
|
2026-02-17 20:07:30 -06:00
|
|
|
>
|
2026-02-20 10:32:32 -06:00
|
|
|
<Play className="h-3.5 w-3.5 sm:mr-1" />
|
|
|
|
|
<span className="hidden sm:inline">Invoke</span>
|
2026-02-17 20:07:30 -06:00
|
|
|
</Button>
|
2026-02-18 13:02:23 -06:00
|
|
|
{agent.status === "paused" ? (
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2026-02-18 13:02:23 -06:00
|
|
|
onClick={() => agentAction.mutate("resume")}
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
disabled={agentAction.isPending || isPendingApproval}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
>
|
2026-02-20 10:32:32 -06:00
|
|
|
<Play className="h-3.5 w-3.5 sm:mr-1" />
|
|
|
|
|
<span className="hidden sm:inline">Resume</span>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</Button>
|
|
|
|
|
) : (
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2026-02-18 13:02:23 -06:00
|
|
|
onClick={() => agentAction.mutate("pause")}
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
disabled={agentAction.isPending || isPendingApproval}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
>
|
2026-02-20 10:32:32 -06:00
|
|
|
<Pause className="h-3.5 w-3.5 sm:mr-1" />
|
|
|
|
|
<span className="hidden sm:inline">Pause</span>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</Button>
|
|
|
|
|
)}
|
2026-02-20 10:32:32 -06:00
|
|
|
<span className="hidden sm:inline"><StatusBadge status={agent.status} /></span>
|
2026-02-20 11:29:13 -06:00
|
|
|
{mobileLiveRun && (
|
2026-02-23 14:41:21 -06:00
|
|
|
<Link
|
|
|
|
|
to={`/agents/${agent.id}/runs/${mobileLiveRun.id}`}
|
|
|
|
|
className="sm:hidden flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-blue-500/10 hover:bg-blue-500/20 transition-colors no-underline"
|
2026-02-20 11:29:13 -06:00
|
|
|
>
|
|
|
|
|
<span className="relative flex h-2 w-2">
|
|
|
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
|
|
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[11px] font-medium text-blue-400">Live</span>
|
2026-02-23 14:41:21 -06:00
|
|
|
</Link>
|
2026-02-20 11:29:13 -06:00
|
|
|
)}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
|
|
|
|
{/* Overflow menu */}
|
|
|
|
|
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon-xs">
|
|
|
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-44 p-1" align="end">
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50"
|
2026-02-17 21:12:31 -06:00
|
|
|
onClick={() => {
|
|
|
|
|
navigator.clipboard.writeText(agent.id);
|
|
|
|
|
setMoreOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Copy className="h-3 w-3" />
|
|
|
|
|
Copy Agent ID
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50"
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
onClick={() => {
|
2026-02-19 14:02:29 -06:00
|
|
|
resetTaskSession.mutate(null);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
setMoreOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<RotateCcw className="h-3 w-3" />
|
2026-02-19 14:02:29 -06:00
|
|
|
Reset Sessions
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-destructive"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
agentAction.mutate("terminate");
|
|
|
|
|
setMoreOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
Terminate
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
{isPendingApproval && (
|
|
|
|
|
<p className="text-sm text-amber-500">
|
|
|
|
|
This agent is pending board approval and cannot be invoked yet.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-20 11:29:13 -06:00
|
|
|
{/* Floating Save/Cancel (desktop) */}
|
|
|
|
|
{!isMobile && (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"sticky top-6 z-10 float-right transition-opacity duration-150",
|
|
|
|
|
showConfigActionBar
|
|
|
|
|
? "opacity-100"
|
|
|
|
|
: "opacity-0 pointer-events-none"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 bg-background/90 backdrop-blur-sm border border-border rounded-lg px-3 py-1.5 shadow-lg">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => cancelConfigActionRef.current?.()}
|
|
|
|
|
disabled={configSaving}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => saveConfigActionRef.current?.()}
|
|
|
|
|
disabled={configSaving}
|
|
|
|
|
>
|
|
|
|
|
{configSaving ? "Saving..." : "Save"}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Mobile bottom Save/Cancel bar */}
|
|
|
|
|
{isMobile && showConfigActionBar && (
|
|
|
|
|
<div className="fixed inset-x-0 bottom-0 z-30 border-t border-border bg-background/95 backdrop-blur-sm">
|
|
|
|
|
<div
|
|
|
|
|
className="flex items-center justify-end gap-2 px-3 py-2"
|
|
|
|
|
style={{ paddingBottom: "max(env(safe-area-inset-bottom), 0.5rem)" }}
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
>
|
2026-02-20 11:29:13 -06:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => cancelConfigActionRef.current?.()}
|
|
|
|
|
disabled={configSaving}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => saveConfigActionRef.current?.()}
|
|
|
|
|
disabled={configSaving}
|
|
|
|
|
>
|
|
|
|
|
{configSaving ? "Saving..." : "Save"}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-02-18 13:02:23 -06:00
|
|
|
</div>
|
2026-02-20 11:29:13 -06:00
|
|
|
)}
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
{/* View content */}
|
|
|
|
|
{activeView === "overview" && (
|
|
|
|
|
<AgentOverview
|
|
|
|
|
agent={agent}
|
|
|
|
|
runs={heartbeats ?? []}
|
|
|
|
|
assignedIssues={assignedIssues}
|
|
|
|
|
runtimeState={runtimeState}
|
|
|
|
|
reportsToAgent={reportsToAgent ?? null}
|
|
|
|
|
directReports={directReports}
|
|
|
|
|
agentId={agentId!}
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
/>
|
2026-02-23 14:41:21 -06:00
|
|
|
)}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
{activeView === "configure" && (
|
|
|
|
|
<AgentConfigurePage
|
|
|
|
|
agent={agent}
|
|
|
|
|
agentId={agentId!}
|
|
|
|
|
onDirtyChange={setConfigDirty}
|
|
|
|
|
onSaveActionChange={setSaveConfigAction}
|
|
|
|
|
onCancelActionChange={setCancelConfigAction}
|
|
|
|
|
onSavingChange={setConfigSaving}
|
|
|
|
|
updatePermissions={updatePermissions}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
{activeView === "runs" && (
|
|
|
|
|
<RunsTab
|
|
|
|
|
runs={heartbeats ?? []}
|
|
|
|
|
companyId={selectedCompanyId!}
|
|
|
|
|
agentId={agentId!}
|
|
|
|
|
selectedRunId={urlRunId ?? null}
|
|
|
|
|
adapterType={agent.adapterType}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Helper components ---- */
|
|
|
|
|
|
|
|
|
|
function SummaryRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-muted-foreground text-xs">{label}</span>
|
|
|
|
|
<div className="flex items-center gap-1">{children}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:39:48 -06:00
|
|
|
function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: string }) {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
|
|
if (runs.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const sorted = [...runs].sort(
|
|
|
|
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const liveRun = sorted.find((r) => r.status === "running" || r.status === "queued");
|
|
|
|
|
const run = liveRun ?? sorted[0];
|
|
|
|
|
const isLive = run.status === "running" || run.status === "queued";
|
|
|
|
|
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
|
|
|
|
|
const StatusIcon = statusInfo.icon;
|
|
|
|
|
const summary = run.resultJson
|
|
|
|
|
? String((run.resultJson as Record<string, unknown>).summary ?? (run.resultJson as Record<string, unknown>).result ?? "")
|
|
|
|
|
: run.error ?? "";
|
|
|
|
|
|
2026-02-19 14:02:29 -06:00
|
|
|
return (
|
2026-02-19 14:39:48 -06:00
|
|
|
<div className={cn(
|
|
|
|
|
"border rounded-lg p-4 space-y-3",
|
|
|
|
|
isLive ? "border-cyan-500/30 shadow-[0_0_12px_rgba(6,182,212,0.08)]" : "border-border"
|
|
|
|
|
)}>
|
2026-02-19 14:02:29 -06:00
|
|
|
<div className="flex items-center justify-between">
|
2026-02-19 14:39:48 -06:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{isLive && (
|
|
|
|
|
<span className="relative flex h-2 w-2">
|
|
|
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
|
|
|
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-400" />
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<h3 className="text-sm font-medium">{isLive ? "Live Run" : "Latest Run"}</h3>
|
|
|
|
|
</div>
|
2026-02-23 14:41:21 -06:00
|
|
|
<Link
|
|
|
|
|
to={`/agents/${agentId}/runs/${run.id}`}
|
|
|
|
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors no-underline"
|
2026-02-19 14:02:29 -06:00
|
|
|
>
|
2026-02-19 14:39:48 -06:00
|
|
|
View details →
|
2026-02-23 14:41:21 -06:00
|
|
|
</Link>
|
2026-02-19 14:02:29 -06:00
|
|
|
</div>
|
2026-02-19 14:39:48 -06:00
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<StatusIcon className={cn("h-3.5 w-3.5", statusInfo.color, run.status === "running" && "animate-spin")} />
|
|
|
|
|
<StatusBadge status={run.status} />
|
|
|
|
|
<span className="font-mono text-xs text-muted-foreground">{run.id.slice(0, 8)}</span>
|
|
|
|
|
<span className={cn(
|
|
|
|
|
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
|
|
|
|
run.invocationSource === "timer" ? "bg-blue-900/50 text-blue-300"
|
|
|
|
|
: run.invocationSource === "assignment" ? "bg-violet-900/50 text-violet-300"
|
|
|
|
|
: run.invocationSource === "on_demand" ? "bg-cyan-900/50 text-cyan-300"
|
|
|
|
|
: "bg-neutral-800 text-neutral-400"
|
|
|
|
|
)}>
|
|
|
|
|
{sourceLabels[run.invocationSource] ?? run.invocationSource}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="ml-auto text-xs text-muted-foreground">{relativeTime(run.createdAt)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{summary && (
|
|
|
|
|
<p className="text-xs text-muted-foreground truncate">{summary}</p>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Agent Overview (main single-page view) ---- */
|
|
|
|
|
|
|
|
|
|
function AgentOverview({
|
|
|
|
|
agent,
|
|
|
|
|
runs,
|
|
|
|
|
assignedIssues,
|
|
|
|
|
runtimeState,
|
|
|
|
|
reportsToAgent,
|
|
|
|
|
directReports,
|
|
|
|
|
agentId,
|
|
|
|
|
}: {
|
|
|
|
|
agent: Agent;
|
|
|
|
|
runs: HeartbeatRun[];
|
|
|
|
|
assignedIssues: { id: string; title: string; status: string; priority: string; identifier?: string | null; createdAt: Date }[];
|
|
|
|
|
runtimeState?: AgentRuntimeState;
|
|
|
|
|
reportsToAgent: Agent | null;
|
|
|
|
|
directReports: Agent[];
|
|
|
|
|
agentId: string;
|
|
|
|
|
}) {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-8">
|
|
|
|
|
{/* Latest Run */}
|
|
|
|
|
<LatestRunCard runs={runs} agentId={agentId} />
|
|
|
|
|
|
|
|
|
|
{/* Charts */}
|
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
|
|
<ChartCard title="Run Activity" subtitle="Last 14 days">
|
|
|
|
|
<RunActivityChart runs={runs} />
|
|
|
|
|
</ChartCard>
|
|
|
|
|
<ChartCard title="Issues by Priority" subtitle="Last 14 days">
|
|
|
|
|
<PriorityChart issues={assignedIssues} />
|
|
|
|
|
</ChartCard>
|
|
|
|
|
<ChartCard title="Issues by Status" subtitle="Last 14 days">
|
|
|
|
|
<IssueStatusChart issues={assignedIssues} />
|
|
|
|
|
</ChartCard>
|
|
|
|
|
<ChartCard title="Success Rate" subtitle="Last 14 days">
|
|
|
|
|
<SuccessRateChart runs={runs} />
|
|
|
|
|
</ChartCard>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Recent Issues */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h3 className="text-sm font-medium">Recent Issues ({assignedIssues.length})</h3>
|
|
|
|
|
{assignedIssues.length === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground">No assigned issues.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="border border-border rounded-lg">
|
|
|
|
|
{assignedIssues.slice(0, 10).map((issue) => (
|
|
|
|
|
<EntityRow
|
|
|
|
|
key={issue.id}
|
|
|
|
|
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
|
|
|
|
title={issue.title}
|
|
|
|
|
to={`/issues/${issue.identifier ?? issue.id}`}
|
|
|
|
|
trailing={<StatusBadge status={issue.status} />}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
{assignedIssues.length > 10 && (
|
|
|
|
|
<div className="px-3 py-2 text-xs text-muted-foreground text-center border-t border-border">
|
|
|
|
|
+{assignedIssues.length - 10} more issues
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Costs */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h3 className="text-sm font-medium">Costs</h3>
|
|
|
|
|
<CostsSection runtimeState={runtimeState} runs={runs} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Configuration Summary */}
|
|
|
|
|
<ConfigSummary
|
|
|
|
|
agent={agent}
|
|
|
|
|
agentId={agentId}
|
|
|
|
|
reportsToAgent={reportsToAgent}
|
|
|
|
|
directReports={directReports}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Chart Components ---- */
|
|
|
|
|
|
|
|
|
|
function getLast14Days(): string[] {
|
|
|
|
|
return Array.from({ length: 14 }, (_, i) => {
|
|
|
|
|
const d = new Date();
|
|
|
|
|
d.setDate(d.getDate() - (13 - i));
|
|
|
|
|
return d.toISOString().slice(0, 10);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDayLabel(dateStr: string): string {
|
|
|
|
|
const d = new Date(dateStr + "T12:00:00");
|
|
|
|
|
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DateLabels({ days }: { days: string[] }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex gap-[3px] mt-1.5">
|
|
|
|
|
{days.map((day, i) => (
|
|
|
|
|
<div key={day} className="flex-1 text-center overflow-hidden">
|
|
|
|
|
{(i === 0 || i === 6 || i === 13) ? (
|
|
|
|
|
<span className="text-[9px] text-muted-foreground tabular-nums">{formatDayLabel(day)}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ChartLegend({ items }: { items: { color: string; label: string }[] }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-wrap gap-x-2.5 gap-y-0.5 mt-2">
|
|
|
|
|
{items.map(item => (
|
|
|
|
|
<span key={item.label} className="flex items-center gap-1 text-[9px] text-muted-foreground">
|
|
|
|
|
<span className="h-1.5 w-1.5 rounded-full shrink-0" style={{ backgroundColor: item.color }} />
|
|
|
|
|
{item.label}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ChartCard({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="border border-border rounded-lg p-4 space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-xs font-medium text-muted-foreground">{title}</h3>
|
|
|
|
|
{subtitle && <span className="text-[10px] text-muted-foreground/60">{subtitle}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function RunActivityChart({ runs }: { runs: HeartbeatRun[] }) {
|
|
|
|
|
const days = getLast14Days();
|
|
|
|
|
|
|
|
|
|
const grouped = new Map<string, { succeeded: number; failed: number; other: number }>();
|
|
|
|
|
for (const day of days) grouped.set(day, { succeeded: 0, failed: 0, other: 0 });
|
|
|
|
|
for (const run of runs) {
|
|
|
|
|
const day = new Date(run.createdAt).toISOString().slice(0, 10);
|
|
|
|
|
const entry = grouped.get(day);
|
|
|
|
|
if (!entry) continue;
|
|
|
|
|
if (run.status === "succeeded") entry.succeeded++;
|
|
|
|
|
else if (run.status === "failed" || run.status === "timed_out") entry.failed++;
|
|
|
|
|
else entry.other++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const maxValue = Math.max(...Array.from(grouped.values()).map(v => v.succeeded + v.failed + v.other), 1);
|
|
|
|
|
const hasData = Array.from(grouped.values()).some(v => v.succeeded + v.failed + v.other > 0);
|
|
|
|
|
|
|
|
|
|
if (!hasData) return <p className="text-xs text-muted-foreground">No runs yet</p>;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-end gap-[3px] h-20">
|
|
|
|
|
{days.map(day => {
|
|
|
|
|
const entry = grouped.get(day)!;
|
|
|
|
|
const total = entry.succeeded + entry.failed + entry.other;
|
|
|
|
|
const heightPct = (total / maxValue) * 100;
|
|
|
|
|
return (
|
|
|
|
|
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${total} runs`}>
|
|
|
|
|
{total > 0 ? (
|
|
|
|
|
<div className="flex flex-col-reverse gap-px rounded-t-sm overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
|
|
|
|
|
{entry.succeeded > 0 && <div className="bg-emerald-500" style={{ flex: entry.succeeded }} />}
|
|
|
|
|
{entry.failed > 0 && <div className="bg-red-500" style={{ flex: entry.failed }} />}
|
|
|
|
|
{entry.other > 0 && <div className="bg-neutral-500" style={{ flex: entry.other }} />}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="bg-muted/30 rounded-sm" style={{ height: 2 }} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<DateLabels days={days} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const priorityColors: Record<string, string> = {
|
|
|
|
|
critical: "#ef4444",
|
|
|
|
|
high: "#f97316",
|
|
|
|
|
medium: "#eab308",
|
|
|
|
|
low: "#6b7280",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const priorityOrder = ["critical", "high", "medium", "low"] as const;
|
|
|
|
|
|
|
|
|
|
function PriorityChart({ issues }: { issues: { priority: string; createdAt: Date }[] }) {
|
|
|
|
|
const days = getLast14Days();
|
|
|
|
|
const grouped = new Map<string, Record<string, number>>();
|
|
|
|
|
for (const day of days) grouped.set(day, { critical: 0, high: 0, medium: 0, low: 0 });
|
|
|
|
|
for (const issue of issues) {
|
|
|
|
|
const day = new Date(issue.createdAt).toISOString().slice(0, 10);
|
|
|
|
|
const entry = grouped.get(day);
|
|
|
|
|
if (!entry) continue;
|
|
|
|
|
if (issue.priority in entry) entry[issue.priority]++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const maxValue = Math.max(...Array.from(grouped.values()).map(v => Object.values(v).reduce((a, b) => a + b, 0)), 1);
|
|
|
|
|
const hasData = Array.from(grouped.values()).some(v => Object.values(v).reduce((a, b) => a + b, 0) > 0);
|
|
|
|
|
|
|
|
|
|
if (!hasData) return <p className="text-xs text-muted-foreground">No issues</p>;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-end gap-[3px] h-20">
|
|
|
|
|
{days.map(day => {
|
|
|
|
|
const entry = grouped.get(day)!;
|
|
|
|
|
const total = Object.values(entry).reduce((a, b) => a + b, 0);
|
|
|
|
|
const heightPct = (total / maxValue) * 100;
|
|
|
|
|
return (
|
|
|
|
|
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${total} issues`}>
|
|
|
|
|
{total > 0 ? (
|
|
|
|
|
<div className="flex flex-col-reverse gap-px rounded-t-sm overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
|
|
|
|
|
{priorityOrder.map(p => entry[p] > 0 ? (
|
|
|
|
|
<div key={p} style={{ flex: entry[p], backgroundColor: priorityColors[p] }} />
|
|
|
|
|
) : null)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="bg-muted/30 rounded-sm" style={{ height: 2 }} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<DateLabels days={days} />
|
|
|
|
|
<ChartLegend items={priorityOrder.map(p => ({ color: priorityColors[p], label: p.charAt(0).toUpperCase() + p.slice(1) }))} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusColors: Record<string, string> = {
|
|
|
|
|
todo: "#3b82f6",
|
|
|
|
|
in_progress: "#8b5cf6",
|
|
|
|
|
in_review: "#a855f7",
|
|
|
|
|
done: "#10b981",
|
|
|
|
|
blocked: "#ef4444",
|
|
|
|
|
cancelled: "#6b7280",
|
|
|
|
|
backlog: "#64748b",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
|
|
|
todo: "To Do",
|
|
|
|
|
in_progress: "In Progress",
|
|
|
|
|
in_review: "In Review",
|
|
|
|
|
done: "Done",
|
|
|
|
|
blocked: "Blocked",
|
|
|
|
|
cancelled: "Cancelled",
|
|
|
|
|
backlog: "Backlog",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function IssueStatusChart({ issues }: { issues: { status: string; createdAt: Date }[] }) {
|
|
|
|
|
const days = getLast14Days();
|
|
|
|
|
const allStatuses = new Set<string>();
|
|
|
|
|
const grouped = new Map<string, Record<string, number>>();
|
|
|
|
|
for (const day of days) grouped.set(day, {});
|
|
|
|
|
for (const issue of issues) {
|
|
|
|
|
const day = new Date(issue.createdAt).toISOString().slice(0, 10);
|
|
|
|
|
const entry = grouped.get(day);
|
|
|
|
|
if (!entry) continue;
|
|
|
|
|
entry[issue.status] = (entry[issue.status] ?? 0) + 1;
|
|
|
|
|
allStatuses.add(issue.status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusOrder = ["todo", "in_progress", "in_review", "done", "blocked", "cancelled", "backlog"].filter(s => allStatuses.has(s));
|
|
|
|
|
const maxValue = Math.max(...Array.from(grouped.values()).map(v => Object.values(v).reduce((a, b) => a + b, 0)), 1);
|
|
|
|
|
const hasData = allStatuses.size > 0;
|
|
|
|
|
|
|
|
|
|
if (!hasData) return <p className="text-xs text-muted-foreground">No issues</p>;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-end gap-[3px] h-20">
|
|
|
|
|
{days.map(day => {
|
|
|
|
|
const entry = grouped.get(day)!;
|
|
|
|
|
const total = Object.values(entry).reduce((a, b) => a + b, 0);
|
|
|
|
|
const heightPct = (total / maxValue) * 100;
|
|
|
|
|
return (
|
|
|
|
|
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${total} issues`}>
|
|
|
|
|
{total > 0 ? (
|
|
|
|
|
<div className="flex flex-col-reverse gap-px rounded-t-sm overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
|
|
|
|
|
{statusOrder.map(s => (entry[s] ?? 0) > 0 ? (
|
|
|
|
|
<div key={s} style={{ flex: entry[s], backgroundColor: statusColors[s] ?? "#6b7280" }} />
|
|
|
|
|
) : null)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="bg-muted/30 rounded-sm" style={{ height: 2 }} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<DateLabels days={days} />
|
|
|
|
|
<ChartLegend items={statusOrder.map(s => ({ color: statusColors[s] ?? "#6b7280", label: statusLabels[s] ?? s }))} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SuccessRateChart({ runs }: { runs: HeartbeatRun[] }) {
|
|
|
|
|
const days = getLast14Days();
|
|
|
|
|
const grouped = new Map<string, { succeeded: number; total: number }>();
|
|
|
|
|
for (const day of days) grouped.set(day, { succeeded: 0, total: 0 });
|
|
|
|
|
for (const run of runs) {
|
|
|
|
|
const day = new Date(run.createdAt).toISOString().slice(0, 10);
|
|
|
|
|
const entry = grouped.get(day);
|
|
|
|
|
if (!entry) continue;
|
|
|
|
|
entry.total++;
|
|
|
|
|
if (run.status === "succeeded") entry.succeeded++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hasData = Array.from(grouped.values()).some(v => v.total > 0);
|
|
|
|
|
if (!hasData) return <p className="text-xs text-muted-foreground">No runs yet</p>;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-end gap-[3px] h-20">
|
|
|
|
|
{days.map(day => {
|
|
|
|
|
const entry = grouped.get(day)!;
|
|
|
|
|
const rate = entry.total > 0 ? entry.succeeded / entry.total : 0;
|
|
|
|
|
const color = entry.total === 0 ? undefined : rate >= 0.8 ? "#10b981" : rate >= 0.5 ? "#eab308" : "#ef4444";
|
|
|
|
|
return (
|
|
|
|
|
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${entry.total > 0 ? Math.round(rate * 100) : 0}% (${entry.succeeded}/${entry.total})`}>
|
|
|
|
|
{entry.total > 0 ? (
|
|
|
|
|
<div className="rounded-t-sm" style={{ height: `${rate * 100}%`, minHeight: 2, backgroundColor: color }} />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="bg-muted/30 rounded-sm" style={{ height: 2 }} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<DateLabels days={days} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Configuration Summary ---- */
|
|
|
|
|
|
|
|
|
|
function ConfigSummary({
|
|
|
|
|
agent,
|
|
|
|
|
agentId,
|
|
|
|
|
reportsToAgent,
|
|
|
|
|
directReports,
|
|
|
|
|
}: {
|
|
|
|
|
agent: Agent;
|
|
|
|
|
agentId: string;
|
|
|
|
|
reportsToAgent: Agent | null;
|
|
|
|
|
directReports: Agent[];
|
|
|
|
|
}) {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const config = agent.adapterConfig as Record<string, unknown>;
|
|
|
|
|
const promptText = typeof config?.promptTemplate === "string" ? config.promptTemplate : "";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<h3 className="text-sm font-medium">Configuration</h3>
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
|
|
|
onClick={() => navigate(`/agents/${agentId}/configure`)}
|
|
|
|
|
>
|
|
|
|
|
<Settings className="h-3 w-3" />
|
|
|
|
|
Manage →
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<div className="border border-border rounded-lg p-4 space-y-3">
|
|
|
|
|
<h4 className="text-xs text-muted-foreground font-medium">Agent Details</h4>
|
|
|
|
|
<div className="space-y-2 text-sm">
|
|
|
|
|
<SummaryRow label="Adapter">
|
|
|
|
|
<span className="font-mono">{adapterLabels[agent.adapterType] ?? agent.adapterType}</span>
|
|
|
|
|
{String(config?.model ?? "") !== "" && (
|
|
|
|
|
<span className="text-muted-foreground ml-1">
|
|
|
|
|
({String(config.model)})
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</SummaryRow>
|
|
|
|
|
<SummaryRow label="Heartbeat">
|
|
|
|
|
{(agent.runtimeConfig as Record<string, unknown>)?.heartbeat
|
|
|
|
|
? (() => {
|
|
|
|
|
const hb = (agent.runtimeConfig as Record<string, unknown>).heartbeat as Record<string, unknown>;
|
|
|
|
|
if (!hb.enabled) return <span className="text-muted-foreground">Disabled</span>;
|
|
|
|
|
const sec = Number(hb.intervalSec) || 300;
|
|
|
|
|
const maxConcurrentRuns = Math.max(1, Math.floor(Number(hb.maxConcurrentRuns) || 1));
|
|
|
|
|
const intervalLabel = sec >= 60 ? `${Math.round(sec / 60)} min` : `${sec}s`;
|
|
|
|
|
return (
|
|
|
|
|
<span>
|
|
|
|
|
Every {intervalLabel}
|
|
|
|
|
{maxConcurrentRuns > 1 ? ` (max ${maxConcurrentRuns} concurrent)` : ""}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()
|
|
|
|
|
: <span className="text-muted-foreground">Not configured</span>
|
|
|
|
|
}
|
|
|
|
|
</SummaryRow>
|
|
|
|
|
<SummaryRow label="Last heartbeat">
|
|
|
|
|
{agent.lastHeartbeatAt
|
|
|
|
|
? <span>{relativeTime(agent.lastHeartbeatAt)}</span>
|
|
|
|
|
: <span className="text-muted-foreground">Never</span>
|
|
|
|
|
}
|
|
|
|
|
</SummaryRow>
|
|
|
|
|
<SummaryRow label="Reports to">
|
|
|
|
|
{reportsToAgent ? (
|
|
|
|
|
<Link
|
|
|
|
|
to={`/agents/${reportsToAgent.id}`}
|
|
|
|
|
className="text-blue-400 hover:underline"
|
|
|
|
|
>
|
|
|
|
|
<Identity name={reportsToAgent.name} size="sm" />
|
|
|
|
|
</Link>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Nobody (top-level)</span>
|
|
|
|
|
)}
|
|
|
|
|
</SummaryRow>
|
|
|
|
|
</div>
|
|
|
|
|
{directReports.length > 0 && (
|
|
|
|
|
<div className="pt-1">
|
|
|
|
|
<span className="text-xs text-muted-foreground">Direct reports</span>
|
|
|
|
|
<div className="mt-1 space-y-1">
|
|
|
|
|
{directReports.map((r) => (
|
|
|
|
|
<Link
|
|
|
|
|
key={r.id}
|
|
|
|
|
to={`/agents/${r.id}`}
|
|
|
|
|
className="flex items-center gap-2 text-sm text-blue-400 hover:underline"
|
|
|
|
|
>
|
|
|
|
|
<span className="relative flex h-2 w-2">
|
|
|
|
|
<span className={`absolute inline-flex h-full w-full rounded-full ${
|
|
|
|
|
r.status === "active"
|
|
|
|
|
? "bg-green-400"
|
|
|
|
|
: r.status === "pending_approval"
|
|
|
|
|
? "bg-amber-400"
|
|
|
|
|
: r.status === "error"
|
|
|
|
|
? "bg-red-400"
|
|
|
|
|
: "bg-neutral-400"
|
|
|
|
|
}`} />
|
|
|
|
|
</span>
|
|
|
|
|
{r.name}
|
|
|
|
|
<span className="text-muted-foreground text-xs">({roleLabels[r.role] ?? r.role})</span>
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{agent.capabilities && (
|
|
|
|
|
<div className="pt-1">
|
|
|
|
|
<span className="text-xs text-muted-foreground">Capabilities</span>
|
|
|
|
|
<p className="text-sm mt-0.5">{agent.capabilities}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{promptText && (
|
|
|
|
|
<div className="border border-border rounded-lg p-4 space-y-2">
|
|
|
|
|
<h4 className="text-xs text-muted-foreground font-medium">Prompt Template</h4>
|
|
|
|
|
<pre className="text-xs text-muted-foreground line-clamp-[12] font-mono whitespace-pre-wrap">{promptText}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Costs Section (inline) ---- */
|
|
|
|
|
|
|
|
|
|
function CostsSection({
|
|
|
|
|
runtimeState,
|
|
|
|
|
runs,
|
|
|
|
|
}: {
|
|
|
|
|
runtimeState?: AgentRuntimeState;
|
|
|
|
|
runs: HeartbeatRun[];
|
|
|
|
|
}) {
|
|
|
|
|
const runsWithCost = runs
|
|
|
|
|
.filter((r) => {
|
|
|
|
|
const u = r.usageJson as Record<string, unknown> | null;
|
|
|
|
|
return u && (u.cost_usd || u.total_cost_usd || u.input_tokens);
|
|
|
|
|
})
|
|
|
|
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{runtimeState && (
|
|
|
|
|
<div className="border border-border rounded-lg p-4">
|
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-xs text-muted-foreground block">Input tokens</span>
|
|
|
|
|
<span className="text-lg font-semibold">{formatTokens(runtimeState.totalInputTokens)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-xs text-muted-foreground block">Output tokens</span>
|
|
|
|
|
<span className="text-lg font-semibold">{formatTokens(runtimeState.totalOutputTokens)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-xs text-muted-foreground block">Cached tokens</span>
|
|
|
|
|
<span className="text-lg font-semibold">{formatTokens(runtimeState.totalCachedInputTokens)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-xs text-muted-foreground block">Total cost</span>
|
|
|
|
|
<span className="text-lg font-semibold">{formatCents(runtimeState.totalCostCents)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{runsWithCost.length > 0 && (
|
|
|
|
|
<div className="border border-border rounded-lg overflow-hidden">
|
|
|
|
|
<table className="w-full text-xs">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="border-b border-border bg-accent/20">
|
|
|
|
|
<th className="text-left px-3 py-2 font-medium text-muted-foreground">Date</th>
|
|
|
|
|
<th className="text-left px-3 py-2 font-medium text-muted-foreground">Run</th>
|
|
|
|
|
<th className="text-right px-3 py-2 font-medium text-muted-foreground">Input</th>
|
|
|
|
|
<th className="text-right px-3 py-2 font-medium text-muted-foreground">Output</th>
|
|
|
|
|
<th className="text-right px-3 py-2 font-medium text-muted-foreground">Cost</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{runsWithCost.slice(0, 10).map((run) => {
|
|
|
|
|
const u = run.usageJson as Record<string, unknown>;
|
|
|
|
|
return (
|
|
|
|
|
<tr key={run.id} className="border-b border-border last:border-b-0">
|
|
|
|
|
<td className="px-3 py-2">{formatDate(run.createdAt)}</td>
|
|
|
|
|
<td className="px-3 py-2 font-mono">{run.id.slice(0, 8)}</td>
|
|
|
|
|
<td className="px-3 py-2 text-right">{formatTokens(Number(u.input_tokens ?? 0))}</td>
|
|
|
|
|
<td className="px-3 py-2 text-right">{formatTokens(Number(u.output_tokens ?? 0))}</td>
|
|
|
|
|
<td className="px-3 py-2 text-right">
|
|
|
|
|
{(u.cost_usd || u.total_cost_usd)
|
|
|
|
|
? `$${Number(u.cost_usd ?? u.total_cost_usd ?? 0).toFixed(4)}`
|
|
|
|
|
: "-"
|
|
|
|
|
}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
2026-02-19 14:02:29 -06:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
/* ---- Agent Configure Page ---- */
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
function AgentConfigurePage({
|
2026-02-18 13:02:23 -06:00
|
|
|
agent,
|
2026-02-23 14:41:21 -06:00
|
|
|
agentId,
|
2026-02-18 13:02:23 -06:00
|
|
|
onDirtyChange,
|
|
|
|
|
onSaveActionChange,
|
|
|
|
|
onCancelActionChange,
|
|
|
|
|
onSavingChange,
|
2026-02-19 14:39:48 -06:00
|
|
|
updatePermissions,
|
2026-02-18 13:02:23 -06:00
|
|
|
}: {
|
|
|
|
|
agent: Agent;
|
2026-02-23 14:41:21 -06:00
|
|
|
agentId: string;
|
2026-02-18 13:02:23 -06:00
|
|
|
onDirtyChange: (dirty: boolean) => void;
|
|
|
|
|
onSaveActionChange: (save: (() => void) | null) => void;
|
|
|
|
|
onCancelActionChange: (cancel: (() => void) | null) => void;
|
|
|
|
|
onSavingChange: (saving: boolean) => void;
|
2026-02-19 14:39:48 -06:00
|
|
|
updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean };
|
2026-02-18 13:02:23 -06:00
|
|
|
}) {
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const queryClient = useQueryClient();
|
2026-02-23 14:41:21 -06:00
|
|
|
const [revisionsOpen, setRevisionsOpen] = useState(false);
|
2026-02-17 20:07:30 -06:00
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
const { data: configRevisions } = useQuery({
|
|
|
|
|
queryKey: queryKeys.agents.configRevisions(agent.id),
|
|
|
|
|
queryFn: () => agentsApi.listConfigRevisions(agent.id),
|
2026-02-17 20:07:30 -06:00
|
|
|
});
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
const rollbackConfig = useMutation({
|
|
|
|
|
mutationFn: (revisionId: string) => agentsApi.rollbackConfigRevision(agent.id, revisionId),
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) });
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.configRevisions(agent.id) });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
return (
|
|
|
|
|
<div className="max-w-3xl space-y-6">
|
|
|
|
|
<ConfigurationTab
|
|
|
|
|
agent={agent}
|
|
|
|
|
onDirtyChange={onDirtyChange}
|
|
|
|
|
onSaveActionChange={onSaveActionChange}
|
|
|
|
|
onCancelActionChange={onCancelActionChange}
|
|
|
|
|
onSavingChange={onSavingChange}
|
|
|
|
|
updatePermissions={updatePermissions}
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-sm font-medium mb-3">API Keys</h3>
|
|
|
|
|
<KeysTab agentId={agentId} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Configuration Revisions — collapsible at the bottom */}
|
|
|
|
|
<div>
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-2 text-sm font-medium hover:text-foreground transition-colors"
|
|
|
|
|
onClick={() => setRevisionsOpen((v) => !v)}
|
|
|
|
|
>
|
|
|
|
|
{revisionsOpen
|
|
|
|
|
? <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
: <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
}
|
|
|
|
|
Configuration Revisions
|
|
|
|
|
<span className="text-xs font-normal text-muted-foreground">{configRevisions?.length ?? 0}</span>
|
|
|
|
|
</button>
|
|
|
|
|
{revisionsOpen && (
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
{(configRevisions ?? []).length === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground">No configuration revisions yet.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{(configRevisions ?? []).slice(0, 10).map((revision) => (
|
|
|
|
|
<div key={revision.id} className="border border-border/70 rounded-md p-3 space-y-2">
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<div className="text-xs text-muted-foreground">
|
|
|
|
|
<span className="font-mono">{revision.id.slice(0, 8)}</span>
|
|
|
|
|
<span className="mx-1">·</span>
|
|
|
|
|
<span>{formatDate(revision.createdAt)}</span>
|
|
|
|
|
<span className="mx-1">·</span>
|
|
|
|
|
<span>{revision.source}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="h-7 px-2.5 text-xs"
|
|
|
|
|
onClick={() => rollbackConfig.mutate(revision.id)}
|
|
|
|
|
disabled={rollbackConfig.isPending}
|
|
|
|
|
>
|
|
|
|
|
Restore
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Changed:{" "}
|
|
|
|
|
{revision.changedKeys.length > 0 ? revision.changedKeys.join(", ") : "no tracked changes"}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Configuration Tab ---- */
|
|
|
|
|
|
|
|
|
|
function ConfigurationTab({
|
|
|
|
|
agent,
|
|
|
|
|
onDirtyChange,
|
|
|
|
|
onSaveActionChange,
|
|
|
|
|
onCancelActionChange,
|
|
|
|
|
onSavingChange,
|
|
|
|
|
updatePermissions,
|
|
|
|
|
}: {
|
|
|
|
|
agent: Agent;
|
|
|
|
|
onDirtyChange: (dirty: boolean) => void;
|
|
|
|
|
onSaveActionChange: (save: (() => void) | null) => void;
|
|
|
|
|
onCancelActionChange: (cancel: (() => void) | null) => void;
|
|
|
|
|
onSavingChange: (saving: boolean) => void;
|
|
|
|
|
updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean };
|
|
|
|
|
}) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
const { data: adapterModels } = useQuery({
|
|
|
|
|
queryKey: ["adapter-models", agent.adapterType],
|
|
|
|
|
queryFn: () => agentsApi.adapterModels(agent.adapterType),
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
});
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
const updateAgent = useMutation({
|
|
|
|
|
mutationFn: (data: Record<string, unknown>) => agentsApi.update(agent.id, data),
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.configRevisions(agent.id) });
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-18 13:02:23 -06:00
|
|
|
useEffect(() => {
|
|
|
|
|
onSavingChange(updateAgent.isPending);
|
|
|
|
|
}, [onSavingChange, updateAgent.isPending]);
|
|
|
|
|
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
return (
|
2026-02-23 14:41:21 -06:00
|
|
|
<div className="space-y-6">
|
|
|
|
|
<AgentConfigForm
|
|
|
|
|
mode="edit"
|
|
|
|
|
agent={agent}
|
|
|
|
|
onSave={(patch) => updateAgent.mutate(patch)}
|
|
|
|
|
isSaving={updateAgent.isPending}
|
|
|
|
|
adapterModels={adapterModels}
|
|
|
|
|
onDirtyChange={onDirtyChange}
|
|
|
|
|
onSaveActionChange={onSaveActionChange}
|
|
|
|
|
onCancelActionChange={onCancelActionChange}
|
|
|
|
|
hideInlineSave
|
|
|
|
|
sectionLayout="cards"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-sm font-medium mb-3">Permissions</h3>
|
|
|
|
|
<div className="border border-border rounded-lg p-4">
|
|
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
|
|
|
<span>Can create new agents</span>
|
|
|
|
|
<Button
|
|
|
|
|
variant={agent.permissions?.canCreateAgents ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 px-2.5 text-xs"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
updatePermissions.mutate(!Boolean(agent.permissions?.canCreateAgents))
|
|
|
|
|
}
|
|
|
|
|
disabled={updatePermissions.isPending}
|
|
|
|
|
>
|
|
|
|
|
{agent.permissions?.canCreateAgents ? "Enabled" : "Disabled"}
|
|
|
|
|
</Button>
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
</div>
|
2026-02-23 14:41:21 -06:00
|
|
|
</div>
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
</div>
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Runs Tab ---- */
|
|
|
|
|
|
2026-02-20 10:33:18 -06:00
|
|
|
function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelected: boolean; agentId: string }) {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
|
|
|
|
|
const StatusIcon = statusInfo.icon;
|
|
|
|
|
const metrics = runMetrics(run);
|
|
|
|
|
const summary = run.resultJson
|
|
|
|
|
? String((run.resultJson as Record<string, unknown>).summary ?? (run.resultJson as Record<string, unknown>).result ?? "")
|
|
|
|
|
: run.error ?? "";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex flex-col gap-1 w-full px-3 py-2.5 text-left border-b border-border last:border-b-0 transition-colors",
|
|
|
|
|
isSelected ? "bg-accent/40" : "hover:bg-accent/20",
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => navigate(isSelected ? `/agents/${agentId}/runs` : `/agents/${agentId}/runs/${run.id}`)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<StatusIcon className={cn("h-3.5 w-3.5 shrink-0", statusInfo.color, run.status === "running" && "animate-spin")} />
|
|
|
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
|
|
|
{run.id.slice(0, 8)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className={cn(
|
|
|
|
|
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium shrink-0",
|
|
|
|
|
run.invocationSource === "timer" ? "bg-blue-900/50 text-blue-300"
|
|
|
|
|
: run.invocationSource === "assignment" ? "bg-violet-900/50 text-violet-300"
|
|
|
|
|
: run.invocationSource === "on_demand" ? "bg-cyan-900/50 text-cyan-300"
|
|
|
|
|
: "bg-neutral-800 text-neutral-400"
|
|
|
|
|
)}>
|
|
|
|
|
{sourceLabels[run.invocationSource] ?? run.invocationSource}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="ml-auto text-[11px] text-muted-foreground shrink-0">
|
|
|
|
|
{relativeTime(run.createdAt)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{summary && (
|
|
|
|
|
<span className="text-xs text-muted-foreground truncate pl-5.5">
|
|
|
|
|
{summary.slice(0, 60)}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{(metrics.totalTokens > 0 || metrics.cost > 0) && (
|
|
|
|
|
<div className="flex items-center gap-2 pl-5.5 text-[11px] text-muted-foreground">
|
|
|
|
|
{metrics.totalTokens > 0 && <span>{formatTokens(metrics.totalTokens)} tok</span>}
|
|
|
|
|
{metrics.cost > 0 && <span>${metrics.cost.toFixed(3)}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 13:53:03 -06:00
|
|
|
function RunsTab({ runs, companyId, agentId, selectedRunId, adapterType }: { runs: HeartbeatRun[]; companyId: string; agentId: string; selectedRunId: string | null; adapterType: string }) {
|
2026-02-18 13:02:23 -06:00
|
|
|
const navigate = useNavigate();
|
2026-02-20 10:33:18 -06:00
|
|
|
const { isMobile } = useSidebar();
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
|
|
|
|
if (runs.length === 0) {
|
|
|
|
|
return <p className="text-sm text-muted-foreground">No runs yet.</p>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort by created descending
|
|
|
|
|
const sorted = [...runs].sort(
|
|
|
|
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-20 10:33:18 -06:00
|
|
|
// On mobile, don't auto-select so the list shows first; on desktop, auto-select latest
|
|
|
|
|
const effectiveRunId = isMobile ? selectedRunId : (selectedRunId ?? sorted[0]?.id ?? null);
|
2026-02-18 13:02:23 -06:00
|
|
|
const selectedRun = sorted.find((r) => r.id === effectiveRunId) ?? null;
|
|
|
|
|
|
2026-02-20 10:33:18 -06:00
|
|
|
// Mobile: show either run list OR run detail with back button
|
|
|
|
|
if (isMobile) {
|
|
|
|
|
if (selectedRun) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
|
|
|
onClick={() => navigate(`/agents/${agentId}/runs`)}
|
|
|
|
|
>
|
|
|
|
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
|
|
|
Back to runs
|
|
|
|
|
</button>
|
|
|
|
|
<RunDetail key={selectedRun.id} run={selectedRun} adapterType={adapterType} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div className="border border-border rounded-lg">
|
|
|
|
|
{sorted.map((run) => (
|
|
|
|
|
<RunListItem key={run.id} run={run} isSelected={false} agentId={agentId} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Desktop: side-by-side layout
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
return (
|
2026-02-18 13:17:03 -06:00
|
|
|
<div className="flex gap-0">
|
2026-02-18 16:46:55 -06:00
|
|
|
{/* Left: run list — border stretches full height, content sticks */}
|
2026-02-18 13:02:23 -06:00
|
|
|
<div className={cn(
|
2026-02-18 16:46:55 -06:00
|
|
|
"shrink-0 border border-border rounded-lg",
|
2026-02-18 13:02:23 -06:00
|
|
|
selectedRun ? "w-72" : "w-full",
|
2026-02-18 16:46:55 -06:00
|
|
|
)}>
|
|
|
|
|
<div className="sticky top-4 overflow-y-auto" style={{ maxHeight: "calc(100vh - 2rem)" }}>
|
2026-02-20 10:33:18 -06:00
|
|
|
{sorted.map((run) => (
|
|
|
|
|
<RunListItem key={run.id} run={run} isSelected={run.id === effectiveRunId} agentId={agentId} />
|
|
|
|
|
))}
|
2026-02-18 16:46:55 -06:00
|
|
|
</div>
|
2026-02-18 13:02:23 -06:00
|
|
|
</div>
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
2026-02-18 13:17:03 -06:00
|
|
|
{/* Right: run detail — natural height, page scrolls */}
|
2026-02-18 13:02:23 -06:00
|
|
|
{selectedRun && (
|
2026-02-18 13:17:03 -06:00
|
|
|
<div className="flex-1 min-w-0 pl-4">
|
2026-02-18 13:53:03 -06:00
|
|
|
<RunDetail key={selectedRun.id} run={selectedRun} adapterType={adapterType} />
|
2026-02-18 13:02:23 -06:00
|
|
|
</div>
|
|
|
|
|
)}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Run Detail (expanded) ---- */
|
|
|
|
|
|
2026-02-18 13:53:03 -06:00
|
|
|
function RunDetail({ run, adapterType }: { run: HeartbeatRun; adapterType: string }) {
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const queryClient = useQueryClient();
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const navigate = useNavigate();
|
2026-02-18 13:02:23 -06:00
|
|
|
const metrics = runMetrics(run);
|
2026-02-18 15:29:29 -06:00
|
|
|
const [sessionOpen, setSessionOpen] = useState(false);
|
2026-02-23 12:25:13 -06:00
|
|
|
const [claudeLoginResult, setClaudeLoginResult] = useState<ClaudeLoginResult | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setClaudeLoginResult(null);
|
|
|
|
|
}, [run.id]);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
|
|
|
|
const cancelRun = useMutation({
|
|
|
|
|
mutationFn: () => heartbeatsApi.cancel(run.id),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(run.companyId, run.agentId) });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const { data: touchedIssues } = useQuery({
|
|
|
|
|
queryKey: queryKeys.runIssues(run.id),
|
|
|
|
|
queryFn: () => activityApi.issuesForRun(run.id),
|
|
|
|
|
});
|
2026-02-20 10:32:32 -06:00
|
|
|
const touchedIssueIds = useMemo(
|
|
|
|
|
() => Array.from(new Set((touchedIssues ?? []).map((issue) => issue.issueId))),
|
|
|
|
|
[touchedIssues],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const clearSessionsForTouchedIssues = useMutation({
|
|
|
|
|
mutationFn: async () => {
|
|
|
|
|
if (touchedIssueIds.length === 0) return 0;
|
|
|
|
|
await Promise.all(touchedIssueIds.map((issueId) => agentsApi.resetSession(run.agentId, issueId)));
|
|
|
|
|
return touchedIssueIds.length;
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(run.agentId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(run.agentId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.runIssues(run.id) });
|
|
|
|
|
},
|
|
|
|
|
});
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
|
2026-02-23 12:25:13 -06:00
|
|
|
const runClaudeLogin = useMutation({
|
|
|
|
|
mutationFn: () => agentsApi.loginWithClaude(run.agentId),
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
setClaudeLoginResult(data);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-18 15:29:29 -06:00
|
|
|
const timeFormat: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false };
|
|
|
|
|
const startTime = run.startedAt ? new Date(run.startedAt).toLocaleTimeString("en-US", timeFormat) : null;
|
|
|
|
|
const endTime = run.finishedAt ? new Date(run.finishedAt).toLocaleTimeString("en-US", timeFormat) : null;
|
|
|
|
|
const durationSec = run.startedAt && run.finishedAt
|
|
|
|
|
? Math.round((new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()) / 1000)
|
|
|
|
|
: null;
|
|
|
|
|
const hasMetrics = metrics.input > 0 || metrics.output > 0 || metrics.cached > 0 || metrics.cost > 0;
|
|
|
|
|
const hasSession = !!(run.sessionIdBefore || run.sessionIdAfter);
|
|
|
|
|
const sessionChanged = run.sessionIdBefore && run.sessionIdAfter && run.sessionIdBefore !== run.sessionIdAfter;
|
|
|
|
|
const sessionId = run.sessionIdAfter || run.sessionIdBefore;
|
|
|
|
|
const hasNonZeroExit = run.exitCode !== null && run.exitCode !== 0;
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
2026-02-18 15:29:29 -06:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* Run summary card */}
|
|
|
|
|
<div className="border border-border rounded-lg overflow-hidden">
|
|
|
|
|
<div className="flex">
|
|
|
|
|
{/* Left column: status + timing */}
|
|
|
|
|
<div className="flex-1 p-4 space-y-3">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<StatusBadge status={run.status} />
|
|
|
|
|
{(run.status === "running" || run.status === "queued") && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-destructive hover:text-destructive text-xs h-6 px-2"
|
|
|
|
|
onClick={() => cancelRun.mutate()}
|
|
|
|
|
disabled={cancelRun.isPending}
|
|
|
|
|
>
|
|
|
|
|
{cancelRun.isPending ? "Cancelling..." : "Cancel"}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</div>
|
2026-02-18 15:29:29 -06:00
|
|
|
{startTime && (
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<div className="text-sm font-mono">
|
|
|
|
|
{startTime}
|
|
|
|
|
{endTime && <span className="text-muted-foreground"> → </span>}
|
|
|
|
|
{endTime}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">
|
|
|
|
|
{relativeTime(run.startedAt!)}
|
|
|
|
|
{run.finishedAt && <> → {relativeTime(run.finishedAt)}</>}
|
|
|
|
|
</div>
|
|
|
|
|
{durationSec !== null && (
|
|
|
|
|
<div className="text-xs text-muted-foreground">
|
|
|
|
|
Duration: {durationSec >= 60 ? `${Math.floor(durationSec / 60)}m ${durationSec % 60}s` : `${durationSec}s`}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{run.error && (
|
|
|
|
|
<div className="text-xs">
|
|
|
|
|
<span className="text-red-400">{run.error}</span>
|
|
|
|
|
{run.errorCode && <span className="text-muted-foreground ml-1">({run.errorCode})</span>}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-23 12:25:13 -06:00
|
|
|
{run.errorCode === "claude_auth_required" && adapterType === "claude_local" && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 px-2 text-xs"
|
|
|
|
|
onClick={() => runClaudeLogin.mutate()}
|
|
|
|
|
disabled={runClaudeLogin.isPending}
|
|
|
|
|
>
|
|
|
|
|
{runClaudeLogin.isPending ? "Running claude login..." : "Login to Claude Code"}
|
|
|
|
|
</Button>
|
|
|
|
|
{runClaudeLogin.isError && (
|
|
|
|
|
<p className="text-xs text-destructive">
|
|
|
|
|
{runClaudeLogin.error instanceof Error
|
|
|
|
|
? runClaudeLogin.error.message
|
|
|
|
|
: "Failed to run Claude login"}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{claudeLoginResult?.loginUrl && (
|
|
|
|
|
<p className="text-xs">
|
|
|
|
|
Login URL:
|
|
|
|
|
<a
|
|
|
|
|
href={claudeLoginResult.loginUrl}
|
|
|
|
|
className="text-blue-400 underline underline-offset-2 ml-1 break-all"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
>
|
|
|
|
|
{claudeLoginResult.loginUrl}
|
|
|
|
|
</a>
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{claudeLoginResult && (
|
|
|
|
|
<>
|
|
|
|
|
{!!claudeLoginResult.stdout && (
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">
|
|
|
|
|
{claudeLoginResult.stdout}
|
|
|
|
|
</pre>
|
|
|
|
|
)}
|
|
|
|
|
{!!claudeLoginResult.stderr && (
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-3 text-xs font-mono text-red-300 overflow-x-auto whitespace-pre-wrap">
|
|
|
|
|
{claudeLoginResult.stderr}
|
|
|
|
|
</pre>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-18 15:29:29 -06:00
|
|
|
{hasNonZeroExit && (
|
|
|
|
|
<div className="text-xs text-red-400">
|
|
|
|
|
Exit code {run.exitCode}
|
|
|
|
|
{run.signal && <span className="text-muted-foreground ml-1">(signal: {run.signal})</span>}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
2026-02-18 15:29:29 -06:00
|
|
|
{/* Right column: metrics */}
|
|
|
|
|
{hasMetrics && (
|
|
|
|
|
<div className="border-l border-border p-4 grid grid-cols-2 gap-x-8 gap-y-3 content-center">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">Input</div>
|
|
|
|
|
<div className="text-sm font-medium font-mono">{formatTokens(metrics.input)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">Output</div>
|
|
|
|
|
<div className="text-sm font-medium font-mono">{formatTokens(metrics.output)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">Cached</div>
|
|
|
|
|
<div className="text-sm font-medium font-mono">{formatTokens(metrics.cached)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">Cost</div>
|
|
|
|
|
<div className="text-sm font-medium font-mono">{metrics.cost > 0 ? `$${metrics.cost.toFixed(4)}` : "-"}</div>
|
|
|
|
|
</div>
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-18 15:29:29 -06:00
|
|
|
{/* Collapsible session row */}
|
|
|
|
|
{hasSession && (
|
|
|
|
|
<div className="border-t border-border">
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-1.5 w-full px-4 py-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
|
|
|
onClick={() => setSessionOpen((v) => !v)}
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight className={cn("h-3 w-3 transition-transform", sessionOpen && "rotate-90")} />
|
|
|
|
|
Session
|
|
|
|
|
{sessionChanged && <span className="text-yellow-400 ml-1">(changed)</span>}
|
|
|
|
|
</button>
|
|
|
|
|
{sessionOpen && (
|
|
|
|
|
<div className="px-4 pb-3 space-y-1 text-xs">
|
|
|
|
|
{run.sessionIdBefore && (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-muted-foreground w-12">{sessionChanged ? "Before" : "ID"}</span>
|
|
|
|
|
<CopyText text={run.sessionIdBefore} className="font-mono" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{sessionChanged && run.sessionIdAfter && (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-muted-foreground w-12">After</span>
|
|
|
|
|
<CopyText text={run.sessionIdAfter} className="font-mono" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-20 10:32:32 -06:00
|
|
|
{touchedIssueIds.length > 0 && (
|
|
|
|
|
<div className="pt-1">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="text-[11px] text-muted-foreground underline underline-offset-2 hover:text-foreground disabled:opacity-60"
|
|
|
|
|
disabled={clearSessionsForTouchedIssues.isPending}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const issueCount = touchedIssueIds.length;
|
|
|
|
|
const confirmed = window.confirm(
|
|
|
|
|
`Clear session for ${issueCount} issue${issueCount === 1 ? "" : "s"} touched by this run?`,
|
|
|
|
|
);
|
|
|
|
|
if (!confirmed) return;
|
|
|
|
|
clearSessionsForTouchedIssues.mutate();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{clearSessionsForTouchedIssues.isPending
|
|
|
|
|
? "clearing session..."
|
|
|
|
|
: "clear session for these issues"}
|
|
|
|
|
</button>
|
|
|
|
|
{clearSessionsForTouchedIssues.isError && (
|
|
|
|
|
<p className="text-[11px] text-destructive mt-1">
|
|
|
|
|
{clearSessionsForTouchedIssues.error instanceof Error
|
|
|
|
|
? clearSessionsForTouchedIssues.error.message
|
|
|
|
|
: "Failed to clear sessions"}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-18 15:29:29 -06:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
{/* Issues touched by this run */}
|
|
|
|
|
{touchedIssues && touchedIssues.length > 0 && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">Issues Touched ({touchedIssues.length})</span>
|
|
|
|
|
<div className="border border-border rounded-lg divide-y divide-border">
|
|
|
|
|
{touchedIssues.map((issue) => (
|
|
|
|
|
<button
|
|
|
|
|
key={issue.issueId}
|
|
|
|
|
className="flex items-center justify-between w-full px-3 py-2 text-xs hover:bg-accent/20 transition-colors text-left"
|
2026-02-20 16:04:05 -06:00
|
|
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.issueId}`)}
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
|
|
|
<StatusBadge status={issue.status} />
|
|
|
|
|
<span className="truncate">{issue.title}</span>
|
|
|
|
|
</div>
|
2026-02-20 16:04:05 -06:00
|
|
|
<span className="font-mono text-muted-foreground shrink-0 ml-2">{issue.identifier ?? issue.issueId.slice(0, 8)}</span>
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-18 13:02:23 -06:00
|
|
|
{/* stderr excerpt for failed runs */}
|
|
|
|
|
{run.stderrExcerpt && (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<span className="text-xs font-medium text-red-400">stderr</span>
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-3 text-xs font-mono text-red-300 overflow-x-auto whitespace-pre-wrap">{run.stderrExcerpt}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* stdout excerpt when no log is available */}
|
|
|
|
|
{run.stdoutExcerpt && !run.logRef && (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">stdout</span>
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">{run.stdoutExcerpt}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
{/* Log viewer */}
|
2026-02-18 13:53:03 -06:00
|
|
|
<LogViewer run={run} adapterType={adapterType} />
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Log Viewer ---- */
|
|
|
|
|
|
2026-02-18 13:53:03 -06:00
|
|
|
function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: string }) {
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const [events, setEvents] = useState<HeartbeatRunEvent[]>([]);
|
2026-02-18 13:02:23 -06:00
|
|
|
const [logLines, setLogLines] = useState<Array<{ ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }>>([]);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const [loading, setLoading] = useState(true);
|
2026-02-18 13:02:23 -06:00
|
|
|
const [logLoading, setLogLoading] = useState(!!run.logRef);
|
|
|
|
|
const [logError, setLogError] = useState<string | null>(null);
|
|
|
|
|
const [logOffset, setLogOffset] = useState(0);
|
2026-02-20 15:48:42 -06:00
|
|
|
const [isFollowing, setIsFollowing] = useState(false);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
const logEndRef = useRef<HTMLDivElement>(null);
|
2026-02-18 13:02:23 -06:00
|
|
|
const pendingLogLineRef = useRef("");
|
2026-02-20 15:48:42 -06:00
|
|
|
const scrollContainerRef = useRef<ScrollContainer | null>(null);
|
|
|
|
|
const isFollowingRef = useRef(false);
|
|
|
|
|
const lastMetricsRef = useRef<{ scrollHeight: number; distanceFromBottom: number }>({
|
|
|
|
|
scrollHeight: 0,
|
|
|
|
|
distanceFromBottom: Number.POSITIVE_INFINITY,
|
|
|
|
|
});
|
2026-02-18 13:02:23 -06:00
|
|
|
const isLive = run.status === "running" || run.status === "queued";
|
|
|
|
|
|
|
|
|
|
function appendLogContent(content: string, finalize = false) {
|
|
|
|
|
if (!content && !finalize) return;
|
|
|
|
|
const combined = `${pendingLogLineRef.current}${content}`;
|
|
|
|
|
const split = combined.split("\n");
|
|
|
|
|
pendingLogLineRef.current = split.pop() ?? "";
|
|
|
|
|
if (finalize && pendingLogLineRef.current) {
|
|
|
|
|
split.push(pendingLogLineRef.current);
|
|
|
|
|
pendingLogLineRef.current = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed: Array<{ ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }> = [];
|
|
|
|
|
for (const line of split) {
|
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
if (!trimmed) continue;
|
|
|
|
|
try {
|
|
|
|
|
const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown };
|
|
|
|
|
const stream =
|
|
|
|
|
raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout";
|
|
|
|
|
const chunk = typeof raw.chunk === "string" ? raw.chunk : "";
|
|
|
|
|
const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString();
|
|
|
|
|
if (!chunk) continue;
|
|
|
|
|
parsed.push({ ts, stream, chunk });
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore malformed lines
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parsed.length > 0) {
|
|
|
|
|
setLogLines((prev) => [...prev, ...parsed]);
|
|
|
|
|
}
|
|
|
|
|
}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
|
|
|
|
// Fetch events
|
|
|
|
|
const { data: initialEvents } = useQuery({
|
2026-02-18 13:02:23 -06:00
|
|
|
queryKey: ["run-events", run.id],
|
|
|
|
|
queryFn: () => heartbeatsApi.events(run.id, 0, 200),
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (initialEvents) {
|
|
|
|
|
setEvents(initialEvents);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [initialEvents]);
|
|
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
const getScrollContainer = useCallback((): ScrollContainer => {
|
|
|
|
|
if (scrollContainerRef.current) return scrollContainerRef.current;
|
|
|
|
|
const container = findScrollContainer(logEndRef.current);
|
|
|
|
|
scrollContainerRef.current = container;
|
|
|
|
|
return container;
|
2026-02-20 10:32:32 -06:00
|
|
|
}, []);
|
|
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
const updateFollowingState = useCallback(() => {
|
|
|
|
|
const container = getScrollContainer();
|
|
|
|
|
const metrics = readScrollMetrics(container);
|
|
|
|
|
lastMetricsRef.current = metrics;
|
|
|
|
|
const nearBottom = metrics.distanceFromBottom <= LIVE_SCROLL_BOTTOM_TOLERANCE_PX;
|
|
|
|
|
isFollowingRef.current = nearBottom;
|
|
|
|
|
setIsFollowing((prev) => (prev === nearBottom ? prev : nearBottom));
|
|
|
|
|
}, [getScrollContainer]);
|
|
|
|
|
|
2026-02-20 10:32:32 -06:00
|
|
|
useEffect(() => {
|
2026-02-20 15:48:42 -06:00
|
|
|
scrollContainerRef.current = null;
|
|
|
|
|
lastMetricsRef.current = {
|
|
|
|
|
scrollHeight: 0,
|
|
|
|
|
distanceFromBottom: Number.POSITIVE_INFINITY,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!isLive) {
|
|
|
|
|
isFollowingRef.current = false;
|
|
|
|
|
setIsFollowing(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateFollowingState();
|
|
|
|
|
}, [isLive, run.id, updateFollowingState]);
|
2026-02-20 10:32:32 -06:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isLive) return;
|
2026-02-20 15:48:42 -06:00
|
|
|
const container = getScrollContainer();
|
2026-02-20 10:32:32 -06:00
|
|
|
updateFollowingState();
|
2026-02-20 15:48:42 -06:00
|
|
|
|
|
|
|
|
if (container === window) {
|
|
|
|
|
window.addEventListener("scroll", updateFollowingState, { passive: true });
|
|
|
|
|
} else {
|
|
|
|
|
container.addEventListener("scroll", updateFollowingState, { passive: true });
|
|
|
|
|
}
|
2026-02-20 10:32:32 -06:00
|
|
|
window.addEventListener("resize", updateFollowingState);
|
|
|
|
|
return () => {
|
2026-02-20 15:48:42 -06:00
|
|
|
if (container === window) {
|
|
|
|
|
window.removeEventListener("scroll", updateFollowingState);
|
|
|
|
|
} else {
|
|
|
|
|
container.removeEventListener("scroll", updateFollowingState);
|
|
|
|
|
}
|
2026-02-20 10:32:32 -06:00
|
|
|
window.removeEventListener("resize", updateFollowingState);
|
|
|
|
|
};
|
2026-02-20 15:48:42 -06:00
|
|
|
}, [isLive, run.id, getScrollContainer, updateFollowingState]);
|
2026-02-20 10:32:32 -06:00
|
|
|
|
|
|
|
|
// Auto-scroll only for live runs when following
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
useEffect(() => {
|
2026-02-20 15:48:42 -06:00
|
|
|
if (!isLive || !isFollowingRef.current) return;
|
|
|
|
|
|
|
|
|
|
const container = getScrollContainer();
|
|
|
|
|
const previous = lastMetricsRef.current;
|
|
|
|
|
const current = readScrollMetrics(container);
|
|
|
|
|
const growth = Math.max(0, current.scrollHeight - previous.scrollHeight);
|
|
|
|
|
const expectedDistance = previous.distanceFromBottom + growth;
|
|
|
|
|
const movedAwayBy = current.distanceFromBottom - expectedDistance;
|
|
|
|
|
|
|
|
|
|
// If user moved away from bottom between updates, release auto-follow immediately.
|
|
|
|
|
if (movedAwayBy > LIVE_SCROLL_BOTTOM_TOLERANCE_PX) {
|
|
|
|
|
isFollowingRef.current = false;
|
|
|
|
|
setIsFollowing(false);
|
|
|
|
|
lastMetricsRef.current = current;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scrollToContainerBottom(container, "auto");
|
|
|
|
|
const after = readScrollMetrics(container);
|
|
|
|
|
lastMetricsRef.current = after;
|
|
|
|
|
if (!isFollowingRef.current) {
|
|
|
|
|
isFollowingRef.current = true;
|
2026-02-18 15:29:29 -06:00
|
|
|
}
|
2026-02-20 15:48:42 -06:00
|
|
|
setIsFollowing((prev) => (prev ? prev : true));
|
|
|
|
|
}, [events.length, logLines.length, isLive, getScrollContainer]);
|
2026-02-18 13:02:23 -06:00
|
|
|
|
|
|
|
|
// Fetch persisted shell log
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
pendingLogLineRef.current = "";
|
|
|
|
|
setLogLines([]);
|
|
|
|
|
setLogOffset(0);
|
|
|
|
|
setLogError(null);
|
|
|
|
|
|
|
|
|
|
if (!run.logRef) {
|
|
|
|
|
setLogLoading(false);
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setLogLoading(true);
|
|
|
|
|
const firstLimit =
|
|
|
|
|
typeof run.logBytes === "number" && run.logBytes > 0
|
|
|
|
|
? Math.min(Math.max(run.logBytes + 1024, 256_000), 2_000_000)
|
|
|
|
|
: 256_000;
|
|
|
|
|
|
|
|
|
|
const load = async () => {
|
|
|
|
|
try {
|
|
|
|
|
let offset = 0;
|
|
|
|
|
let first = true;
|
|
|
|
|
while (!cancelled) {
|
|
|
|
|
const result = await heartbeatsApi.log(run.id, offset, first ? firstLimit : 256_000);
|
2026-02-18 15:29:29 -06:00
|
|
|
if (cancelled) break;
|
2026-02-18 13:02:23 -06:00
|
|
|
appendLogContent(result.content, result.nextOffset === undefined);
|
|
|
|
|
const next = result.nextOffset ?? offset + result.content.length;
|
|
|
|
|
setLogOffset(next);
|
|
|
|
|
offset = next;
|
|
|
|
|
first = false;
|
|
|
|
|
if (result.nextOffset === undefined || isLive) break;
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
setLogError(err instanceof Error ? err.message : "Failed to load run log");
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
if (!cancelled) setLogLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void load();
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
|
|
|
|
}, [run.id, run.logRef, run.logBytes, isLive]);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
|
|
|
|
// Poll for live updates
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isLive) return;
|
|
|
|
|
const interval = setInterval(async () => {
|
|
|
|
|
const maxSeq = events.length > 0 ? Math.max(...events.map((e) => e.seq)) : 0;
|
|
|
|
|
try {
|
2026-02-18 13:02:23 -06:00
|
|
|
const newEvents = await heartbeatsApi.events(run.id, maxSeq, 100);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
if (newEvents.length > 0) {
|
|
|
|
|
setEvents((prev) => [...prev, ...newEvents]);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore polling errors
|
|
|
|
|
}
|
|
|
|
|
}, 2000);
|
|
|
|
|
return () => clearInterval(interval);
|
2026-02-18 13:02:23 -06:00
|
|
|
}, [run.id, isLive, events]);
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
|
2026-02-18 13:02:23 -06:00
|
|
|
// Poll shell log for running runs
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isLive || !run.logRef) return;
|
|
|
|
|
const interval = setInterval(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await heartbeatsApi.log(run.id, logOffset, 256_000);
|
|
|
|
|
if (result.content) {
|
|
|
|
|
appendLogContent(result.content, result.nextOffset === undefined);
|
|
|
|
|
}
|
|
|
|
|
if (result.nextOffset !== undefined) {
|
|
|
|
|
setLogOffset(result.nextOffset);
|
|
|
|
|
} else if (result.content.length > 0) {
|
|
|
|
|
setLogOffset((prev) => prev + result.content.length);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore polling errors
|
|
|
|
|
}
|
|
|
|
|
}, 2000);
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
}, [run.id, run.logRef, isLive, logOffset]);
|
|
|
|
|
|
|
|
|
|
const adapterInvokePayload = useMemo(() => {
|
|
|
|
|
const evt = events.find((e) => e.eventType === "adapter.invoke");
|
|
|
|
|
return asRecord(evt?.payload ?? null);
|
|
|
|
|
}, [events]);
|
|
|
|
|
|
2026-02-18 13:53:03 -06:00
|
|
|
const adapter = useMemo(() => getUIAdapter(adapterType), [adapterType]);
|
|
|
|
|
const transcript = useMemo(() => buildTranscript(logLines, adapter.parseStdoutLine), [logLines, adapter]);
|
2026-02-18 13:02:23 -06:00
|
|
|
|
|
|
|
|
if (loading && logLoading) {
|
|
|
|
|
return <p className="text-xs text-muted-foreground">Loading run logs...</p>;
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 13:02:23 -06:00
|
|
|
if (events.length === 0 && logLines.length === 0 && !logError) {
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
return <p className="text-xs text-muted-foreground">No log events.</p>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const levelColors: Record<string, string> = {
|
|
|
|
|
info: "text-foreground",
|
|
|
|
|
warn: "text-yellow-400",
|
|
|
|
|
error: "text-red-400",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const streamColors: Record<string, string> = {
|
|
|
|
|
stdout: "text-foreground",
|
|
|
|
|
stderr: "text-red-300",
|
|
|
|
|
system: "text-blue-300",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-18 13:02:23 -06:00
|
|
|
<div className="space-y-3">
|
|
|
|
|
{adapterInvokePayload && (
|
|
|
|
|
<div className="rounded-lg border border-border bg-background/60 p-3 space-y-2">
|
|
|
|
|
<div className="text-xs font-medium text-muted-foreground">Invocation</div>
|
|
|
|
|
{typeof adapterInvokePayload.adapterType === "string" && (
|
|
|
|
|
<div className="text-xs"><span className="text-muted-foreground">Adapter: </span>{adapterInvokePayload.adapterType}</div>
|
|
|
|
|
)}
|
|
|
|
|
{typeof adapterInvokePayload.cwd === "string" && (
|
|
|
|
|
<div className="text-xs"><span className="text-muted-foreground">Working dir: </span><span className="font-mono">{adapterInvokePayload.cwd}</span></div>
|
|
|
|
|
)}
|
|
|
|
|
{typeof adapterInvokePayload.command === "string" && (
|
|
|
|
|
<div className="text-xs">
|
|
|
|
|
<span className="text-muted-foreground">Command: </span>
|
|
|
|
|
<span className="font-mono">
|
|
|
|
|
{[
|
|
|
|
|
adapterInvokePayload.command,
|
|
|
|
|
...(Array.isArray(adapterInvokePayload.commandArgs)
|
|
|
|
|
? adapterInvokePayload.commandArgs.filter((v): v is string => typeof v === "string")
|
|
|
|
|
: []),
|
|
|
|
|
].join(" ")}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{adapterInvokePayload.prompt !== undefined && (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-muted-foreground mb-1">Prompt</div>
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
|
|
|
|
{typeof adapterInvokePayload.prompt === "string"
|
|
|
|
|
? adapterInvokePayload.prompt
|
|
|
|
|
: JSON.stringify(adapterInvokePayload.prompt, null, 2)}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{adapterInvokePayload.context !== undefined && (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-muted-foreground mb-1">Context</div>
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
|
|
|
|
{JSON.stringify(adapterInvokePayload.context, null, 2)}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{adapterInvokePayload.env !== undefined && (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-muted-foreground mb-1">Environment</div>
|
2026-02-18 16:46:55 -06:00
|
|
|
<pre className="bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap font-mono">
|
|
|
|
|
{formatEnvForDisplay(adapterInvokePayload.env)}
|
2026-02-18 13:02:23 -06:00
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
|
|
|
Transcript ({transcript.length})
|
|
|
|
|
</span>
|
2026-02-20 10:32:32 -06:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{isLive && !isFollowing && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="xs"
|
|
|
|
|
onClick={() => {
|
2026-02-20 15:48:42 -06:00
|
|
|
const container = getScrollContainer();
|
|
|
|
|
isFollowingRef.current = true;
|
2026-02-20 10:32:32 -06:00
|
|
|
setIsFollowing(true);
|
2026-02-20 15:48:42 -06:00
|
|
|
scrollToContainerBottom(container, "auto");
|
|
|
|
|
lastMetricsRef.current = readScrollMetrics(container);
|
2026-02-20 10:32:32 -06:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Jump to live
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
{isLive && (
|
|
|
|
|
<span className="flex items-center gap-1 text-xs text-cyan-400">
|
|
|
|
|
<span className="relative flex h-2 w-2">
|
|
|
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
|
|
|
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-400" />
|
|
|
|
|
</span>
|
|
|
|
|
Live
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</span>
|
2026-02-20 10:32:32 -06:00
|
|
|
)}
|
|
|
|
|
</div>
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</div>
|
2026-02-18 16:46:55 -06:00
|
|
|
<div className="bg-neutral-950 rounded-lg p-3 font-mono text-xs space-y-0.5">
|
2026-02-18 13:02:23 -06:00
|
|
|
{transcript.length === 0 && !run.logRef && (
|
|
|
|
|
<div className="text-neutral-500">No persisted transcript for this run.</div>
|
|
|
|
|
)}
|
|
|
|
|
{transcript.map((entry, idx) => {
|
|
|
|
|
const time = new Date(entry.ts).toLocaleTimeString("en-US", { hour12: false });
|
2026-02-18 15:29:29 -06:00
|
|
|
const grid = "grid grid-cols-[auto_auto_1fr] gap-x-3 items-baseline";
|
|
|
|
|
const tsCell = "text-neutral-600 select-none w-16";
|
|
|
|
|
const lblCell = "w-20";
|
|
|
|
|
const contentCell = "min-w-0 whitespace-pre-wrap break-words";
|
|
|
|
|
const expandCell = "col-span-full md:col-start-3 md:col-span-1";
|
|
|
|
|
|
2026-02-18 13:02:23 -06:00
|
|
|
if (entry.kind === "assistant") {
|
|
|
|
|
return (
|
2026-02-18 15:29:29 -06:00
|
|
|
<div key={`${entry.ts}-assistant-${idx}`} className={cn(grid, "py-0.5")}>
|
|
|
|
|
<span className={tsCell}>{time}</span>
|
|
|
|
|
<span className={cn(lblCell, "text-green-300")}>assistant</span>
|
|
|
|
|
<span className={cn(contentCell, "text-green-100")}>{entry.text}</span>
|
2026-02-18 13:02:23 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 16:46:55 -06:00
|
|
|
if (entry.kind === "thinking") {
|
|
|
|
|
return (
|
|
|
|
|
<div key={`${entry.ts}-thinking-${idx}`} className={cn(grid, "py-0.5")}>
|
|
|
|
|
<span className={tsCell}>{time}</span>
|
|
|
|
|
<span className={cn(lblCell, "text-green-300/60")}>thinking</span>
|
|
|
|
|
<span className={cn(contentCell, "text-green-100/60 italic")}>{entry.text}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entry.kind === "user") {
|
|
|
|
|
return (
|
|
|
|
|
<div key={`${entry.ts}-user-${idx}`} className={cn(grid, "py-0.5")}>
|
|
|
|
|
<span className={tsCell}>{time}</span>
|
|
|
|
|
<span className={cn(lblCell, "text-neutral-400")}>user</span>
|
|
|
|
|
<span className={cn(contentCell, "text-neutral-300")}>{entry.text}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 13:02:23 -06:00
|
|
|
if (entry.kind === "tool_call") {
|
|
|
|
|
return (
|
2026-02-18 15:29:29 -06:00
|
|
|
<div key={`${entry.ts}-tool-${idx}`} className={cn(grid, "gap-y-1 py-0.5")}>
|
|
|
|
|
<span className={tsCell}>{time}</span>
|
|
|
|
|
<span className={cn(lblCell, "text-yellow-300")}>tool_call</span>
|
|
|
|
|
<span className="text-yellow-100 min-w-0">{entry.name}</span>
|
|
|
|
|
<pre className={cn(expandCell, "bg-neutral-900 rounded p-2 text-[11px] overflow-x-auto whitespace-pre-wrap text-neutral-200")}>
|
2026-02-18 13:02:23 -06:00
|
|
|
{JSON.stringify(entry.input, null, 2)}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 15:29:29 -06:00
|
|
|
if (entry.kind === "tool_result") {
|
|
|
|
|
return (
|
|
|
|
|
<div key={`${entry.ts}-toolres-${idx}`} className={cn(grid, "gap-y-1 py-0.5")}>
|
|
|
|
|
<span className={tsCell}>{time}</span>
|
|
|
|
|
<span className={cn(lblCell, entry.isError ? "text-red-300" : "text-purple-300")}>tool_result</span>
|
|
|
|
|
{entry.isError ? <span className="text-red-400 min-w-0">error</span> : <span />}
|
|
|
|
|
<pre className={cn(expandCell, "bg-neutral-900 rounded p-2 text-[11px] overflow-x-auto whitespace-pre-wrap text-neutral-300 max-h-60 overflow-y-auto")}>
|
2026-02-18 16:46:55 -06:00
|
|
|
{(() => { try { return JSON.stringify(JSON.parse(entry.content), null, 2); } catch { return entry.content; } })()}
|
2026-02-18 15:29:29 -06:00
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 13:02:23 -06:00
|
|
|
if (entry.kind === "init") {
|
|
|
|
|
return (
|
2026-02-18 15:29:29 -06:00
|
|
|
<div key={`${entry.ts}-init-${idx}`} className={grid}>
|
|
|
|
|
<span className={tsCell}>{time}</span>
|
|
|
|
|
<span className={cn(lblCell, "text-blue-300")}>init</span>
|
|
|
|
|
<span className={cn(contentCell, "text-blue-100")}>model: {entry.model}{entry.sessionId ? `, session: ${entry.sessionId}` : ""}</span>
|
2026-02-18 13:02:23 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entry.kind === "result") {
|
|
|
|
|
return (
|
2026-02-18 15:29:29 -06:00
|
|
|
<div key={`${entry.ts}-result-${idx}`} className={cn(grid, "gap-y-1 py-0.5")}>
|
|
|
|
|
<span className={tsCell}>{time}</span>
|
|
|
|
|
<span className={cn(lblCell, "text-cyan-300")}>result</span>
|
|
|
|
|
<span className={cn(contentCell, "text-cyan-100")}>
|
|
|
|
|
tokens in={formatTokens(entry.inputTokens)} out={formatTokens(entry.outputTokens)} cached={formatTokens(entry.cachedTokens)} cost=${entry.costUsd.toFixed(6)}
|
|
|
|
|
</span>
|
2026-02-18 13:17:03 -06:00
|
|
|
{(entry.subtype || entry.isError || entry.errors.length > 0) && (
|
2026-02-18 15:29:29 -06:00
|
|
|
<div className={cn(expandCell, "text-red-300 whitespace-pre-wrap break-words")}>
|
2026-02-18 13:17:03 -06:00
|
|
|
subtype={entry.subtype || "unknown"} is_error={entry.isError ? "true" : "false"}
|
|
|
|
|
{entry.errors.length > 0 ? ` errors=${entry.errors.join(" | ")}` : ""}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-18 13:02:23 -06:00
|
|
|
{entry.text && (
|
2026-02-18 15:29:29 -06:00
|
|
|
<div className={cn(expandCell, "whitespace-pre-wrap break-words text-neutral-100")}>{entry.text}</div>
|
2026-02-18 13:02:23 -06:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rawText = entry.text;
|
|
|
|
|
const label =
|
|
|
|
|
entry.kind === "stderr" ? "stderr" :
|
|
|
|
|
entry.kind === "system" ? "system" :
|
|
|
|
|
"stdout";
|
|
|
|
|
const color =
|
|
|
|
|
entry.kind === "stderr" ? "text-red-300" :
|
|
|
|
|
entry.kind === "system" ? "text-blue-300" :
|
2026-02-18 15:29:29 -06:00
|
|
|
"text-neutral-500";
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
return (
|
2026-02-18 15:29:29 -06:00
|
|
|
<div key={`${entry.ts}-raw-${idx}`} className={grid}>
|
|
|
|
|
<span className={tsCell}>{time}</span>
|
|
|
|
|
<span className={cn(lblCell, color)}>{label}</span>
|
|
|
|
|
<span className={cn(contentCell, color)}>{rawText}</span>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
2026-02-18 13:02:23 -06:00
|
|
|
)
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
})}
|
2026-02-18 13:02:23 -06:00
|
|
|
{logError && <div className="text-red-300">{logError}</div>}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
<div ref={logEndRef} />
|
|
|
|
|
</div>
|
2026-02-18 13:02:23 -06:00
|
|
|
|
2026-02-18 13:17:03 -06:00
|
|
|
{(run.status === "failed" || run.status === "timed_out") && (
|
|
|
|
|
<div className="rounded-lg border border-red-500/30 bg-red-950/20 p-3 space-y-2">
|
|
|
|
|
<div className="text-xs font-medium text-red-300">Failure details</div>
|
|
|
|
|
{run.error && (
|
|
|
|
|
<div className="text-xs text-red-200">
|
|
|
|
|
<span className="text-red-300">Error: </span>
|
|
|
|
|
{run.error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{run.stderrExcerpt && run.stderrExcerpt.trim() && (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-red-300 mb-1">stderr excerpt</div>
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-100">
|
|
|
|
|
{run.stderrExcerpt}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{run.resultJson && (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-red-300 mb-1">adapter result JSON</div>
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-100">
|
|
|
|
|
{JSON.stringify(run.resultJson, null, 2)}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{run.stdoutExcerpt && run.stdoutExcerpt.trim() && !run.resultJson && (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs text-red-300 mb-1">stdout excerpt</div>
|
|
|
|
|
<pre className="bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-100">
|
|
|
|
|
{run.stdoutExcerpt}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-18 13:02:23 -06:00
|
|
|
{events.length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="mb-2 text-xs font-medium text-muted-foreground">Events ({events.length})</div>
|
2026-02-18 16:46:55 -06:00
|
|
|
<div className="bg-neutral-950 rounded-lg p-3 font-mono text-xs space-y-0.5">
|
2026-02-18 13:02:23 -06:00
|
|
|
{events.map((evt) => {
|
|
|
|
|
const color = evt.color
|
|
|
|
|
?? (evt.level ? levelColors[evt.level] : null)
|
|
|
|
|
?? (evt.stream ? streamColors[evt.stream] : null)
|
|
|
|
|
?? "text-foreground";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={evt.id} className="flex gap-2">
|
|
|
|
|
<span className="text-neutral-600 shrink-0 select-none w-16">
|
|
|
|
|
{new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })}
|
|
|
|
|
</span>
|
|
|
|
|
<span className={cn("shrink-0 w-14", evt.stream ? (streamColors[evt.stream] ?? "text-neutral-500") : "text-neutral-500")}>
|
|
|
|
|
{evt.stream ? `[${evt.stream}]` : ""}
|
|
|
|
|
</span>
|
|
|
|
|
<span className={cn("break-all", color)}>
|
|
|
|
|
{evt.message ?? (evt.payload ? JSON.stringify(evt.payload) : "")}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
Build out agent management UI: detail page, create dialog, list view
Add NewAgentDialog for creating agents with adapter config. Expand
AgentDetail page with tabbed view (overview, runs, config, logs),
run history timeline, and live status. Enhance Agents list page with
richer cards and filtering. Update AgentProperties panel, API client,
query keys, and utility helpers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:33:04 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
/* ---- Keys Tab ---- */
|
|
|
|
|
|
|
|
|
|
function KeysTab({ agentId }: { agentId: string }) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const [newKeyName, setNewKeyName] = useState("");
|
|
|
|
|
const [newToken, setNewToken] = useState<string | null>(null);
|
|
|
|
|
const [tokenVisible, setTokenVisible] = useState(false);
|
|
|
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
|
|
|
|
|
|
const { data: keys, isLoading } = useQuery({
|
|
|
|
|
queryKey: queryKeys.agents.keys(agentId),
|
|
|
|
|
queryFn: () => agentsApi.listKeys(agentId),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const createKey = useMutation({
|
|
|
|
|
mutationFn: () => agentsApi.createKey(agentId, newKeyName.trim() || "Default"),
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
setNewToken(data.token);
|
|
|
|
|
setTokenVisible(true);
|
|
|
|
|
setNewKeyName("");
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.keys(agentId) });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const revokeKey = useMutation({
|
|
|
|
|
mutationFn: (keyId: string) => agentsApi.revokeKey(agentId, keyId),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.keys(agentId) });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function copyToken() {
|
|
|
|
|
if (!newToken) return;
|
|
|
|
|
navigator.clipboard.writeText(newToken);
|
|
|
|
|
setCopied(true);
|
|
|
|
|
setTimeout(() => setCopied(false), 2000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const activeKeys = (keys ?? []).filter((k: AgentKey) => !k.revokedAt);
|
|
|
|
|
const revokedKeys = (keys ?? []).filter((k: AgentKey) => k.revokedAt);
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-20 10:33:18 -06:00
|
|
|
<div className="space-y-6">
|
2026-02-17 20:46:12 -06:00
|
|
|
{/* New token banner */}
|
|
|
|
|
{newToken && (
|
|
|
|
|
<div className="border border-yellow-600/40 bg-yellow-500/5 rounded-lg p-4 space-y-2">
|
|
|
|
|
<p className="text-sm font-medium text-yellow-400">
|
|
|
|
|
API key created — copy it now, it will not be shown again.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<code className="flex-1 bg-neutral-950 rounded px-3 py-1.5 text-xs font-mono text-green-300 truncate">
|
|
|
|
|
{tokenVisible ? newToken : newToken.replace(/./g, "•")}
|
|
|
|
|
</code>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
onClick={() => setTokenVisible((v) => !v)}
|
|
|
|
|
title={tokenVisible ? "Hide" : "Show"}
|
|
|
|
|
>
|
|
|
|
|
{tokenVisible ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
onClick={copyToken}
|
|
|
|
|
title="Copy"
|
|
|
|
|
>
|
|
|
|
|
<Copy className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
{copied && <span className="text-xs text-green-400">Copied!</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-muted-foreground text-xs"
|
|
|
|
|
onClick={() => setNewToken(null)}
|
|
|
|
|
>
|
|
|
|
|
Dismiss
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Create new key */}
|
|
|
|
|
<div className="border border-border rounded-lg p-4 space-y-3">
|
2026-02-23 14:41:21 -06:00
|
|
|
<h3 className="text-xs font-medium text-muted-foreground flex items-center gap-2">
|
|
|
|
|
<Key className="h-3.5 w-3.5" />
|
2026-02-17 20:46:12 -06:00
|
|
|
Create API Key
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
API keys allow this agent to authenticate calls to the Paperclip server.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Key name (e.g. production)"
|
|
|
|
|
value={newKeyName}
|
|
|
|
|
onChange={(e) => setNewKeyName(e.target.value)}
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter") createKey.mutate();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => createKey.mutate()}
|
|
|
|
|
disabled={createKey.isPending}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
|
|
|
Create
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Active keys */}
|
|
|
|
|
{isLoading && <p className="text-sm text-muted-foreground">Loading keys...</p>}
|
|
|
|
|
|
|
|
|
|
{!isLoading && activeKeys.length === 0 && !newToken && (
|
|
|
|
|
<p className="text-sm text-muted-foreground">No active API keys.</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeKeys.length > 0 && (
|
|
|
|
|
<div>
|
2026-02-23 14:41:21 -06:00
|
|
|
<h3 className="text-xs font-medium text-muted-foreground mb-2">
|
2026-02-17 20:46:12 -06:00
|
|
|
Active Keys
|
|
|
|
|
</h3>
|
2026-02-23 14:41:21 -06:00
|
|
|
<div className="border border-border rounded-lg divide-y divide-border">
|
2026-02-17 20:46:12 -06:00
|
|
|
{activeKeys.map((key: AgentKey) => (
|
|
|
|
|
<div key={key.id} className="flex items-center justify-between px-4 py-2.5">
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-sm font-medium">{key.name}</span>
|
|
|
|
|
<span className="text-xs text-muted-foreground ml-3">
|
|
|
|
|
Created {formatDate(key.createdAt)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-destructive hover:text-destructive text-xs"
|
|
|
|
|
onClick={() => revokeKey.mutate(key.id)}
|
|
|
|
|
disabled={revokeKey.isPending}
|
|
|
|
|
>
|
|
|
|
|
Revoke
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
{/* Revoked keys */}
|
2026-02-17 20:46:12 -06:00
|
|
|
{revokedKeys.length > 0 && (
|
|
|
|
|
<div>
|
2026-02-23 14:41:21 -06:00
|
|
|
<h3 className="text-xs font-medium text-muted-foreground mb-2">
|
2026-02-17 20:46:12 -06:00
|
|
|
Revoked Keys
|
|
|
|
|
</h3>
|
2026-02-23 14:41:21 -06:00
|
|
|
<div className="border border-border rounded-lg divide-y divide-border opacity-50">
|
2026-02-17 20:46:12 -06:00
|
|
|
{revokedKeys.map((key: AgentKey) => (
|
|
|
|
|
<div key={key.id} className="flex items-center justify-between px-4 py-2.5">
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-sm line-through">{key.name}</span>
|
|
|
|
|
<span className="text-xs text-muted-foreground ml-3">
|
|
|
|
|
Revoked {key.revokedAt ? formatDate(key.revokedAt) : ""}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|