From 35c4623db6bcb62aff3a1be607773c8ff83a3c68 Mon Sep 17 00:00:00 2001 From: Ultracite Date: Sat, 28 Feb 2026 06:01:04 +0000 Subject: [PATCH] fix: Auto-fix lint issues Automatically fixed by Ultracite --- .claude/settings.json | 5 +- .cursor/mcp.json | 5 +- dashboard/app/api/actions/route.ts | 219 +++-- dashboard/app/api/snapshot/route.ts | 12 +- dashboard/app/globals.css | 3 +- dashboard/app/lanes/page.tsx | 288 +++--- dashboard/app/layout.tsx | 17 +- dashboard/app/page.tsx | 376 ++++--- dashboard/app/system/page.tsx | 154 ++- dashboard/app/threads/page.tsx | 311 +++--- dashboard/app/topology/page.tsx | 133 ++- dashboard/app/webhooks/page.tsx | 503 ++++++---- dashboard/components/hud-primitives.tsx | 146 +-- dashboard/components/lane-model-primer.tsx | 51 +- dashboard/components/sidebar-nav.tsx | 65 +- dashboard/lib/dashboard-data.ts | 1026 +++++++++++--------- dashboard/lib/dashboard-ui.ts | 153 +-- dashboard/lib/use-dashboard-snapshot.ts | 122 +-- dashboard/next.config.ts | 2 +- src/modules/bridge/service.ts | 10 +- src/modules/webhooks/index.ts | 5 +- tsconfig.json | 4 +- 22 files changed, 2111 insertions(+), 1499 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index d5d1587..3b080a7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -3,10 +3,7 @@ "hack": { "type": "stdio", "command": "hack", - "args": [ - "mcp", - "serve" - ] + "args": ["mcp", "serve"] } } } diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 4011643..25679ff 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -2,10 +2,7 @@ "mcpServers": { "hack": { "command": "hack", - "args": [ - "mcp", - "serve" - ] + "args": ["mcp", "serve"] } } } diff --git a/dashboard/app/api/actions/route.ts b/dashboard/app/api/actions/route.ts index ea00822..fa2358d 100644 --- a/dashboard/app/api/actions/route.ts +++ b/dashboard/app/api/actions/route.ts @@ -1,6 +1,6 @@ -import { NextResponse } from "next/server" +import { NextResponse } from "next/server"; -import { resolveBridgeBaseUrl } from "@/lib/dashboard-data" +import { resolveBridgeBaseUrl } from "@/lib/dashboard-data"; type ActionName = | "reconcile-stale" @@ -16,56 +16,67 @@ type ActionName = | "webhook-test" | "webhook-create" | "webhook-toggle" - | "webhook-delete" + | "webhook-delete"; interface BridgeRequestResult { - status: number - payload: unknown + status: number; + payload: unknown; } const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value) + typeof value === "object" && value !== null && !Array.isArray(value); const toStringValue = ({ value }: { value: unknown }): string | null => { if (typeof value !== "string") { - return null + return null; } - const trimmed = value.trim() - return trimmed.length > 0 ? trimmed : null -} + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +}; -const toBooleanValue = ({ value, fallback }: { value: unknown; fallback: boolean }): boolean => - typeof value === "boolean" ? value : fallback +const toBooleanValue = ({ + value, + fallback, +}: { + value: unknown; + fallback: boolean; +}): boolean => (typeof value === "boolean" ? value : fallback); -const toNumberValue = ({ value, fallback }: { value: unknown; fallback: number }): number => - typeof value === "number" && Number.isFinite(value) ? value : fallback +const toNumberValue = ({ + value, + fallback, +}: { + value: unknown; + fallback: number; +}): number => + typeof value === "number" && Number.isFinite(value) ? value : fallback; const toStringArray = ({ value }: { value: unknown }): string[] | undefined => { if (!Array.isArray(value)) { - return undefined + return undefined; } const items = value .map((item) => toStringValue({ value: item })) - .filter((item): item is string => item !== null) - return items.length > 0 ? items : undefined -} + .filter((item): item is string => item !== null); + return items.length > 0 ? items : undefined; +}; const toWebhookScope = ({ value, }: { - value: unknown + value: unknown; }): | { - laneId?: string - laneIds?: string[] - threadId?: string - threadIds?: string[] - sessionId?: string - sessionIds?: string[] + laneId?: string; + laneIds?: string[]; + threadId?: string; + threadIds?: string[]; + sessionId?: string; + sessionIds?: string[]; } | undefined => { if (!isRecord(value)) { - return undefined + return undefined; } const scope = { laneId: toStringValue({ value: value.laneId }) ?? undefined, @@ -74,30 +85,30 @@ const toWebhookScope = ({ threadIds: toStringArray({ value: value.threadIds }), sessionId: toStringValue({ value: value.sessionId }) ?? undefined, sessionIds: toStringArray({ value: value.sessionIds }), - } + }; const hasAnyScope = Boolean(scope.laneId) || Boolean(scope.threadId) || Boolean(scope.sessionId) || Boolean(scope.laneIds?.length) || Boolean(scope.threadIds?.length) || - Boolean(scope.sessionIds?.length) - return hasAnyScope ? scope : undefined -} + Boolean(scope.sessionIds?.length); + return hasAnyScope ? scope : undefined; +}; const badRequest = ({ message }: { message: string }) => - NextResponse.json({ error: "invalid_request", message }, { status: 400 }) + NextResponse.json({ error: "invalid_request", message }, { status: 400 }); const bridgeRequest = async ({ endpoint, method, body, }: { - endpoint: string - method: "POST" | "PATCH" | "DELETE" - body?: Record + endpoint: string; + method: "POST" | "PATCH" | "DELETE"; + body?: Record; }): Promise => { - const baseUrl = resolveBridgeBaseUrl() + const baseUrl = resolveBridgeBaseUrl(); try { const response = await fetch(`${baseUrl}${endpoint}`, { @@ -108,21 +119,21 @@ const bridgeRequest = async ({ }, body: body ? JSON.stringify(body) : undefined, cache: "no-store", - }) + }); - const text = await response.text() + const text = await response.text(); if (!text.trim()) { return { status: response.status, payload: { ok: response.ok }, - } + }; } try { return { status: response.status, payload: JSON.parse(text) as unknown, - } + }; } catch { return { status: response.status, @@ -130,37 +141,40 @@ const bridgeRequest = async ({ ok: response.ok, body: text, }, - } + }; } } catch (error) { return { status: 502, payload: { error: "bridge_request_failed", - message: error instanceof Error ? error.message : "Unknown bridge request failure", + message: + error instanceof Error + ? error.message + : "Unknown bridge request failure", }, - } + }; } -} +}; export async function POST(request: Request) { - let payload: unknown + let payload: unknown; try { - payload = (await request.json()) as unknown + payload = (await request.json()) as unknown; } catch { - return badRequest({ message: "Request body must be valid JSON" }) + return badRequest({ message: "Request body must be valid JSON" }); } if (!isRecord(payload)) { - return badRequest({ message: "Request body must be an object" }) + return badRequest({ message: "Request body must be an object" }); } - const actionValue = toStringValue({ value: payload.action }) + const actionValue = toStringValue({ value: payload.action }); if (!actionValue) { - return badRequest({ message: "Missing action" }) + return badRequest({ message: "Missing action" }); } - const action = actionValue as ActionName + const action = actionValue as ActionName; if (action === "reconcile-stale") { const result = await bridgeRequest({ @@ -176,8 +190,8 @@ export async function POST(request: Request) { fallback: true, }), }, - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "watch-start") { @@ -185,71 +199,74 @@ export async function POST(request: Request) { endpoint: "/watch/start", method: "POST", body: { - intervalSec: toNumberValue({ value: payload.intervalSec, fallback: 15 }), + intervalSec: toNumberValue({ + value: payload.intervalSec, + fallback: 15, + }), }, - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "watch-stop") { const result = await bridgeRequest({ endpoint: "/watch/stop", method: "POST", - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "watch-tick") { const result = await bridgeRequest({ endpoint: "/watch/tick", method: "POST", - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } - const laneId = toStringValue({ value: payload.laneId }) + const laneId = toStringValue({ value: payload.laneId }); if (action === "lane-sync") { if (!laneId) { - return badRequest({ message: "lane-sync requires laneId" }) + return badRequest({ message: "lane-sync requires laneId" }); } const result = await bridgeRequest({ endpoint: `/lanes/${laneId}/sync-from-codex`, method: "POST", - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "lane-start-session") { if (!laneId) { - return badRequest({ message: "lane-start-session requires laneId" }) + return badRequest({ message: "lane-start-session requires laneId" }); } const result = await bridgeRequest({ endpoint: `/lanes/${laneId}/session/start`, method: "POST", - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "lane-stop-session") { if (!laneId) { - return badRequest({ message: "lane-stop-session requires laneId" }) + return badRequest({ message: "lane-stop-session requires laneId" }); } const result = await bridgeRequest({ endpoint: `/lanes/${laneId}/session/stop`, method: "POST", - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "lane-delete") { if (!laneId) { - return badRequest({ message: "lane-delete requires laneId" }) + return badRequest({ message: "lane-delete requires laneId" }); } const result = await bridgeRequest({ endpoint: `/lanes/${laneId}`, method: "DELETE", - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "lane-cleanup") { @@ -257,7 +274,7 @@ export async function POST(request: Request) { ? payload.statuses .map((value) => toStringValue({ value })) .filter((value): value is string => value !== null) - : undefined + : undefined; const result = await bridgeRequest({ endpoint: "/lanes/cleanup", method: "POST", @@ -274,83 +291,85 @@ export async function POST(request: Request) { limit: toNumberValue({ value: payload.limit, fallback: 100 }), dryRun: toBooleanValue({ value: payload.dryRun, fallback: false }), }, - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } - const threadId = toStringValue({ value: payload.threadId }) + const threadId = toStringValue({ value: payload.threadId }); if (action === "thread-archive") { if (!threadId) { - return badRequest({ message: "thread-archive requires threadId" }) + return badRequest({ message: "thread-archive requires threadId" }); } const result = await bridgeRequest({ endpoint: `/codex/threads/${threadId}/archive`, method: "POST", body: {}, - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } - const webhookId = toStringValue({ value: payload.id }) + const webhookId = toStringValue({ value: payload.id }); if (action === "webhook-create") { - const url = toStringValue({ value: payload.url }) + const url = toStringValue({ value: payload.url }); if (!url) { - return badRequest({ message: "webhook-create requires url" }) + return badRequest({ message: "webhook-create requires url" }); } - const events = toStringArray({ value: payload.events }) - const secret = toStringValue({ value: payload.secret }) - const scope = toWebhookScope({ value: payload.scope }) + const events = toStringArray({ value: payload.events }); + const secret = toStringValue({ value: payload.secret }); + const scope = toWebhookScope({ value: payload.scope }); const body: Record = { url, ...(events ? { events } : {}), - ...(typeof payload.enabled === "boolean" ? { enabled: payload.enabled } : {}), + ...(typeof payload.enabled === "boolean" + ? { enabled: payload.enabled } + : {}), ...(secret ? { secret } : {}), ...(scope ? { scope } : {}), - } + }; const result = await bridgeRequest({ endpoint: "/webhooks", method: "POST", body, - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "webhook-test") { if (!webhookId) { - return badRequest({ message: "webhook-test requires id" }) + return badRequest({ message: "webhook-test requires id" }); } const result = await bridgeRequest({ endpoint: `/webhooks/${webhookId}/test`, method: "POST", - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "webhook-toggle") { if (!webhookId) { - return badRequest({ message: "webhook-toggle requires id" }) + return badRequest({ message: "webhook-toggle requires id" }); } if (typeof payload.enabled !== "boolean") { - return badRequest({ message: "webhook-toggle requires boolean enabled" }) + return badRequest({ message: "webhook-toggle requires boolean enabled" }); } const result = await bridgeRequest({ endpoint: `/webhooks/${webhookId}`, method: "PATCH", body: { enabled: payload.enabled }, - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } if (action === "webhook-delete") { if (!webhookId) { - return badRequest({ message: "webhook-delete requires id" }) + return badRequest({ message: "webhook-delete requires id" }); } const result = await bridgeRequest({ endpoint: `/webhooks/${webhookId}`, method: "DELETE", - }) - return NextResponse.json(result.payload, { status: result.status }) + }); + return NextResponse.json(result.payload, { status: result.status }); } - return badRequest({ message: `Unsupported action: ${actionValue}` }) + return badRequest({ message: `Unsupported action: ${actionValue}` }); } diff --git a/dashboard/app/api/snapshot/route.ts b/dashboard/app/api/snapshot/route.ts index 27a2121..293852f 100644 --- a/dashboard/app/api/snapshot/route.ts +++ b/dashboard/app/api/snapshot/route.ts @@ -1,11 +1,11 @@ -import { NextResponse } from "next/server" +import { NextResponse } from "next/server"; -import { buildDashboardSnapshot } from "@/lib/dashboard-data" +import { buildDashboardSnapshot } from "@/lib/dashboard-data"; -export const dynamic = "force-dynamic" -export const revalidate = 0 +export const dynamic = "force-dynamic"; +export const revalidate = 0; export async function GET() { - const snapshot = await buildDashboardSnapshot() - return NextResponse.json(snapshot) + const snapshot = await buildDashboardSnapshot(); + return NextResponse.json(snapshot); } diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css index eb0c513..390c34d 100644 --- a/dashboard/app/globals.css +++ b/dashboard/app/globals.css @@ -34,7 +34,8 @@ a { } .mono { - font-family: var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace; + font-family: + var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace; } .panel { diff --git a/dashboard/app/lanes/page.tsx b/dashboard/app/lanes/page.tsx index 6366dc1..6484feb 100644 --- a/dashboard/app/lanes/page.tsx +++ b/dashboard/app/lanes/page.tsx @@ -1,8 +1,8 @@ -"use client" +"use client"; -import { RefreshCw, Search, Trash2 } from "lucide-react" -import Link from "next/link" -import { useMemo, useState } from "react" +import { RefreshCw, Search, Trash2 } from "lucide-react"; +import Link from "next/link"; +import { useMemo, useState } from "react"; import { ActionButton, @@ -12,16 +12,19 @@ import { RowActionMenu, RowActionMenuButton, RowActionMenuLink, -} from "@/components/hud-primitives" -import { LaneModelPrimer } from "@/components/lane-model-primer" +} from "@/components/hud-primitives"; +import { LaneModelPrimer } from "@/components/lane-model-primer"; import { + formatIso, laneOwnershipClassName, laneStatusClassName, - formatIso, runtimeOriginLabel, toDisplayValue, -} from "@/lib/dashboard-ui" -import { runDashboardAction, useDashboardSnapshot } from "@/lib/use-dashboard-snapshot" +} from "@/lib/dashboard-ui"; +import { + runDashboardAction, + useDashboardSnapshot, +} from "@/lib/use-dashboard-snapshot"; const knownLaneStatuses = [ "all", @@ -34,44 +37,52 @@ const knownLaneStatuses = [ "done", "failed", "interrupted", -] as const +] as const; -type LaneStatusFilter = (typeof knownLaneStatuses)[number] +type LaneStatusFilter = (typeof knownLaneStatuses)[number]; export default function LanesPage() { - const { snapshot, error, isLoading, isRefreshing, refreshSnapshot } = useDashboardSnapshot() - const [actionError, setActionError] = useState(null) - const [actionMessage, setActionMessage] = useState(null) - const [pendingActionKey, setPendingActionKey] = useState(null) + const { snapshot, error, isLoading, isRefreshing, refreshSnapshot } = + useDashboardSnapshot(); + const [actionError, setActionError] = useState(null); + const [actionMessage, setActionMessage] = useState(null); + const [pendingActionKey, setPendingActionKey] = useState(null); const [laneSearch, setLaneSearch] = useState(() => { if (typeof window === "undefined") { - return "" + return ""; } - return new URLSearchParams(window.location.search).get("search") ?? "" - }) - const [statusFilter, setStatusFilter] = useState("all") - const [cleanupHours, setCleanupHours] = useState(8) - const [cleanupArmed, setCleanupArmed] = useState(false) - const [deleteLaneArmed, setDeleteLaneArmed] = useState(null) + return new URLSearchParams(window.location.search).get("search") ?? ""; + }); + const [statusFilter, setStatusFilter] = useState("all"); + const [cleanupHours, setCleanupHours] = useState(8); + const [cleanupArmed, setCleanupArmed] = useState(false); + const [deleteLaneArmed, setDeleteLaneArmed] = useState(null); const laneSessionsById = useMemo( - () => new Map((snapshot?.laneSessions ?? []).map((session) => [session.laneId, session])), + () => + new Map( + (snapshot?.laneSessions ?? []).map((session) => [ + session.laneId, + session, + ]) + ), [snapshot?.laneSessions] - ) + ); const threadById = useMemo( - () => new Map((snapshot?.threads ?? []).map((thread) => [thread.id, thread])), + () => + new Map((snapshot?.threads ?? []).map((thread) => [thread.id, thread])), [snapshot?.threads] - ) + ); const filteredLanes = useMemo(() => { - const lanes = snapshot?.lanes ?? [] - const lowered = laneSearch.trim().toLowerCase() + const lanes = snapshot?.lanes ?? []; + const lowered = laneSearch.trim().toLowerCase(); return lanes.filter((lane) => { if (statusFilter !== "all" && lane.status !== statusFilter) { - return false + return false; } if (!lowered) { - return true + return true; } const haystack = [lane.laneId, lane.threadId, lane.repoRoot, lane.surface] .concat([ @@ -81,71 +92,78 @@ export default function LanesPage() { threadById.get(lane.threadId ?? "")?.sourceLabel ?? "", ]) .map((part) => part?.toLowerCase() ?? "") - .join(" ") - return haystack.includes(lowered) - }) - }, [snapshot?.lanes, laneSearch, statusFilter, threadById]) + .join(" "); + return haystack.includes(lowered); + }); + }, [snapshot?.lanes, laneSearch, statusFilter, threadById]); const runAction = async ({ payload, actionKey, successMessage, }: { - payload: Record - actionKey: string - successMessage: string + payload: Record; + actionKey: string; + successMessage: string; }) => { - setPendingActionKey(actionKey) - setActionError(null) - setActionMessage(null) + setPendingActionKey(actionKey); + setActionError(null); + setActionMessage(null); try { - await runDashboardAction({ payload }) - setActionMessage(successMessage) - await refreshSnapshot({ isPoll: false }) + await runDashboardAction({ payload }); + setActionMessage(successMessage); + await refreshSnapshot({ isPoll: false }); } catch (requestError) { - const message = requestError instanceof Error ? requestError.message : "Action failed" - setActionError(message) + const message = + requestError instanceof Error ? requestError.message : "Action failed"; + setActionError(message); } finally { - setPendingActionKey(null) + setPendingActionKey(null); } - } + }; return (
-

Lanes

+

Lanes

- Control lane lifecycle, sessions, and execution ownership for thread-attached work. + Control lane lifecycle, sessions, and execution ownership for + thread-attached work.

} busy={isRefreshing} + icon={ + + } + label={isRefreshing ? "Refreshing..." : "Refresh"} onClick={() => { - refreshSnapshot({ isPoll: false }).catch(() => undefined) + refreshSnapshot({ isPoll: false }).catch(() => undefined); }} /> setCleanupHours(Number(event.target.value))} - className="h-9 w-24 rounded-md border border-zinc-700 bg-zinc-900 px-2 text-sm text-zinc-100" + type="number" + value={cleanupHours} /> } busy={pendingActionKey === "lane-cleanup"} + icon={} + label={cleanupArmed ? "Confirm cleanup" : "Cleanup stale"} onClick={() => { if (!cleanupArmed) { - setCleanupArmed(true) - return + setCleanupArmed(true); + return; } - setCleanupArmed(false) + setCleanupArmed(false); runAction({ actionKey: "lane-cleanup", successMessage: "Lane cleanup completed", @@ -156,19 +174,19 @@ export default function LanesPage() { syntheticOnly: false, limit: 250, }, - }).catch(() => undefined) + }).catch(() => undefined); }} />
- {error ? : null} - {actionError ? : null} - {actionMessage ? : null} + {error ? : null} + {actionError ? : null} + {actionMessage ? : null} {cleanupArmed ? ( -

- Cleanup is armed. Click “Confirm cleanup” to delete stale terminal lanes older than{" "} - {cleanupHours}h. +

+ Cleanup is armed. Click “Confirm cleanup” to delete stale + terminal lanes older than {cleanupHours}h.

) : null} @@ -177,19 +195,21 @@ export default function LanesPage() {
setSearch(event.target.value)} placeholder="Search thread id, title, source, cwd..." - className="w-full bg-transparent outline-none placeholder:text-zinc-500" + type="search" + value={search} />
@@ -212,7 +239,7 @@ export default function ThreadsPage() { ) : (
- + @@ -226,32 +253,47 @@ export default function ThreadsPage() { {treeRows.map(({ thread, depth }) => { - const attachedLanes = laneIdsByThreadId.get(thread.id) ?? [] - const hasRunningSession = runningSessionThreadIds.has(thread.id) - const isActive = activeThreadIds.has(thread.id) + const attachedLanes = laneIdsByThreadId.get(thread.id) ?? []; + const hasRunningSession = runningSessionThreadIds.has( + thread.id + ); + const isActive = activeThreadIds.has(thread.id); return ( - + - + - - ) + ); })}
Thread Origin / Source
{depth > 0 ? ( - └─ + + └─ + ) : null}
{thread.displayName} -

{thread.id}

-

{toDisplayValue({ value: thread.title })}

+

+ {thread.id} +

+

+ {toDisplayValue({ value: thread.title })} +

{thread.sourceAgentNickname ? (

sub-agent {thread.sourceAgentNickname} - {thread.sourceAgentRole ? ` (${thread.sourceAgentRole})` : ""} + {thread.sourceAgentRole + ? ` (${thread.sourceAgentRole})` + : ""}

) : null}
@@ -267,18 +309,20 @@ export default function ThreadsPage() {

{thread.sourceLabel} - {thread.sourceDetail ? ` · ${thread.sourceDetail}` : ""} + {thread.sourceDetail + ? ` · ${thread.sourceDetail}` + : ""}

+ {thread.sourceParentThreadId ? (
- {threadById.get(thread.sourceParentThreadId)?.displayName ?? - thread.sourceParentThreadId} + {threadById.get(thread.sourceParentThreadId) + ?.displayName ?? thread.sourceParentThreadId}

{thread.sourceParentThreadId} @@ -295,9 +339,9 @@ export default function ThreadsPage() {

{attachedLanes.slice(0, 4).map((laneId) => ( {laneId} @@ -314,7 +358,9 @@ export default function ThreadsPage() {
{isActive ? "active" : "idle"} @@ -326,12 +372,18 @@ export default function ThreadsPage() { : "bg-zinc-200 text-zinc-700" }`} > - {hasRunningSession ? "session running" : "session stopped"} + {hasRunningSession + ? "session running" + : "session stopped"}
-

{toDisplayValue({ value: thread.cwd })}

+

+ {toDisplayValue({ value: thread.cwd })} +

+
+ {thread.projectName ?? "unknown"} {thread.projectName ?? "unknown"} {formatIso({ value: @@ -341,32 +393,43 @@ export default function ThreadsPage() { })} - + { if (archiveArmedThreadId !== thread.id) { - setArchiveArmedThreadId(thread.id) - return + setArchiveArmedThreadId(thread.id); + return; } - setArchiveArmedThreadId(null) + setArchiveArmedThreadId(null); runAction({ actionKey: `thread-archive:${thread.id}`, successMessage: `Thread ${thread.displayName} archived`, - payload: { action: "thread-archive", threadId: thread.id }, - }).catch(() => undefined) + payload: { + action: "thread-archive", + threadId: thread.id, + }, + }).catch(() => undefined); }} + variant="danger" />
@@ -374,5 +437,5 @@ export default function ThreadsPage() { )}
- ) + ); } diff --git a/dashboard/app/topology/page.tsx b/dashboard/app/topology/page.tsx index feb9baa..50e54e3 100644 --- a/dashboard/app/topology/page.tsx +++ b/dashboard/app/topology/page.tsx @@ -6,8 +6,8 @@ import { type Edge, MiniMap, type Node, - type ReactFlowInstance, ReactFlow, + type ReactFlowInstance, } from "@xyflow/react"; import { Activity, Layers, RefreshCw, Search, Trash2, X } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; @@ -60,7 +60,12 @@ const TERMINAL_LANE_STATUSES = new Set([ "failed", "interrupted", ]); -const PROBLEM_LANE_STATUSES = new Set(["needs_approval", "blocked", "failed", "interrupted"]); +const PROBLEM_LANE_STATUSES = new Set([ + "needs_approval", + "blocked", + "failed", + "interrupted", +]); type TopologyMode = "ops" | "debug" | "audit"; @@ -244,10 +249,9 @@ const threadNodeStyle = ({ return { ...baseNodeStyle, border: isSubAgent ? "1px solid #a78bfa" : "1px solid #22d3ee", - boxShadow: - isSubAgent - ? "0 10px 28px rgba(167, 139, 250, 0.15), 0 0 0 1px rgba(167, 139, 250, 0.22)" - : "0 10px 28px rgba(34, 211, 238, 0.15), 0 0 0 1px rgba(34, 211, 238, 0.22)", + boxShadow: isSubAgent + ? "0 10px 28px rgba(167, 139, 250, 0.15), 0 0 0 1px rgba(167, 139, 250, 0.22)" + : "0 10px 28px rgba(34, 211, 238, 0.15), 0 0 0 1px rgba(34, 211, 238, 0.22)", width: THREAD_NODE_WIDTH, background: isSubAgent ? "#1b1230" : hasChildren ? "#10212a" : "#101824", color: isSubAgent ? "#f5f3ff" : "#ecfeff", @@ -462,8 +466,9 @@ export default function TopologyPage() { return new URLSearchParams(window.location.search).get("search") ?? ""; }); const [selectedNode, setSelectedNode] = useState(null); - const [topologyMode, setTopologyMode] = - useState(DEFAULT_TOPOLOGY_MODE); + const [topologyMode, setTopologyMode] = useState( + DEFAULT_TOPOLOGY_MODE + ); const [viewMode, setViewMode] = useState<"focused" | "all">("focused"); const [layoutPreset, setLayoutPreset] = useState<"compact" | "roomy">( "roomy" @@ -482,9 +487,10 @@ export default function TopologyPage() { const [deleteLanesArmed, setDeleteLanesArmed] = useState(false); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); const [projectFilterQuery, setProjectFilterQuery] = useState(""); - const [reactFlowInstance, setReactFlowInstance] = useState< - ReactFlowInstance, Edge> | null - >(null); + const [reactFlowInstance, setReactFlowInstance] = useState, + Edge + > | null>(null); const searchInputRef = useRef(null); const [actionError, setActionError] = useState(null); const [actionMessage, setActionMessage] = useState(null); @@ -515,27 +521,30 @@ export default function TopologyPage() { } }; - const applyTopologyModePreset = useCallback(({ mode }: { mode: TopologyMode }) => { - setTopologyMode(mode); - setArchiveThreadsArmed(false); - setDeleteLanesArmed(false); - if (mode === "ops") { - setViewMode("focused"); - setHideStaleThreads(true); - setStaleThreadHours(DEFAULT_STALE_THREAD_HOURS); - setLayoutPreset("roomy"); - return; - } - if (mode === "debug") { + const applyTopologyModePreset = useCallback( + ({ mode }: { mode: TopologyMode }) => { + setTopologyMode(mode); + setArchiveThreadsArmed(false); + setDeleteLanesArmed(false); + if (mode === "ops") { + setViewMode("focused"); + setHideStaleThreads(true); + setStaleThreadHours(DEFAULT_STALE_THREAD_HOURS); + setLayoutPreset("roomy"); + return; + } + if (mode === "debug") { + setViewMode("all"); + setHideStaleThreads(false); + setLayoutPreset("roomy"); + return; + } setViewMode("all"); setHideStaleThreads(false); - setLayoutPreset("roomy"); - return; - } - setViewMode("all"); - setHideStaleThreads(false); - setLayoutPreset("compact"); - }, []); + setLayoutPreset("compact"); + }, + [] + ); const laneByThread = useMemo(() => { const map = new Map(); @@ -635,7 +644,10 @@ export default function TopologyPage() { addedInPass = false; for (const thread of allThreads) { const parentId = thread.sourceParentThreadId; - if (!parentId || !included.has(parentId) || included.has(thread.id)) { + if ( + !(parentId && included.has(parentId)) || + included.has(thread.id) + ) { continue; } if (isSubAgentThread({ thread })) { @@ -884,13 +896,17 @@ export default function TopologyPage() { { label: "Title", value: row.thread.title ?? "Untitled" }, { label: "Originator", value: threadOrigin }, { label: "Source", value: row.thread.sourceLabel }, - { label: "Source detail", value: row.thread.sourceDetail ?? "none" }, + { + label: "Source detail", + value: row.thread.sourceDetail ?? "none", + }, { label: "Depth", value: String(row.depth + 1) }, { label: "Attached lanes", value: String(threadLanes.length) }, ], }; - const threadDepthIndent = Math.min(4, Math.max(0, row.depth)) * layoutThreadDepthXStep; + const threadDepthIndent = + Math.min(4, Math.max(0, row.depth)) * layoutThreadDepthXStep; const threadX = PROJECT_PADDING_X + threadDepthIndent; const threadY = cursorY; const threadNodeId = `thread:${row.thread.id}`; @@ -1060,7 +1076,10 @@ export default function TopologyPage() { { label: "Session key", value: session.laneId }, { label: "Lane", value: lane.displayName }, { label: "Thread", value: session.threadId ?? "none" }, - { label: "Ownership", value: "session belongs to lane runtime" }, + { + label: "Ownership", + value: "session belongs to lane runtime", + }, { label: "Started", value: session.startedAt ?? "unknown" }, { label: "Pending requests", @@ -1145,10 +1164,7 @@ export default function TopologyPage() { : threadY + layoutLaneBaseYOffset + layoutLaneYStep ); maxChildX = Math.max(maxChildX, laneX + LANE_NODE_WIDTH + 40); - maxChildX = Math.max( - maxChildX, - sessionX + SESSION_NODE_WIDTH + 48 - ); + maxChildX = Math.max(maxChildX, sessionX + SESSION_NODE_WIDTH + 48); } const hiddenSubAgentCount = @@ -1286,7 +1302,9 @@ export default function TopologyPage() { snapshot?.topologyDiagnostics.unresolvedParentIds.length ?? 0; const selectedNodes = useMemo( () => - flowGraph.nodes.filter((node) => selectedNodeIds.includes(String(node.id))), + flowGraph.nodes.filter((node) => + selectedNodeIds.includes(String(node.id)) + ), [flowGraph.nodes, selectedNodeIds] ); const selectedThreadIds = useMemo( @@ -1336,7 +1354,9 @@ export default function TopologyPage() { parentId = parent.parentId; } const width = - typeof node.style?.width === "number" ? node.style.width : THREAD_NODE_WIDTH; + typeof node.style?.width === "number" + ? node.style.width + : THREAD_NODE_WIDTH; const height = typeof node.style?.height === "number" ? node.style.height @@ -1355,10 +1375,14 @@ export default function TopologyPage() { if (!box) { return; } - reactFlowInstance.setCenter(box.x + box.width / 2, box.y + box.height / 2, { - zoom: 0.98, - duration: 320, - }); + reactFlowInstance.setCenter( + box.x + box.width / 2, + box.y + box.height / 2, + { + zoom: 0.98, + duration: 320, + } + ); }, [reactFlowInstance, resolveNodeGlobalBox] ); @@ -1708,7 +1732,8 @@ export default function TopologyPage() { unresolved parent refs (snapshot): {unresolvedParentCount} - unresolved parent links (rendered): {flowGraph.missingParentEdgeCount} + unresolved parent links (rendered):{" "} + {flowGraph.missingParentEdgeCount} {cleanupArmed ? ( @@ -1795,7 +1820,9 @@ export default function TopologyPage() { }} type="button" > - {archiveThreadsArmed ? "confirm archive threads" : "archive threads"} + {archiveThreadsArmed + ? "confirm archive threads" + : "archive threads"}
@@ -1945,7 +1973,9 @@ export default function TopologyPage() { setSelectedNodeIds((current) => { if ( current.length === nextNodeIds.length && - current.every((nodeId, index) => nodeId === nextNodeIds[index]) + current.every( + (nodeId, index) => nodeId === nextNodeIds[index] + ) ) { return current; } @@ -1960,7 +1990,10 @@ export default function TopologyPage() { maskColor="rgba(10,10,12,0.6)" nodeColor={(node) => miniMapNodeColor({ - node: node as Node<{ label: ReactNode; meta?: TopologyMeta }>, + node: node as Node<{ + label: ReactNode; + meta?: TopologyMeta; + }>, }) } pannable diff --git a/dashboard/app/webhooks/page.tsx b/dashboard/app/webhooks/page.tsx index 0637f79..aa598cf 100644 --- a/dashboard/app/webhooks/page.tsx +++ b/dashboard/app/webhooks/page.tsx @@ -1,8 +1,8 @@ -"use client" +"use client"; -import { Plus, RefreshCw, Search, Trash2 } from "lucide-react" -import Link from "next/link" -import { useMemo, useState } from "react" +import { Plus, RefreshCw, Search, Trash2 } from "lucide-react"; +import Link from "next/link"; +import { useMemo, useState } from "react"; import { ActionButton, @@ -11,131 +11,154 @@ import { Panel, RowActionMenu, RowActionMenuButton, -} from "@/components/hud-primitives" +} from "@/components/hud-primitives"; import { formatIso, + type WebhookHealth, webhookHealth, webhookHealthClassName, - type WebhookHealth, -} from "@/lib/dashboard-ui" -import { runDashboardAction, useDashboardSnapshot } from "@/lib/use-dashboard-snapshot" +} from "@/lib/dashboard-ui"; +import { + runDashboardAction, + useDashboardSnapshot, +} from "@/lib/use-dashboard-snapshot"; interface ScopeSummary { - kinds: string[] - targets: string[] + kinds: string[]; + targets: string[]; } const summarizeScope = ({ subscription, }: { subscription: { - scopeKinds?: string[] + scopeKinds?: string[]; scope?: { - laneIds: string[] - threadIds: string[] - sessionIds: string[] - } - } + laneIds: string[]; + threadIds: string[]; + sessionIds: string[]; + }; + }; }): ScopeSummary => { const kinds = subscription.scopeKinds && subscription.scopeKinds.length > 0 ? subscription.scopeKinds - : ["global"] + : ["global"]; const targets = [ ...(subscription.scope?.laneIds ?? []).map((target) => `lane:${target}`), - ...(subscription.scope?.threadIds ?? []).map((target) => `thread:${target}`), - ...(subscription.scope?.sessionIds ?? []).map((target) => `session:${target}`), - ] + ...(subscription.scope?.threadIds ?? []).map( + (target) => `thread:${target}` + ), + ...(subscription.scope?.sessionIds ?? []).map( + (target) => `session:${target}` + ), + ]; return { kinds, targets: targets.length > 0 ? targets : ["all"], - } -} + }; +}; -const healthOrder: WebhookHealth[] = ["failing", "stale", "disabled", "idle", "active"] +const healthOrder: WebhookHealth[] = [ + "failing", + "stale", + "disabled", + "idle", + "active", +]; const parseTokenList = ({ value }: { value: string }): string[] => { - const dedupe = new Set() + const dedupe = new Set(); for (const token of value.split(/[,\s]+/)) { - const trimmed = token.trim() + const trimmed = token.trim(); if (trimmed.length > 0) { - dedupe.add(trimmed) + dedupe.add(trimmed); } } - return [...dedupe] -} + return [...dedupe]; +}; -const addToken = ({ input, token }: { input: string; token: string }): string => { - const values = parseTokenList({ value: input }) +const addToken = ({ + input, + token, +}: { + input: string; + token: string; +}): string => { + const values = parseTokenList({ value: input }); if (!values.includes(token)) { - values.push(token) + values.push(token); } - return values.join(", ") -} + return values.join(", "); +}; export default function WebhooksPage() { - const { snapshot, error, isLoading, isRefreshing, refreshSnapshot } = useDashboardSnapshot() + const { snapshot, error, isLoading, isRefreshing, refreshSnapshot } = + useDashboardSnapshot(); const [isCreateOpen, setIsCreateOpen] = useState(() => { if (typeof window === "undefined") { - return false + return false; } - const params = new URLSearchParams(window.location.search) + const params = new URLSearchParams(window.location.search); return ( params.get("create") === "1" || params.has("laneId") || params.has("threadId") || params.has("sessionId") - ) - }) + ); + }); const [search, setSearch] = useState(() => { if (typeof window === "undefined") { - return "" + return ""; } - return new URLSearchParams(window.location.search).get("search") ?? "" - }) - const [createUrl, setCreateUrl] = useState("") - const [createEvents, setCreateEvents] = useState("*") - const [createEnabled, setCreateEnabled] = useState(true) - const [createSecret, setCreateSecret] = useState("") + return new URLSearchParams(window.location.search).get("search") ?? ""; + }); + const [createUrl, setCreateUrl] = useState(""); + const [createEvents, setCreateEvents] = useState("*"); + const [createEnabled, setCreateEnabled] = useState(true); + const [createSecret, setCreateSecret] = useState(""); const [scopeMode, setScopeMode] = useState<"global" | "scoped">(() => { if (typeof window === "undefined") { - return "global" + return "global"; } - const params = new URLSearchParams(window.location.search) - return params.has("laneId") || params.has("threadId") || params.has("sessionId") + const params = new URLSearchParams(window.location.search); + return params.has("laneId") || + params.has("threadId") || + params.has("sessionId") ? "scoped" - : "global" - }) + : "global"; + }); const [scopeLaneIds, setScopeLaneIds] = useState(() => { if (typeof window === "undefined") { - return "" + return ""; } - return new URLSearchParams(window.location.search).get("laneId") ?? "" - }) + return new URLSearchParams(window.location.search).get("laneId") ?? ""; + }); const [scopeThreadIds, setScopeThreadIds] = useState(() => { if (typeof window === "undefined") { - return "" + return ""; } - return new URLSearchParams(window.location.search).get("threadId") ?? "" - }) + return new URLSearchParams(window.location.search).get("threadId") ?? ""; + }); const [scopeSessionIds, setScopeSessionIds] = useState(() => { if (typeof window === "undefined") { - return "" + return ""; } - return new URLSearchParams(window.location.search).get("sessionId") ?? "" - }) - const [actionError, setActionError] = useState(null) - const [actionMessage, setActionMessage] = useState(null) - const [pendingActionKey, setPendingActionKey] = useState(null) - const [deleteArmedId, setDeleteArmedId] = useState(null) + return new URLSearchParams(window.location.search).get("sessionId") ?? ""; + }); + const [actionError, setActionError] = useState(null); + const [actionMessage, setActionMessage] = useState(null); + const [pendingActionKey, setPendingActionKey] = useState(null); + const [deleteArmedId, setDeleteArmedId] = useState(null); const laneById = useMemo( () => new Map((snapshot?.lanes ?? []).map((lane) => [lane.laneId, lane])), [snapshot?.lanes] - ) + ); const threadById = useMemo( - () => new Map((snapshot?.threads ?? []).map((thread) => [thread.id, thread])), + () => + new Map((snapshot?.threads ?? []).map((thread) => [thread.id, thread])), [snapshot?.threads] - ) + ); const laneSuggestions = useMemo( () => (snapshot?.lanes ?? []) @@ -143,7 +166,7 @@ export default function WebhooksPage() { .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)) .slice(0, 10), [snapshot?.lanes] - ) + ); const threadSuggestions = useMemo( () => (snapshot?.threads ?? []) @@ -151,24 +174,23 @@ export default function WebhooksPage() { .sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0)) .slice(0, 10), [snapshot?.threads] - ) + ); const subscriptions = useMemo(() => { - const list = snapshot?.webhooks ?? [] - const lowered = search.trim().toLowerCase() - const filtered = !lowered - ? list - : list.filter((subscription) => { - const scope = summarizeScope({ subscription }) + const list = snapshot?.webhooks ?? []; + const lowered = search.trim().toLowerCase(); + const filtered = lowered + ? list.filter((subscription) => { + const scope = summarizeScope({ subscription }); const scopeLabels = scope.targets.map((target) => { if (target.startsWith("lane:")) { - return laneById.get(target.slice(5))?.displayName ?? "" + return laneById.get(target.slice(5))?.displayName ?? ""; } if (target.startsWith("thread:")) { - return threadById.get(target.slice(7))?.displayName ?? "" + return threadById.get(target.slice(7))?.displayName ?? ""; } - return "" - }) + return ""; + }); const haystack = [ subscription.id, subscription.url, @@ -178,204 +200,215 @@ export default function WebhooksPage() { ...scopeLabels, ] .join(" ") - .toLowerCase() - return haystack.includes(lowered) + .toLowerCase(); + return haystack.includes(lowered); }) + : list; return [...filtered].sort((left, right) => { const leftHealth = webhookHealth({ enabled: left.enabled, lastDeliveryAt: left.lastDeliveryAt, lastError: left.lastError, - }) + }); const rightHealth = webhookHealth({ enabled: right.enabled, lastDeliveryAt: right.lastDeliveryAt, lastError: right.lastError, - }) - return healthOrder.indexOf(leftHealth) - healthOrder.indexOf(rightHealth) - }) - }, [laneById, snapshot?.webhooks, search, threadById]) + }); + return healthOrder.indexOf(leftHealth) - healthOrder.indexOf(rightHealth); + }); + }, [laneById, snapshot?.webhooks, search, threadById]); const runAction = async ({ payload, actionKey, successMessage, }: { - payload: Record - actionKey: string - successMessage: string + payload: Record; + actionKey: string; + successMessage: string; }): Promise => { - setPendingActionKey(actionKey) - setActionError(null) - setActionMessage(null) + setPendingActionKey(actionKey); + setActionError(null); + setActionMessage(null); try { - await runDashboardAction({ payload }) - setActionMessage(successMessage) - await refreshSnapshot({ isPoll: false }) - return true + await runDashboardAction({ payload }); + setActionMessage(successMessage); + await refreshSnapshot({ isPoll: false }); + return true; } catch (requestError) { - const message = requestError instanceof Error ? requestError.message : "Action failed" - setActionError(message) - return false + const message = + requestError instanceof Error ? requestError.message : "Action failed"; + setActionError(message); + return false; } finally { - setPendingActionKey(null) + setPendingActionKey(null); } - } + }; const createWebhook = async () => { - const url = createUrl.trim() + const url = createUrl.trim(); if (!url) { - setActionError("Webhook URL is required") - setActionMessage(null) - return + setActionError("Webhook URL is required"); + setActionMessage(null); + return; } const payload: Record = { action: "webhook-create", url, enabled: createEnabled, - } - const events = parseTokenList({ value: createEvents }) + }; + const events = parseTokenList({ value: createEvents }); if (events.length > 0) { - payload.events = events + payload.events = events; } - const secret = createSecret.trim() + const secret = createSecret.trim(); if (secret.length > 0) { - payload.secret = secret + payload.secret = secret; } if (scopeMode === "scoped") { - const laneIds = parseTokenList({ value: scopeLaneIds }) - const threadIds = parseTokenList({ value: scopeThreadIds }) - const sessionIds = parseTokenList({ value: scopeSessionIds }) + const laneIds = parseTokenList({ value: scopeLaneIds }); + const threadIds = parseTokenList({ value: scopeThreadIds }); + const sessionIds = parseTokenList({ value: scopeSessionIds }); payload.scope = { laneIds, threadIds, sessionIds, - } + }; } const didCreate = await runAction({ actionKey: "webhook-create", successMessage: "Webhook subscription created", payload, - }) + }); if (!didCreate) { - return + return; } - setCreateUrl("") - setCreateEvents("*") - setCreateSecret("") - setCreateEnabled(true) - setScopeMode("global") - setScopeLaneIds("") - setScopeThreadIds("") - setScopeSessionIds("") - setIsCreateOpen(false) - } + setCreateUrl(""); + setCreateEvents("*"); + setCreateSecret(""); + setCreateEnabled(true); + setScopeMode("global"); + setScopeLaneIds(""); + setScopeThreadIds(""); + setScopeSessionIds(""); + setIsCreateOpen(false); + }; return (
-

Webhooks

+

Webhooks

- Hybrid subscriptions with global, lane, thread, and session targeting. + Hybrid subscriptions with global, lane, thread, and session + targeting.

} + label={isCreateOpen ? "Hide create" : "Create webhook"} onClick={() => { - setIsCreateOpen((previous) => !previous) + setIsCreateOpen((previous) => !previous); }} /> } busy={isRefreshing} + icon={ + + } + label={isRefreshing ? "Refreshing..." : "Refresh"} onClick={() => { - refreshSnapshot({ isPoll: false }).catch(() => undefined) + refreshSnapshot({ isPoll: false }).catch(() => undefined); }} />
- {error ? : null} - {actionError ? : null} - {actionMessage ? : null} + {error ? : null} + {actionError ? : null} + {actionMessage ? : null} {deleteArmedId ? ( -

- Delete is armed for webhook {deleteArmedId}. Click delete again to +

+ Delete is armed for webhook{" "} + {deleteArmedId}. Click delete again to confirm.

) : null} {isCreateOpen ? (
-

scope

+

+ scope +

@@ -386,26 +419,26 @@ export default function WebhooksPage() {