diff --git a/packages/dashboard/public/portfolio/arete-mockup-1.jpg b/packages/dashboard/public/portfolio/arete-mockup-1.jpg new file mode 100644 index 00000000..ae97e443 Binary files /dev/null and b/packages/dashboard/public/portfolio/arete-mockup-1.jpg differ diff --git a/packages/dashboard/public/portfolio/arete-mockup-2.jpg b/packages/dashboard/public/portfolio/arete-mockup-2.jpg new file mode 100644 index 00000000..a67de77c Binary files /dev/null and b/packages/dashboard/public/portfolio/arete-mockup-2.jpg differ diff --git a/packages/dashboard/src/app/(dashboard)/agents/page.tsx b/packages/dashboard/src/app/(dashboard)/agents/page.tsx index 4317092e..0a49ecc3 100644 --- a/packages/dashboard/src/app/(dashboard)/agents/page.tsx +++ b/packages/dashboard/src/app/(dashboard)/agents/page.tsx @@ -2,9 +2,14 @@ import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; import { db } from "@/lib/db"; import { getDashboardViewModel, resolveSelectedInstallationIds, getAgentActivity } from "@/lib/queries"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { getAccountState } from "@/lib/account-state"; import { getActiveModelConnection } from "@/lib/model-connections-api"; import { getWorkItemInbox } from "@/lib/work-items"; +*/ +/* --- END MERGE --- */ import { AgentsWorkspace } from "@/components/dashboard/agents/agents-workspace"; // Same rationale as the overview: this page reads the session and queries @@ -45,6 +50,9 @@ export default async function AgentsPage({ ? await getAgentActivity(db, installationIds) : []; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // The agents' real dependency is a connected model — the repo alone can't // produce a review. Drives the honest empty-state CTA (connect a model, not // a repo, once the repo is already connected). @@ -67,6 +75,8 @@ export default async function AgentsPage({ // Same tenant scoping as every other query on this page. const inbox = viewModel.hasAccess ? await getWorkItemInbox(db, installationIds) : null; +*/ +/* --- END MERGE --- */ return (
Kuma GitHub App +/* --- MERGED: PRESERVING UI (HEAD) --- */ + + Core + +
+

+ Install the Kuma GitHub App on your account or org so Kuma can review your pull + requests. This is the core connection that powers your overview dashboard. +

+ + @@ -114,6 +129,8 @@ export default async function ConnectionsPage() { ? "border-border-default bg-surface-2 text-content-secondary hover:bg-content-primary/5" : "border-accent-primary/30 bg-accent-primary/20 text-white hover:bg-accent-primary/30" }`} +*/ +/* --- END MERGE --- */ > {connected ? "Manage on GitHub" : "Install on GitHub"} diff --git a/packages/dashboard/src/app/(dashboard)/overview/page.tsx b/packages/dashboard/src/app/(dashboard)/overview/page.tsx index 10cb5a01..a2179fed 100644 --- a/packages/dashboard/src/app/(dashboard)/overview/page.tsx +++ b/packages/dashboard/src/app/(dashboard)/overview/page.tsx @@ -9,10 +9,17 @@ import { getAccountState } from "@/lib/account-state"; import { deriveOverviewSetup } from "@/lib/overview-setup"; import { bucketByDay } from "@/lib/trends"; import { PageReveal, RevealItem } from "@/components/dashboard/page-reveal"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +import { ActivityList } from "@/components/dashboard/activity-list"; +import { AgentsAtWorkStrip } from "@/components/dashboard/agents-at-work-strip"; +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { SensoriumMap } from "@/components/dashboard/sensorium-map"; import { OverviewSetupCard } from "@/components/dashboard/overview-setup-card"; import { OverviewStatTile } from "@/components/dashboard/overview-stat-tile"; import { DashboardsWorkspace } from "@/components/dashboard/dashboards/dashboards-workspace"; +*/ +/* --- END MERGE --- */ // This page reads the session and queries Prisma scoped to it on every // request — it must never be statically prerendered (that would either fail @@ -47,6 +54,17 @@ export default async function DashboardOverview({ )?.externalId; const sensorium = await getSensoriumViewModel(db, installationIds, graphExternalId); +/* --- MERGED: PRESERVING UI (HEAD) --- */ + const connected = viewModel.hasAccess; + const { totalPrs, criticalBugs, recentReviews, latestReviews, commentsByCategory } = viewModel.hasAccess + ? viewModel + : { totalPrs: 0, criticalBugs: 0, recentReviews: 0, latestReviews: [], commentsByCategory: [] }; + + const findingCountById = Object.fromEntries( + commentsByCategory.map((c) => [c.category, c.count]) + ); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // Account-State Contract: connection facts + onboarding derive from the single // resolver, never ad-hoc hasAccess/totalPrs checks. The userId lets a pending // (pre-repo) model connection count as setup step 1 honestly. @@ -55,12 +73,30 @@ export default async function DashboardOverview({ const { totalPrs, criticalBugs, recentReviews, reviewDates } = dashboardsModel.hasAccess ? dashboardsModel : { totalPrs: 0, criticalBugs: 0, recentReviews: 0, reviewDates: [] as Date[] }; +*/ +/* --- END MERGE --- */ const firstName = (session.user.name ?? "").trim().split(" ")[0]; +/* --- MERGED: PRESERVING UI (HEAD) --- */ + // Onboarding progress — honest, derived from real state. The setup card + // disappears once reviews are actually flowing. + const steps = [ + { label: "Create your Kuma account", done: true }, + { label: "Connect a repository", done: connected }, + { label: "Open a pull request", done: hasReviews }, + { label: "Get your first verified review", done: hasReviews }, + ]; + const doneCount = steps.filter((s) => s.done).length; + const setupComplete = hasReviews; + const nextStep = steps.find((s) => !s.done); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // Onboarding progress — derived from the Account-State resolver (single source // of truth), honest across all stages; the card evolves once reviews flow. const setup = deriveOverviewSetup(accountState); +*/ +/* --- END MERGE --- */ return (
@@ -75,6 +111,69 @@ export default async function DashboardOverview({

+/* --- MERGED: PRESERVING UI (HEAD) --- */ + {/* Setup card (SuperLog-style onboarding) — only while not fully set up */} + {!setupComplete && ( + +
+
+

+ Finish setting up Kuma +

+ + {doneCount} of {steps.length} + +
+ + {/* progress bar */} +
+
+
+ + {/* current-step highlight */} + {nextStep && ( +
+
+

{nextStep.label}

+

+ {nextStep.label === "Connect a repository" + ? "Install the Kuma GitHub App on the repo you want reviewed. Every pull request is then reviewed automatically." + : "Open a pull request on a connected repository — the six specialists review it and post verified findings back to the PR."} +

+
+ {nextStep.label === "Connect a repository" && ( + + Connect a repository + + + )} +
+ )} + + {/* checklist */} +
    + {steps.map((step) => ( +
  • + {step.done ? ( + + ) : ( + + )} + + {step.label} + +
  • + ))} +
+
+/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* {/* Sensorium — a live map of your codebase (real nodes from the code graph, with pain/activity sensor overlays). Honest empty state until a review has indexed the repo; never a fabricated graph. */} @@ -96,6 +195,8 @@ export default async function DashboardOverview({ {sensorium.reason ?? "Kuma is building your code map from your connected repository."} )} +*/ +/* --- END MERGE --- */
)} @@ -124,8 +225,20 @@ export default async function DashboardOverview({
+/* --- MERGED: PRESERVING UI (HEAD) --- */ + {/* Agents at work — the six specialists and what each has caught */} + + Agents at work + + + + {/* Critical findings */} +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* {/* Dashboards — the review-pipeline + telemetry charts, folded in from the former standalone /dashboards page (one home, not two tabs). */} +*/ +/* --- END MERGE --- */ Dashboards diff --git a/packages/dashboard/src/app/(dashboard)/reviews/[id]/page.tsx b/packages/dashboard/src/app/(dashboard)/reviews/[id]/page.tsx index f65d1ca6..37bac68c 100644 --- a/packages/dashboard/src/app/(dashboard)/reviews/[id]/page.tsx +++ b/packages/dashboard/src/app/(dashboard)/reviews/[id]/page.tsx @@ -6,8 +6,13 @@ import { getReviewDetail, resolveSelectedInstallationIds } from "@/lib/queries"; import { PageReveal, RevealItem } from "@/components/dashboard/page-reveal"; import { CopyAgentPrompt } from "@/components/dashboard/copy-agent-prompt"; import { IconArrowLeft, IconSparkles } from "@tabler/icons-react"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { AgentRunExplorer } from "@/components/dashboard/agent-run-explorer"; import { FindingNoiseControl } from "@/components/dashboard/finding-noise-control"; +*/ +/* --- END MERGE --- */ export const dynamic = "force-dynamic"; @@ -81,6 +86,14 @@ export default async function ReviewDetailPage({ ); } +/* --- MERGED: PRESERVING UI (HEAD) --- */ + // Paste-ready prompt for a coding agent, from the real verified findings. + const agentPrompt = [ + `Fix the following ${review.findings.length} code review ${review.findings.length === 1 ? "finding" : "findings"} on ${review.repositoryFullName} (PR #${review.prNumber}):`, + "", + ...review.findings.map( +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // Silencing has to MEAN something, or the control is decoration: a finding a // human called noise stops driving the agent prompt below, exactly as it // already stops driving the code map (getFindingsByPath filters to OPEN). @@ -94,6 +107,8 @@ export default async function ReviewDetailPage({ `Fix the following ${actionable.length} code review ${actionable.length === 1 ? "finding" : "findings"} on ${review.repositoryFullName} (PR #${review.prNumber}):`, "", ...actionable.map( +*/ +/* --- END MERGE --- */ (f, i) => `${i + 1}. [${formatCategory(f.category)} · ${f.severity}] ${f.path}:${f.line}\n ${f.body}`, ), ].join("\n"); @@ -127,7 +142,13 @@ export default async function ReviewDetailPage({ View in Synthesizer +/* --- MERGED: PRESERVING UI (HEAD) --- */ + {review.findings.length > 0 && } +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* {actionable.length > 0 && } +*/ +/* --- END MERGE --- */ diff --git a/packages/dashboard/src/app/(dashboard)/services/page.tsx b/packages/dashboard/src/app/(dashboard)/services/page.tsx index f76982d4..4abc90f4 100644 --- a/packages/dashboard/src/app/(dashboard)/services/page.tsx +++ b/packages/dashboard/src/app/(dashboard)/services/page.tsx @@ -17,12 +17,23 @@ import { ServicesWorkspace } from "@/components/dashboard/services/services-work // Session-scoped like every dashboard page; never statically prerendered. export const dynamic = "force-dynamic"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +// Services "Triage Inbox" — production signals from connected telemetry, +// compiled per service, with the agent's proposed fix and a human-approve +// flow. Renders with NO services/issues today (no connector ingestion wired +// yet) — ServicesWorkspace shows its honest empty state and routes to +// /connections. Real data will be passed in here once the backend ingestion +// pipeline (Sentry etc.) populates the Service/Issue contract. +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // Services "Triage Inbox" — the connected repo's real reviews, grouped per // repository (the "service"). Each review is a selectable PR; selecting one // streams its real Synthesizer transcript via /api/containers/[id]/stream (the // container id IS the review id). No sample data and no fabricated fixes — // only reviews that actually ran. An account with no reviews yet gets the // honest empty state, which routes to /connections. +*/ +/* --- END MERGE --- */ export default async function ServicesPage({ searchParams, }: { @@ -34,6 +45,11 @@ export default async function ServicesPage({ } const { container } = await searchParams; +/* --- MERGED: PRESERVING UI (HEAD) --- */ + const connected = (session.installations ?? []).length > 0; + return ; +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* const installationIds = resolveSelectedInstallationIds(session.installations ?? [], undefined); const connected = installationIds.length > 0; // Stage 2.2 adds the agents layer, so this page now also loads what a @@ -77,4 +93,6 @@ export default async function ServicesPage({ modelConnected={account?.modelConnected ?? false} /> ); +*/ +/* --- END MERGE --- */ } diff --git a/packages/dashboard/src/app/api/agents/[id]/chat/route.test.ts b/packages/dashboard/src/app/api/agents/[id]/chat/route.test.ts index b825b4c0..3343922a 100644 --- a/packages/dashboard/src/app/api/agents/[id]/chat/route.test.ts +++ b/packages/dashboard/src/app/api/agents/[id]/chat/route.test.ts @@ -6,6 +6,9 @@ vi.mock('@/lib/auth', () => ({ auth: () => authMock() })); const sendAgentChatMock = vi.fn(); vi.mock('@/lib/agent-chat', () => ({ sendAgentChat: (...a: any[]) => sendAgentChatMock(...a) })); +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // The real resolver pulls in @/lib/db, which requires DATABASE_URL at import // time; null means "agents service default", the same contract the route uses. vi.mock('@/lib/model-connections-api', () => ({ resolveActiveLlmForChat: async () => null })); @@ -18,6 +21,8 @@ vi.mock('@/lib/agent-chat-history', () => ({ appendChatTurn: async () => {}, })); +*/ +/* --- END MERGE --- */ import { POST } from './route'; function req(body: unknown) { @@ -62,11 +67,20 @@ describe('POST /api/agents/[id]/chat', () => { it('200 with the upstream reply on success', async () => { authMock.mockResolvedValue({ user: { id: 'u1' } }); +/* --- MERGED: PRESERVING UI (HEAD) --- */ + sendAgentChatMock.mockResolvedValue('Here is my analysis.'); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* sendAgentChatMock.mockResolvedValue({ reply: 'Here is my analysis.' }); +*/ +/* --- END MERGE --- */ const res = await POST(req({ message: 'hi' }), ctx('security')); expect(res.status).toBe(200); expect(await res.json()).toEqual({ reply: 'Here is my analysis.' }); }); +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* it('402 with the provider error message when the model call is rejected', async () => { authMock.mockResolvedValue({ user: { id: 'u1' } }); @@ -80,4 +94,6 @@ describe('POST /api/agents/[id]/chat', () => { kind: 'credit_balance', }); }); +*/ +/* --- END MERGE --- */ }); diff --git a/packages/dashboard/src/app/api/agents/[id]/chat/route.ts b/packages/dashboard/src/app/api/agents/[id]/chat/route.ts index c15bf2aa..87655369 100644 --- a/packages/dashboard/src/app/api/agents/[id]/chat/route.ts +++ b/packages/dashboard/src/app/api/agents/[id]/chat/route.ts @@ -2,12 +2,20 @@ import { NextResponse, type NextRequest } from "next/server"; import { auth } from "@/lib/auth"; import { AGENTS } from "@/components/dashboard/agents/agent-catalog"; import { sendAgentChat } from "@/lib/agent-chat"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { resolveActiveLlmForChat } from "@/lib/model-connections-api"; import { listChatTurns, appendChatTurn } from "@/lib/agent-chat-history"; +*/ +/* --- END MERGE --- */ // Session-scoped; never statically prerendered. export const dynamic = "force-dynamic"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* function resolveAgent(id: string) { return AGENTS.find((a) => a.id === id) ?? null; } @@ -43,17 +51,24 @@ export async function GET( return NextResponse.json({ turns }); } +*/ +/* --- END MERGE --- */ /** * Dashboard -> agent chat. Authenticates the session, validates the agent id * against the real catalog, then proxies to the Python /chat service via * sendAgentChat. On any upstream failure (including a missing model key, which * makes the service refuse to start) it returns a truthful 503 — the composer * renders that as its honest disabled notice, never a fabricated reply. +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* * * Persists both turns to the thread identified by (agent, containerId) — a * specific work item's container, or the "general" thread when none is open — * so the conversation survives navigating away and reloading. Persistence is * best-effort (see agent-chat-history.ts) and never blocks or fails the reply. +*/ +/* --- END MERGE --- */ */ export async function POST( req: NextRequest, @@ -65,7 +80,13 @@ export async function POST( } const { id } = await ctx.params; +/* --- MERGED: PRESERVING UI (HEAD) --- */ + const agent = AGENTS.find((a) => a.id === id); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* const agent = resolveAgent(id); +*/ +/* --- END MERGE --- */ if (!agent) { return NextResponse.json({ error: "Unknown agent" }, { status: 400 }); } @@ -75,6 +96,12 @@ export async function POST( if (!message) { return NextResponse.json({ error: "Empty message" }, { status: 400 }); } +/* --- MERGED: PRESERVING UI (HEAD) --- */ + + try { + const reply = await sendAgentChat({ agent, message }); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* const containerId = typeof body?.containerId === "string" && body.containerId.trim() ? body.containerId.trim() : null; void appendChatTurn(id, containerId, { role: "user", text: message }); @@ -92,6 +119,8 @@ export async function POST( } const reply = result.reply; void appendChatTurn(id, containerId, { role: "agent", text: reply || "(no response)" }); +*/ +/* --- END MERGE --- */ return NextResponse.json({ reply }); } catch (err) { console.error("[agents/chat] upstream chat failed", err); diff --git a/packages/dashboard/src/app/api/containers/[id]/stream/route.ts b/packages/dashboard/src/app/api/containers/[id]/stream/route.ts index 3bdb3841..56eff2ea 100644 --- a/packages/dashboard/src/app/api/containers/[id]/stream/route.ts +++ b/packages/dashboard/src/app/api/containers/[id]/stream/route.ts @@ -3,6 +3,14 @@ * See docs/superpowers/specs/2026-07-13-synthesizer-component-and-critic.md §5.1. * * The container id IS a Kuma review id. We resolve the REAL review (tenancy- +/* --- MERGED: PRESERVING UI (HEAD) --- */ + * scoped) and project it into a container, then emit `init` (the snapshot), + * one `step` per reconstructed SynthStep, and a terminal `done`. A stored review + * is terminal, so steps emit in order without pacing. No sample data is ever + * served here — an unknown / not-yours id returns a uniform 404 and the console + * shows empty. +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* * scoped) and project it into a container; failing that, we fall back to the * driven sample container (live-drive.ts) — the interim live source until the * persistent IssueContainer store lands. Real reviews stay tenancy-scoped; the @@ -13,14 +21,22 @@ * replays a terminal container's history instantly. So a stored review streams * exactly as before, while the driven sample streams genuinely live — the * transcript is driveContainer's output, not a scripted replay. +*/ +/* --- END MERGE --- */ */ import { auth } from "@/lib/auth"; import { db } from "@/lib/db"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +import { getReviewContainer } from "@/lib/issue-pipeline/review-container-store"; +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { InMemoryContainerStore } from "@/lib/issue-pipeline/container-store"; import { getLiveSampleContainer } from "@/lib/issue-pipeline/live-drive"; import { getReviewContainer } from "@/lib/issue-pipeline/review-container-store"; import { getFixContainer } from "@/lib/issue-pipeline/fix-container-store"; +*/ +/* --- END MERGE --- */ export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -33,21 +49,32 @@ export async function GET(_req: Request, ctx: { params: Promise<{ id: string }> const { id } = await ctx.params; const installationIds = (session.installations ?? []).map((i) => i.id); +/* --- MERGED: PRESERVING UI (HEAD) --- */ + const container = await getReviewContainer(db, installationIds, id); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // Resolve, in order: a real review container, then a fix-born IssueContainer // (its persisted drive transcript), then the tenant-neutral sample. const container = (await getReviewContainer(db, installationIds, id)) ?? (await getFixContainer(db, installationIds, id)) ?? getLiveSampleContainer(id); +*/ +/* --- END MERGE --- */ if (!container) { return new Response("Not found", { status: 404 }); } const encoder = new TextEncoder(); +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // A single-container store reuses the tested pacing + terminal-vs-live logic. const store = new InMemoryContainerStore([container]); let unsubscribe: (() => void) | null = null; +*/ +/* --- END MERGE --- */ const stream = new ReadableStream({ start(controller) { const send = (event: string, data: unknown) => { @@ -55,6 +82,14 @@ export async function GET(_req: Request, ctx: { params: Promise<{ id: string }> }; send("init", container); +/* --- MERGED: PRESERVING UI (HEAD) --- */ + for (const step of container.transcript) { + send("step", step); + } + send("done", { ok: true }); + controller.close(); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* unsubscribe = store.subscribe( container.id, (step) => send("step", step), @@ -66,6 +101,8 @@ export async function GET(_req: Request, ctx: { params: Promise<{ id: string }> }, cancel() { unsubscribe?.(); +*/ +/* --- END MERGE --- */ }, }); diff --git a/packages/dashboard/src/app/portfolio/arete/page.tsx b/packages/dashboard/src/app/portfolio/arete/page.tsx new file mode 100644 index 00000000..b4a1d1bf --- /dev/null +++ b/packages/dashboard/src/app/portfolio/arete/page.tsx @@ -0,0 +1,50 @@ +import { ProjectShowcase } from "@/components/portfolio/project-showcase"; +import { IconCpu, IconNetwork, IconShieldLock, IconTerminal2 } from "@tabler/icons-react"; + +export default function AreteShowcasePage() { + const features = [ + { + title: "Agentic Framework", + description: "A robust framework that orchestrates complex autonomous agents to accomplish tasks, communicate seamlessly, and manage context.", + icon: + }, + { + title: "Secure Sub-networking", + description: "Creates isolated execution environments and private networks, ensuring security boundaries are rigorously maintained.", + icon: + }, + { + title: "Marble & Ink UI", + description: "A stunningly fast, elegant, and light-themed Dashboard built with Next.js, Framer Motion, and Tailwind CSS.", + icon: + }, + { + title: "RBAC & Auditing", + description: "Fine-grained role-based access controls and detailed event logging for compliance and monitoring of agent behaviors.", + icon: + } + ]; + + return ( + + ); +} diff --git a/packages/dashboard/src/components/dashboard/agents-at-work-strip.tsx b/packages/dashboard/src/components/dashboard/agents-at-work-strip.tsx new file mode 100644 index 00000000..67fb5e81 --- /dev/null +++ b/packages/dashboard/src/components/dashboard/agents-at-work-strip.tsx @@ -0,0 +1,78 @@ +import { AGENTS } from "./agents/agent-catalog"; +import { cn } from "@/lib/utils"; + +/** + * Overview "agents at work" strip (approved Overview redesign, artifact + * 0f96f422) — the six specialists and what each has caught, drawn straight from + * real review data (`commentsByCategory`, keyed by agent id == category). It + * replaces the old node-graph with a calm, legible status row that ties the + * Overview to the /agents workspace. + * + * HONESTY: counts are real. Before any review has run, every card shows a muted + * "stands ready" — never a fabricated number, never a fake-active state. + */ +export function AgentsAtWorkStrip({ + findingCountById, + hasReviews, +}: { + findingCountById: Record; + hasReviews: boolean; +}) { + return ( +
+ {AGENTS.map((agent) => { + const count = findingCountById[agent.id] ?? 0; + const Icon = agent.icon; + const caught = hasReviews && count > 0; + return ( +
+
+ + + + {hasReviews ? ( + + {count} + + ) : ( + + — + + )} +
+
+

{agent.label}

+

+ {!hasReviews + ? "Stands ready" + : count === 0 + ? "Clear in its lane" + : `${count} verified ${count === 1 ? "finding" : "findings"}`} +

+
+
+ ); + })} +
+ ); +} diff --git a/packages/dashboard/src/components/dashboard/agents/agent-conversation.test.tsx b/packages/dashboard/src/components/dashboard/agents/agent-conversation.test.tsx index c4fac917..0cb06d43 100644 --- a/packages/dashboard/src/components/dashboard/agents/agent-conversation.test.tsx +++ b/packages/dashboard/src/components/dashboard/agents/agent-conversation.test.tsx @@ -16,6 +16,15 @@ function finding(over: Partial = {}): AgentActivityFinding } describe('AgentConversation', () => { +/* --- MERGED: PRESERVING UI (HEAD) --- */ + it('renders the agent header, model tier, and real finding rows', () => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain('Security'); + expect(html).toContain('Opus'); // security tier +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* it('renders the agent header, connected model, and real finding rows', () => { const html = renderToStaticMarkup( { ); expect(html).toContain('Security'); expect(html).toContain('qwen2.5-coder'); // dynamic connected model, not a hardcoded tier +*/ +/* --- END MERGE --- */ expect(html).toContain('src/auth/session.ts:42'); expect(html).toContain('Refresh token written to localStorage'); expect(html).toContain('PR #7'); diff --git a/packages/dashboard/src/components/dashboard/agents/agent-conversation.tsx b/packages/dashboard/src/components/dashboard/agents/agent-conversation.tsx index b54cc18b..6490e1db 100644 --- a/packages/dashboard/src/components/dashboard/agents/agent-conversation.tsx +++ b/packages/dashboard/src/components/dashboard/agents/agent-conversation.tsx @@ -1,5 +1,17 @@ "use client"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +import { useState, type FormEvent } from "react"; +import Link from "next/link"; +import { IconSettings, IconSend, IconArrowRight } from "@tabler/icons-react"; +import type { Agent } from "./agent-catalog"; +import type { AgentActivityFinding } from "@/lib/queries"; +import { cn } from "@/lib/utils"; + +const TIER_LABEL = { opus: "Opus", sonnet: "Sonnet" } as const; + +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { useEffect, useRef, useState, type FormEvent } from "react"; import Link from "next/link"; import { IconSettings, IconSend, IconArrowRight, IconCpu } from "@tabler/icons-react"; @@ -8,6 +20,8 @@ import type { AgentActivityFinding } from "@/lib/queries"; import type { ActiveModelConnection } from "@/lib/model-connections-map"; import { cn } from "@/lib/utils"; +*/ +/* --- END MERGE --- */ const SEV_PILL: Record = { error: "text-accent-danger border-accent-danger/30 bg-accent-danger/10", warning: "text-accent-warning border-accent-warning/30 bg-accent-warning/10", @@ -24,6 +38,9 @@ export interface AgentConversationProps { findings: AgentActivityFinding[]; findingCount: number; hasReviews: boolean; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* /** Whether a repository is connected. */ repoConnected?: boolean; /** Whether an AI model is connected — what the agent actually needs to run. */ @@ -36,6 +53,8 @@ export interface AgentConversationProps { * different, fresh conversation — the parent remounts this component (key) * when it changes, and history is (re)loaded for the new scope. */ containerId?: string | null; +*/ +/* --- END MERGE --- */ onConfigure: (agentId: string) => void; } @@ -47,11 +66,20 @@ export interface AgentConversationProps { * service is unreachable the composer surfaces a truthful notice instead of a * canned reply. */ +/* --- MERGED: PRESERVING UI (HEAD) --- */ +export function AgentConversation({ agent, findings, findingCount, hasReviews, onConfigure }: AgentConversationProps) { +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* export function AgentConversation({ agent, findings, findingCount, hasReviews, repoConnected = false, modelConnected = false, activeModel = null, containerId = null, onConfigure }: AgentConversationProps) { +*/ +/* --- END MERGE --- */ const [message, setMessage] = useState(""); const [turns, setTurns] = useState([]); const [sending, setSending] = useState(false); const [unavailable, setUnavailable] = useState(false); +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // Keep the newest message in view: the scroll region should follow the // conversation to the bottom as turns are appended (and while a reply is // pending), so replies never get cut off below the fold. @@ -78,6 +106,8 @@ export function AgentConversation({ agent, findings, findingCount, hasReviews, r }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); +*/ +/* --- END MERGE --- */ async function handleSend(e: FormEvent) { e.preventDefault(); @@ -91,6 +121,12 @@ export function AgentConversation({ agent, findings, findingCount, hasReviews, r const res = await fetch(`/api/agents/${agent.id}/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, +/* --- MERGED: PRESERVING UI (HEAD) --- */ + body: JSON.stringify({ message: text }), + }); + if (!res.ok) { +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* body: JSON.stringify({ message: text, containerId }), }); if (!res.ok) { @@ -103,6 +139,8 @@ export function AgentConversation({ agent, findings, findingCount, hasReviews, r setTurns((t) => [...t, { role: "agent", text: msg }]); return; } +*/ +/* --- END MERGE --- */ setUnavailable(true); return; } @@ -128,6 +166,12 @@ export function AgentConversation({ agent, findings, findingCount, hasReviews, r aria-hidden />

{agent.label}

+/* --- MERGED: PRESERVING UI (HEAD) --- */ + + {TIER_LABEL[agent.tier]} + +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* {activeModel && ( {activeModel.model}
)} +*/ +/* --- END MERGE --- */ {status} +

{gateHint}

+ + )} + + ); +} + +function Counter({ + label, + value, + tone, + icon, +}: { + label: string; + value: number; + tone: "success" | "muted" | "attention"; + icon: ReactNode; +}) { + const toneClass = + tone === "success" + ? "text-accent-success" + : tone === "attention" + ? "text-accent-warning" + : "text-content-muted"; + return ( +
+ + {value} + + + + {icon} + + {label} + +
+ ); +} diff --git a/packages/dashboard/src/components/dashboard/agents/synthesizer/synth-phase.test.ts b/packages/dashboard/src/components/dashboard/agents/synthesizer/synth-phase.test.ts index 44384d27..b24400d2 100644 --- a/packages/dashboard/src/components/dashboard/agents/synthesizer/synth-phase.test.ts +++ b/packages/dashboard/src/components/dashboard/agents/synthesizer/synth-phase.test.ts @@ -13,7 +13,12 @@ const ALL_STATES: ContainerState[] = [ "changes_requested", "merged", "dismissed", +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* "fix_failed", +*/ +/* --- END MERGE --- */ ]; describe("phaseOf", () => { @@ -29,7 +34,12 @@ describe("phaseOf", () => { changes_requested: "done", merged: "done", dismissed: "dismissed", +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* fix_failed: "dismissed", +*/ +/* --- END MERGE --- */ }; for (const s of ALL_STATES) expect(phaseOf(s)).toBe(expected[s]); }); diff --git a/packages/dashboard/src/components/dashboard/agents/synthesizer/synth-phase.ts b/packages/dashboard/src/components/dashboard/agents/synthesizer/synth-phase.ts index 50503f83..d998abe4 100644 --- a/packages/dashboard/src/components/dashboard/agents/synthesizer/synth-phase.ts +++ b/packages/dashboard/src/components/dashboard/agents/synthesizer/synth-phase.ts @@ -24,8 +24,13 @@ const PHASE_BY_STATE: Record = { changes_requested: "done", merged: "done", dismissed: "dismissed", +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // A failed fix run is terminal and non-success — render its final state, no motion. fix_failed: "dismissed", +*/ +/* --- END MERGE --- */ }; /** Progress toward a composed, ready PR — 0 at detection, 1 once ready or later. */ @@ -40,7 +45,12 @@ const PROGRESS_BY_STATE: Record = { changes_requested: 1, merged: 1, dismissed: 0, +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* fix_failed: 0, +*/ +/* --- END MERGE --- */ }; export function phaseOf(state: ContainerState): SynthPhase { diff --git a/packages/dashboard/src/components/dashboard/agents/synthesizer/synthesizer-summary.tsx b/packages/dashboard/src/components/dashboard/agents/synthesizer/synthesizer-summary.tsx new file mode 100644 index 00000000..7cdf260f --- /dev/null +++ b/packages/dashboard/src/components/dashboard/agents/synthesizer/synthesizer-summary.tsx @@ -0,0 +1,91 @@ +"use client"; + +import Link from "next/link"; +import { useReducedMotion } from "framer-motion"; +import { IconArrowRight } from "@tabler/icons-react"; +import { cn } from "@/lib/utils"; +import { isSampleContainerId } from "@/lib/issue-pipeline/sample-containers"; +import { useSynthStream } from "./use-synth-stream"; +import { shouldAnimate } from "./synth-transcript-visual"; +import { SynthProgress } from "./synth-progress"; +import { SynthAgentsRail } from "./synth-agents-rail"; +import { SynthLedger } from "./synth-ledger"; +import type { SynthPhase } from "./synth-phase"; + +/** + * Services big-picture variant (spec §3, "Services variant"). A condensed + * projection of the SAME container/stream the Agents console shows — no + * transcript detail: phase progress, which specialists worked, the verdict + * counts, the Post-PR gate, and a deep-link into the focused Agents view (the + * §4 handoff). Big picture = the *what*; the detailed *how* is one click away. + */ +export function SynthesizerSummary({ containerId }: { containerId: string }) { + // key by container so switching issues resets the stream (see use-synth-stream). + return ; +} + +const STATUS: Record = { + idle: "Detected — queued for review", + working: "Agents are solving this issue", + ready: "Verified — ready to post", + done: "Pull request posted", + dismissed: "Dismissed", +}; + +function SummaryStream({ containerId }: { containerId: string }) { + const view = useSynthStream(containerId); + const reducedMotion = useReducedMotion(); + const animating = shouldAnimate(view.phase, Boolean(reducedMotion)); + const isSample = isSampleContainerId(containerId); + + return ( +
+
+ +

Synthesizer

+ {isSample && ( + + Sample + + )} + {STATUS[view.phase]} +
+ + + +
+ +
+ +
+
+ + + See how the agents solved this + + +
+ ); +} diff --git a/packages/dashboard/src/components/dashboard/dashboard-shell.tsx b/packages/dashboard/src/components/dashboard/dashboard-shell.tsx index e9178637..ab9caf76 100644 --- a/packages/dashboard/src/components/dashboard/dashboard-shell.tsx +++ b/packages/dashboard/src/components/dashboard/dashboard-shell.tsx @@ -5,7 +5,12 @@ import { Sidebar } from "./sidebar"; import { Topbar } from "./topbar"; import { cn } from "@/lib/utils"; import type { AuthorizedInstallation } from "@/lib/installations"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import type { ActiveModelConnection } from "@/lib/model-connections-map"; +*/ +/* --- END MERGE --- */ import { LoadingProvider } from "@/lib/loading-context"; interface DashboardShellProps { diff --git a/packages/dashboard/src/components/dashboard/services/services-workspace.test.tsx b/packages/dashboard/src/components/dashboard/services/services-workspace.test.tsx index 6d6b9c51..7c5121e8 100644 --- a/packages/dashboard/src/components/dashboard/services/services-workspace.test.tsx +++ b/packages/dashboard/src/components/dashboard/services/services-workspace.test.tsx @@ -1,3 +1,9 @@ +/* --- MERGED: PRESERVING UI (HEAD) --- */ +import { describe, it, expect } from 'vitest'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { ServicesWorkspace } from './services-workspace'; +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { describe, it, expect, vi } from 'vitest'; import { renderToStaticMarkup } from 'react-dom/server'; @@ -204,19 +210,38 @@ describe('ServicesWorkspace — review panel (real mode)', () => { expect(html).not.toContain('Verified findings'); }); }); +*/ +/* --- END MERGE --- */ describe('ServicesWorkspace', () => { it('hosts the Synthesizer in the center with a connect CTA when nothing is connected', () => { // The authenticated (embedded) center is now the Synthesizer's home — +/* --- MERGED: PRESERVING UI (HEAD) --- */ + // its onboarding state, plus a Connect-a-repository CTA. + const html = renderToStaticMarkup(); + + expect(html).toContain('The Synthesizer coordinates every review'); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // its professional introduction, plus a Connect-a-repository CTA. const html = renderToStaticMarkup(); expect(html).toContain('Kuma'); expect(html).toContain('How a review runs'); +*/ +/* --- END MERGE --- */ expect(html).toContain('Connect a repository'); expect(html).toContain('/connections'); }); +/* --- MERGED: PRESERVING UI (HEAD) --- */ + it('shows the connected onboarding state when a repository is connected', () => { + const html = renderToStaticMarkup(); + + expect(html).toContain('The Synthesizer coordinates every review'); + expect(html).toContain('awaiting your first pull request'); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* it('introduces Kuma without loading/waiting talk when a repository is connected', () => { const html = renderToStaticMarkup(); @@ -452,5 +477,7 @@ describe('ServicesWorkspace — work-item inbox', () => { ); expect(html).toContain('Scanning'); +*/ +/* --- END MERGE --- */ }); }); diff --git a/packages/dashboard/src/components/dashboard/services/services-workspace.tsx b/packages/dashboard/src/components/dashboard/services/services-workspace.tsx index afe8f644..5edd3f10 100644 --- a/packages/dashboard/src/components/dashboard/services/services-workspace.tsx +++ b/packages/dashboard/src/components/dashboard/services/services-workspace.tsx @@ -1,5 +1,14 @@ "use client"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +import { useState, useEffect, useRef, type ReactNode } from "react"; +import Link from "next/link"; +import { SynthesizerConsole } from "../agents/synthesizer-console"; +import { motion, AnimatePresence, useInView } from "framer-motion"; +import { IconArrowRight, IconChevronDown, IconCopy, IconGitBranch, IconGitPullRequest, IconHourglassHigh, IconPlus, IconLoader2, IconCheck } from "@tabler/icons-react"; +import { KumaLogo } from "@/components/ui/kuma-logo"; +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { useState, useEffect, useRef, useSyncExternalStore } from "react"; import Link from "next/link"; import { SynthesizerConsole } from "../agents/synthesizer-console"; @@ -30,6 +39,8 @@ import { AgentConfigDrawer } from "../agents/agent-config-drawer"; import { AgentsSection } from "./agents-section"; import type { AgentActivityFinding } from "@/lib/queries"; import type { ActiveModelConnection } from "@/lib/model-connections-map"; +*/ +/* --- END MERGE --- */ /** * Services "Triage Inbox" workspace. Production signals from CONNECTED @@ -55,6 +66,206 @@ import type { ActiveModelConnection } from "@/lib/model-connections-map"; * this file, so fabricated telemetry cannot reach a real account screen. */ +/* --- MERGED: PRESERVING UI (HEAD) --- */ +export type Severity = "critical" | "high" | "medium"; + +export interface DiffRow { + kind: "context" | "add" | "remove"; + text: string; +} + +export interface Issue { + id: string; + serviceId: string; + source: string; // Sentry | Stripe | CI | Vercel | PostHog | Kuma + severity: Severity; + status: string; + agent: string; + title: string; + occurrences: string; + lastSeen: string; + where: string; // file:line + summary: string; + evidence: { file: string; rows: Array<[string, string]> }; + fix: { file: string; rows: DiffRow[] }; + timeline: Array<{ tone: Severity | "good" | "accent"; text: string; when: string }>; +} + +export interface Service { + id: string; + open: number; + worst: Severity | "clear"; +} + +// ── Illustrative SAMPLE data — marketing preview ONLY, never the real page ── +export const SAMPLE_SERVICES: Service[] = [ + { id: "payments-api", open: 3, worst: "critical" }, + { id: "auth-gateway", open: 2, worst: "high" }, + { id: "orders-service", open: 2, worst: "high" }, + { id: "notifications", open: 1, worst: "medium" }, + { id: "web-dashboard", open: 0, worst: "clear" }, +]; + +export const SAMPLE_ISSUES: Issue[] = [ + { + id: "p1", serviceId: "payments-api", source: "Sentry", severity: "critical", + status: "Agent fixing", agent: "Business Logic", + title: "TypeError: Cannot read 'balance' of null", + occurrences: "1,204 events", lastSeen: "2m ago", where: "src/billing/charge.ts:23", + summary: "Free-tier users have a null balance; the charge path assumes a number and throws before the request completes.", + evidence: { file: "Sentry · TypeError · last 24h", rows: [["user.tier", "'free'"], ["balance", "None ← null"], ["at charge()", "src/billing/charge.ts:23"]] }, + fix: { file: "src/billing/charge.ts", rows: [ + { kind: "context", text: "function charge(order, user) {" }, + { kind: "remove", text: " const amount = order.total * user.balance" }, + { kind: "add", text: " const bal = user.balance ?? 0" }, + { kind: "add", text: " const amount = order.total * bal" }, + { kind: "context", text: " return stripe.charges.create({ amount })" }, + { kind: "context", text: "}" }, + ] }, + timeline: [ + { tone: "critical", text: "Error detected", when: "Sentry · 2m ago" }, + { tone: "accent", text: "Business Logic agent picked it up", when: "1m ago" }, + { tone: "accent", text: "Cross-checked with the diff & telemetry", when: "40s ago" }, + { tone: "good", text: "Fix proposed — awaiting your approval", when: "just now" }, + ], + }, + { + id: "p2", serviceId: "payments-api", source: "Stripe", severity: "high", + status: "Fix proposed", agent: "Business Logic", + title: "Charge retried without an idempotency key", + occurrences: "47 events", lastSeen: "18m ago", where: "src/billing/charge.ts:31", + summary: "This path already retries on timeout; without an idempotency key a retry can double-charge the customer.", + evidence: { file: "Stripe · duplicate_charge · 7d", rows: [["event", "charge.retried"], ["idempotency_key", "null ← missing"], ["impact", "2 customers double-charged"]] }, + fix: { file: "src/billing/charge.ts", rows: [ + { kind: "context", text: "await stripe.charges.create(" }, + { kind: "context", text: " { amount, currency: 'usd' }," }, + { kind: "remove", text: ")" }, + { kind: "add", text: " { idempotencyKey: order.id }," }, + { kind: "add", text: ")" }, + ] }, + timeline: [ + { tone: "high", text: "Duplicate charges detected", when: "Stripe · 18m ago" }, + { tone: "accent", text: "Business Logic agent analyzing", when: "15m ago" }, + { tone: "good", text: "Fix proposed", when: "12m ago" }, + ], + }, + { + id: "p3", serviceId: "payments-api", source: "CI", severity: "medium", + status: "Triaging", agent: "Test Coverage", + title: "Flaky test: refund rounding", + occurrences: "6 of 20 runs", lastSeen: "1h ago", where: "src/orders/refund.test.ts:15", + summary: "The partial-refund test fails intermittently on rounding — the assertion compares floats directly.", + evidence: { file: "GitHub Actions · test · 20 runs", rows: [["expected", "4.50"], ["received", "4.499999999"], ["flake_rate", "30%"]] }, + fix: { file: "src/orders/refund.test.ts", rows: [ + { kind: "remove", text: "expect(r.amount).toBe(4.5)" }, + { kind: "add", text: "expect(r.amount).toBeCloseTo(4.5, 2)" }, + ] }, + timeline: [ + { tone: "medium", text: "Flaky test flagged", when: "CI · 1h ago" }, + { tone: "accent", text: "Test Coverage agent triaging", when: "55m ago" }, + ], + }, + { + id: "a1", serviceId: "auth-gateway", source: "Kuma", severity: "high", + status: "Fix proposed", agent: "Security", + title: "Refresh token written to localStorage", + occurrences: "312 events", lastSeen: "22m ago", where: "src/auth/session.ts:42", + summary: "Tokens in localStorage are exposed to any script — an XSS bug would leak long-lived sessions.", + evidence: { file: "Kuma review · Security · PR #418", rows: [["storage", "localStorage"], ["token_ttl", "30 days"], ["risk", "XSS → session theft"]] }, + fix: { file: "src/auth/session.ts", rows: [ + { kind: "remove", text: "localStorage.setItem('refresh', token)" }, + { kind: "add", text: "cookies().set('refresh', token, {" }, + { kind: "add", text: " httpOnly: true, secure: true })" }, + ] }, + timeline: [ + { tone: "high", text: "Flagged on PR #418", when: "Kuma · 22m ago" }, + { tone: "accent", text: "Security agent analyzing", when: "20m ago" }, + { tone: "good", text: "Fix proposed", when: "16m ago" }, + ], + }, + { + id: "a2", serviceId: "auth-gateway", source: "Vercel", severity: "medium", + status: "Triaging", agent: "Deployment Safety", + title: "AUTH_SECRET missing in preview builds", + occurrences: "preview builds", lastSeen: "40m ago", where: "vercel.json", + summary: "Preview deployments boot without AUTH_SECRET, so login silently 500s on every PR preview.", + evidence: { file: "Vercel · deploy · preview", rows: [["AUTH_SECRET", "undefined"], ["env", "preview"], ["result", "login 500"]] }, + fix: { file: "vercel.json", rows: [ + { kind: "context", text: '"env": {' }, + { kind: "add", text: ' "AUTH_SECRET": "@auth_secret"' }, + { kind: "context", text: "}" }, + ] }, + timeline: [ + { tone: "medium", text: "Preview boot failure", when: "Vercel · 40m ago" }, + { tone: "accent", text: "Deployment Safety triaging", when: "38m ago" }, + ], + }, + { + id: "o1", serviceId: "orders-service", source: "Sentry", severity: "high", + status: "Agent fixing", agent: "Performance", + title: "N+1 query on the orders list", + occurrences: "8,900 events", lastSeen: "5m ago", where: "src/orders/list.ts:88", + summary: "Each order row fires its own query; a 500-order page issues 500 round-trips and times out under load.", + evidence: { file: "Sentry · slow_query · p95", rows: [["queries_per_req", "512"], ["p95_latency", "4.2s"], ["budget", "800ms"]] }, + fix: { file: "src/orders/list.ts", rows: [ + { kind: "remove", text: "for (const id of ids)" }, + { kind: "remove", text: " orders.push(await find(id))" }, + { kind: "add", text: "const orders = await db.order.findMany({" }, + { kind: "add", text: " where: { id: { in: ids } } })" }, + ] }, + timeline: [ + { tone: "high", text: "Latency regression", when: "Sentry · 5m ago" }, + { tone: "accent", text: "Performance agent picked it up", when: "4m ago" }, + { tone: "accent", text: "Building the fix", when: "1m ago" }, + ], + }, + { + id: "o2", serviceId: "orders-service", source: "CI", severity: "medium", + status: "Triaging", agent: "Quality", + title: "Dead code after checkout refactor", + occurrences: "lint", lastSeen: "2h ago", where: "src/orders/legacy.ts", + summary: "A whole module is unreferenced after the checkout refactor.", + evidence: { file: "CI · knip", rows: [["unused_exports", "7"], ["file", "legacy.ts"]] }, + fix: { file: "src/orders/legacy.ts", rows: [{ kind: "remove", text: "// entire file unused — safe to delete" }] }, + timeline: [{ tone: "medium", text: "Dead code flagged", when: "CI · 2h ago" }], + }, + { + id: "n1", serviceId: "notifications", source: "PostHog", severity: "medium", + status: "Triaging", agent: "Business Logic", + title: "Signup → verify funnel dropped 18%", + occurrences: "trend", lastSeen: "3h ago", where: "src/notify/email.ts", + summary: "Verification emails started bouncing after the template change; PostHog shows the funnel drop.", + evidence: { file: "PostHog · funnel · 24h", rows: [["step", "verify_email"], ["drop", "-18%"], ["suspect", "template v3"]] }, + fix: { file: "src/notify/email.ts", rows: [ + { kind: "remove", text: "from: 'no-reply@localhost'" }, + { kind: "add", text: "from: process.env.MAIL_FROM" }, + ] }, + timeline: [{ tone: "medium", text: "Funnel anomaly", when: "PostHog · 3h ago" }], + }, +]; + +// ── Style maps (semantic tokens — adapt to both themes) ────────────────────── +const SEV_DOT: Record = { + critical: "bg-accent-danger", high: "bg-accent-warning", medium: "bg-accent-info", clear: "bg-accent-success", +}; +const SEV_PILL: Record = { + critical: "text-accent-danger border-accent-danger/30 bg-accent-danger/10", + high: "text-accent-warning border-accent-warning/30 bg-accent-warning/10", + medium: "text-accent-info border-accent-info/30 bg-accent-info/10", +}; +const SEV_LABEL: Record = { critical: "Critical", high: "High", medium: "Medium" }; +const TONE_TEXT: Record = { + critical: "text-accent-danger", high: "text-accent-warning", medium: "text-accent-info", + good: "text-accent-success", accent: "text-accent-primary", +}; + +function markerForTone(tone: Issue["timeline"][number]["tone"]): string { + if (tone === "good") return "✓"; + if (tone === "accent") return "◈"; + return "●"; // critical/high/medium — the telemetry source's own detection +} +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // Back-compat public surface. The data contract and the work-item panel were // split into their own modules, but both were imported FROM this path before // the split (`diff-view.tsx`, `diff-stat.ts` and their tests take `DiffRow` @@ -62,6 +273,8 @@ import type { ActiveModelConnection } from "@/lib/model-connections-map"; // every one of those import paths resolving, so the split touched no importer. export type { Severity, DiffRow, Issue, Service } from "./types"; export { WorkItemPanel } from "./work-item-panel"; +*/ +/* --- END MERGE --- */ export interface ServicesWorkspaceProps { services?: Service[]; @@ -81,6 +294,9 @@ export interface ServicesWorkspaceProps { * Synthesizer shows its onboarding state. */ containerId?: string | null; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* /** * The tenant's connected repositories (full names). Listed in the rail even * before any review runs — a connected repo is a populated state (the Git @@ -142,6 +358,8 @@ function useHasMounted(): boolean { () => true, () => false, ); +*/ +/* --- END MERGE --- */ } /** @@ -150,11 +368,22 @@ function useHasMounted(): boolean { * fabricated rows. The marketing preview passes SAMPLE_* + variant="framed" * to show the populated UI inside a card. */ +/* --- MERGED: PRESERVING UI (HEAD) --- */ +export function ServicesWorkspace({ services = [], issues = [], variant = "embedded", connected = false, containerId = null }: ServicesWorkspaceProps) { +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* export function ServicesWorkspace({ services = [], issues = [], variant = "embedded", connected = false, containerId = null, reviewGroups, repositories = [], inbox = null, approvals = [], activity = [], findingCountById = {}, activeModel = null, modelConnected = false }: ServicesWorkspaceProps) { +*/ +/* --- END MERGE --- */ const containerRef = useRef(null); const isInView = useInView(containerRef, { margin: "-100px 0px -100px 0px" }); // Guard against hydration: defer observer logic until after first client render +/* --- MERGED: PRESERVING UI (HEAD) --- */ + const [hasMounted, setHasMounted] = useState(false); + useEffect(() => { setHasMounted(true); }, []); +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* const hasMounted = useHasMounted(); // Real mode: the authenticated /services page passes reviewGroups (even []), @@ -198,6 +427,8 @@ export function ServicesWorkspace({ services = [], issues = [], variant = "embed // Leaving agent mode: the centre pane returns to this item's transcript. setActiveAgentId(null); } +*/ +/* --- END MERGE --- */ const [serviceId, setServiceId] = useState(services[0]?.id ?? null); const [issueId, setIssueId] = useState( @@ -288,9 +519,15 @@ export function ServicesWorkspace({ services = [], issues = [], variant = "embed : deriveTriage(issues.map((i) => ({ status: sampleStatus(i.status) }))); return ( +/* --- MERGED: PRESERVING UI (HEAD) --- */ +
+/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/*
+*/ +/* --- END MERGE --- */ {/* Rail: services (each expandable to its issues) + connect catalog */}
@@ -505,6 +742,14 @@ export function ServicesWorkspace({ services = [], issues = [], variant = "embed className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-border-default bg-surface-2 px-3 py-2 text-[12px] font-medium text-content-secondary transition-colors hover:border-border-strong hover:bg-content-primary/5" > +/* --- MERGED: PRESERVING UI (HEAD) --- */ + {hasServices + ? "Connect more services" + : connected + ? "Connect a telemetry source" + : "Connect your services"} +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* {realMode ? connected ? "Add connections" @@ -514,6 +759,8 @@ export function ServicesWorkspace({ services = [], issues = [], variant = "embed : connected ? "Connect a telemetry source" : "Connect your services"} +*/ +/* --- END MERGE --- */
@@ -526,6 +773,106 @@ export function ServicesWorkspace({ services = [], issues = [], variant = "embed illustrative sample data). */}
{variant === "embedded" ? ( +/* --- MERGED: PRESERVING UI (HEAD) --- */ + // Streams the deep-linked review (container id = review id) via the + // existing /api/containers/[id]/stream SSE; null → onboarding state. + + ) : ( + + )} +
+ + {/* Right: the issue's own detail */} +
+ +
+
+ ); +} + +/** + * Center pane: the Synthesizer's verification narrative for the selected + * issue. Structurally identical to the /agents Synthesizer console (header + + * scripted transcript + pinned, disabled chat input) — reusing the same + * shape is deliberate: it's the same Synthesizer, just focused on one issue + * instead of the whole account, so the verification record for a given + * issue/PR reads the same on both pages. + */ +function IssueSynthesizerConsole({ issue, isReplaying, replayStep }: { issue: Issue | null; isReplaying: boolean; replayStep: number }) { + return ( + <> +
+ +

Synthesizer

+ Preview + {issue && focused on {issue.title}} +
+ +
+ {issue ? ( +
    +
  1. + Scripted replay of this issue's verification — not a live model +
  2. + {issue.timeline.map((t, idx) => { + const status = isReplaying ? (replayStep > idx ? 'done' : replayStep === idx ? 'running' : 'waiting') : 'done'; + if (status === 'waiting') return null; + + return ( + +
    + {status === 'running' ? ( + + + + ) : ( + {markerForTone(t.tone)} + )} +
    +

    {t.text}

    +

    {t.when}

    +
    +
    +
    + ); + })} +
+ ) : ( +
+
+ +
+
+

The Synthesizer verifies every issue

+

+ Pick a service and an issue on the left to see how the Synthesizer verified it — the + same verification record whether you look at it here or on the Agents page. +

+
+ + {/* Same connect CTA as the Agents page, so a fresh account has a + clear first step from the center pane, not just the rail. */} + + Connect a repository + + +
+/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* // Streams the selected review (container id = review id) via the // existing /api/containers/[id]/stream SSE; null → onboarding state. // Real mode drives it from the rail selection; otherwise the @@ -557,6 +904,8 @@ export function ServicesWorkspace({ services = [], issues = [], variant = "embed ) ) : ( +*/ +/* --- END MERGE --- */ )} @@ -576,6 +925,203 @@ export function ServicesWorkspace({ services = [], issues = [], variant = "embed
+/* --- MERGED: PRESERVING UI (HEAD) --- */ + {/* Pinned input strip — deliberately disabled: no live model yet, same + honesty pattern as the /agents Synthesizer console. */} +
+
+ > + +
+

+ preview shell · live chat coming soon{issue ? ` · focused on ${issue.serviceId}` : ""} +

+
+ + ); +} + +/** + * Right pane: the issue's concrete detail — agents involved, evidence, the + * formatted pull request — repo target, the PR title/body, and the review + * comment(s) as they'll post — plus the send gate. Per the pipeline spec, the + * repo target and Post PR / Request changes live HERE (Services), not on Agents. + */ +function IssuePanel({ issue, isReplaying }: { issue: Issue | null; isReplaying: boolean }) { + return ( + <> +
+

Pull request

+ {issue && !isReplaying && ( + {SEV_LABEL[issue.severity]} + )} +
+ + {!issue || isReplaying ? ( + + +
+

+ {isReplaying ? "Synthesizing pull request from verified findings..." : "Select an issue to load its pull request — the formatted review Kuma will post, and where you approve sending it."} +

+
+
+ +

+ The target repo and base ← branch the PR opens against. +

+
+ +

+ The formatted PR — title and description — assembled from the verified findings. +

+
+ +

+ Each verified finding, rendered as the path:line comment it posts to the PR. +

+
+
+
+ +
+ + +
+

+ Posting opens the PR on your repo — the solution is approved on the Agents page first. +

+
+
+
+ ) : ( + + {/* The big-picture Synthesizer projection (SynthesizerSummary) mounts + here once a real IssueContainer backs the selected issue — it is + deliberately NOT wired to sample data, so the product surface never + shows a fabricated review (connector step unifies Issue↔Container). */} + + {/* Repository target */} +
+ {/* Repo selector → Connections: the direct path to install/manage + the Kuma GitHub App, where repos are actually connected. When no + repo is connected the same link is how you add one. */} + + + acme-corp/{issue.serviceId} + + +

main ← arete/fix-{issue.id}

+
+ +
+ +
+

Fix: {issue.title}

+

{issue.summary}

+
+
+ + +
+
+ {SEV_LABEL[issue.severity]} + {issue.where} +
+
+
{issue.fix.file}
+
+                    {issue.fix.rows.map((r, idx) => (
+                      
+ + {r.kind === "add" ? "+" : r.kind === "remove" ? "-" : " "} + + {r.text} +
+ ))} +
+
+
+
+
+ +
+ +
+ + +
+

+ Posting opens the pull request on your repo — the solution is approved on the Agents page first. +

+
+
+ )} + + ); +} + +function PanelSection({ title, children }: { title: string; children: ReactNode }) { + const [open, setOpen] = useState(true); + + return ( +
+ + {open &&
{children}
} +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* {/* Parameters open over the workspace rather than navigating away — that is the "config controls in place" half of Stage 2.2. The drawer is unchanged from /agents, including its honestly-disabled Save: making @@ -588,6 +1134,8 @@ export function ServicesWorkspace({ services = [], issues = [], variant = "embed activeModel={activeModel} onClose={() => setConfigAgentId(null)} /> +*/ +/* --- END MERGE --- */
); } diff --git a/packages/dashboard/src/components/dashboard/sidebar.tsx b/packages/dashboard/src/components/dashboard/sidebar.tsx index 585228b2..496c0be9 100644 --- a/packages/dashboard/src/components/dashboard/sidebar.tsx +++ b/packages/dashboard/src/components/dashboard/sidebar.tsx @@ -10,7 +10,12 @@ import { springTransition } from "@/lib/motion"; import { InstallationSwitcher } from "@/components/InstallationSwitcher"; import { KumaLogo } from "@/components/ui/kuma-logo"; import { useGlobalLoading } from "@/lib/loading-context"; +/* --- MERGED: PRESERVING UI (HEAD) --- */ +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* import { MODEL_PROVIDERS } from "@/lib/model-catalog"; +*/ +/* --- END MERGE --- */ import type { ReactNode } from "react"; import type { AuthorizedInstallation } from "@/lib/installations"; import type { ActiveModelConnection } from "@/lib/model-connections-map"; @@ -56,6 +61,17 @@ interface SidebarProps { export function Sidebar({ collapsed, onToggleCollapsed, installations, userName, userEmail, activeModel, signOutSlot }: SidebarProps) { const pathname = usePathname(); +/* --- MERGED: PRESERVING UI (HEAD) --- */ + const { isLoading, setIsLoading } = useGlobalLoading(); + const initial = userName.charAt(0).toUpperCase(); + + // A quick demonstration handler to prove the decoupled data pipeline loading state + const handleSimulateLoad = () => { + setIsLoading(true); + setTimeout(() => setIsLoading(false), 3000); // Spins for 3 seconds simulating a heavy data fetch + }; +/* --- MERGED: NEW LOGIC FROM MAIN (COMMENTED OUT FOR REVIEW) --- */ +/* const router = useRouter(); const { isLoading } = useGlobalLoading(); const initial = userName.charAt(0).toUpperCase(); @@ -84,6 +100,8 @@ export function Sidebar({ collapsed, onToggleCollapsed, installations, userName, const activeProviderName = activeModel ? MODEL_PROVIDERS.find((p) => p.id === activeModel.provider)?.name ?? activeModel.provider : null; +*/ +/* --- END MERGE --- */ return (