Merge: glassmorphism + gradient theme, and review-inbox transparency
App-wide glassmorphism (frosted cards/popovers/sheets/inputs/pills + gradient field, gradient primary actions, dark-glass sidebar) with contrast fixes; and a restructured review inbox showing each held item as Action -> Result -> Run log, with the run log surfacing latency, skills, tools called, memory hits, product-identity inclusion, raw output, and the assembled prompt (enriched assembler run endpoint).
This commit is contained in:
@@ -27,8 +27,11 @@ export function AppShell({ children }: { children: ReactNode }) {
|
|||||||
const logout = useAuth((s) => s.logout)
|
const logout = useAuth((s) => s.logout)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-background text-foreground">
|
<div className="flex min-h-screen text-foreground">
|
||||||
<aside className="flex w-60 shrink-0 flex-col bg-sidebar text-sidebar-foreground">
|
<aside
|
||||||
|
className="flex w-60 shrink-0 flex-col border-r border-white/15 text-sidebar-foreground backdrop-blur-2xl"
|
||||||
|
style={{ background: 'linear-gradient(180deg, oklch(0.27 0.1 287 / 0.78) 0%, oklch(0.2 0.085 298 / 0.78) 100%)' }}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3 px-5 py-4">
|
<div className="flex items-center gap-3 px-5 py-4">
|
||||||
<span className="grid size-8 place-items-center rounded-md bg-sidebar-primary font-bold text-sidebar-primary-foreground">
|
<span className="grid size-8 place-items-center rounded-md bg-sidebar-primary font-bold text-sidebar-primary-foreground">
|
||||||
T
|
T
|
||||||
@@ -93,9 +96,11 @@ function NavItem({
|
|||||||
const active = to ? location.pathname === to : false
|
const active = to ? location.pathname === to : false
|
||||||
|
|
||||||
const className = cn(
|
const className = cn(
|
||||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm',
|
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors',
|
||||||
active ? 'bg-sidebar-accent font-medium text-sidebar-accent-foreground' : 'text-sidebar-foreground/80',
|
active
|
||||||
muted ? 'opacity-50' : 'hover:bg-sidebar-accent/60',
|
? 'bg-white/15 font-medium text-white shadow-sm ring-1 ring-white/15 backdrop-blur-sm'
|
||||||
|
: 'text-sidebar-foreground/80',
|
||||||
|
muted ? 'opacity-50' : 'hover:bg-white/10 hover:text-white',
|
||||||
)
|
)
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
|
|||||||
+105
-13
@@ -8,21 +8,21 @@
|
|||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
|
|
||||||
/* Light content surface — the "calm command center" body. */
|
/* Glassmorphism body — frosted surfaces over a vivid gradient field. */
|
||||||
--background: oklch(0.99 0.003 280);
|
--background: oklch(0.95 0.022 286);
|
||||||
--foreground: oklch(0.21 0.03 280);
|
--foreground: oklch(0.19 0.03 280);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0 / 0.74);
|
||||||
--card-foreground: oklch(0.21 0.03 280);
|
--card-foreground: oklch(0.19 0.03 280);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0 / 0.92);
|
||||||
--popover-foreground: oklch(0.21 0.03 280);
|
--popover-foreground: oklch(0.19 0.03 280);
|
||||||
|
|
||||||
/* Brand: indigo, rationed so it always means something. */
|
/* Brand: indigo, rationed so it always means something. */
|
||||||
--primary: oklch(0.511 0.262 276.966);
|
--primary: oklch(0.511 0.262 276.966);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.967 0.012 280);
|
--secondary: oklch(0.967 0.012 280);
|
||||||
--secondary-foreground: oklch(0.3 0.05 280);
|
--secondary-foreground: oklch(0.3 0.05 280);
|
||||||
--muted: oklch(0.967 0.006 280);
|
--muted: oklch(0.95 0.01 280 / 0.6);
|
||||||
--muted-foreground: oklch(0.52 0.03 280);
|
--muted-foreground: oklch(0.44 0.035 280);
|
||||||
--accent: oklch(0.95 0.03 280);
|
--accent: oklch(0.95 0.03 280);
|
||||||
--accent-foreground: oklch(0.4 0.16 277);
|
--accent-foreground: oklch(0.4 0.16 277);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
@@ -57,6 +57,14 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Hanken Grotesk Variable", system-ui, sans-serif;
|
font-family: "Hanken Grotesk Variable", system-ui, sans-serif;
|
||||||
|
/* Vivid gradient field behind the frosted-glass surfaces — gives the glass something to lift off. */
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 640px at 6% -10%, oklch(0.62 0.2 288 / 0.34), transparent 62%),
|
||||||
|
radial-gradient(1050px 720px at 112% 4%, oklch(0.7 0.16 210 / 0.27), transparent 58%),
|
||||||
|
radial-gradient(960px 680px at 48% 122%, oklch(0.72 0.18 334 / 0.22), transparent 62%),
|
||||||
|
var(--background);
|
||||||
|
background-attachment: fixed;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -110,11 +118,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.205 0.03 280);
|
--background: oklch(0.17 0.035 287);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.257 0.04 281);
|
--card: oklch(0.31 0.055 286 / 0.62);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.257 0.04 281);
|
--popover: oklch(0.26 0.05 286 / 0.94);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.673 0.182 276.935);
|
--primary: oklch(0.673 0.182 276.935);
|
||||||
--primary-foreground: oklch(0.205 0.03 280);
|
--primary-foreground: oklch(0.205 0.03 280);
|
||||||
@@ -143,9 +151,93 @@ body {
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Glassmorphism + gradients (app-wide, keyed on shadcn data-slots) ----
|
||||||
|
* Unlayered so they sit above Tailwind utilities; inline styles still win, so the
|
||||||
|
* gradient Team cards keep their own backgrounds. */
|
||||||
|
[data-slot="card"] {
|
||||||
|
backdrop-filter: blur(20px) saturate(155%);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(155%);
|
||||||
|
border: 1px solid color-mix(in oklch, white 65%, transparent);
|
||||||
|
box-shadow: 0 16px 44px -20px oklch(0.32 0.13 285 / 0.42), inset 0 1px 0 0 oklch(1 0 0 / 0.55);
|
||||||
|
}
|
||||||
|
.dark [data-slot="card"] {
|
||||||
|
border-color: color-mix(in oklch, white 16%, transparent);
|
||||||
|
box-shadow: 0 18px 48px -22px oklch(0 0 0 / 0.65), inset 0 1px 0 0 oklch(1 0 0 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="popover-content"],
|
||||||
|
[data-slot="select-content"],
|
||||||
|
[data-slot="dropdown-menu-content"],
|
||||||
|
[data-slot="sheet-content"] {
|
||||||
|
backdrop-filter: blur(18px) saturate(160%);
|
||||||
|
-webkit-backdrop-filter: blur(18px) saturate(160%);
|
||||||
|
border: 1px solid color-mix(in oklch, white 40%, transparent);
|
||||||
|
}
|
||||||
|
.dark [data-slot="popover-content"],
|
||||||
|
.dark [data-slot="select-content"],
|
||||||
|
.dark [data-slot="dropdown-menu-content"],
|
||||||
|
.dark [data-slot="sheet-content"] {
|
||||||
|
border-color: color-mix(in oklch, white 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary actions become a gradient; secondary/outline become glass. */
|
||||||
|
[data-slot="button"][data-variant="default"] {
|
||||||
|
background-image: linear-gradient(135deg, oklch(0.58 0.24 277) 0%, oklch(0.56 0.25 305) 100%);
|
||||||
|
box-shadow: 0 8px 20px -10px oklch(0.5 0.23 288 / 0.7);
|
||||||
|
}
|
||||||
|
[data-slot="button"][data-variant="default"]:hover {
|
||||||
|
background-image: linear-gradient(135deg, oklch(0.62 0.24 277) 0%, oklch(0.6 0.25 305) 100%);
|
||||||
|
}
|
||||||
|
[data-slot="button"][data-variant="outline"],
|
||||||
|
[data-slot="button"][data-variant="secondary"] {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
background-color: color-mix(in oklch, var(--card) 65%, transparent);
|
||||||
|
border: 1px solid color-mix(in oklch, white 40%, transparent);
|
||||||
|
}
|
||||||
|
.dark [data-slot="button"][data-variant="outline"],
|
||||||
|
.dark [data-slot="button"][data-variant="secondary"] {
|
||||||
|
border-color: color-mix(in oklch, white 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Frosted form fields — kept more opaque than cards so input text stays high-contrast. */
|
||||||
|
[data-slot="select-trigger"],
|
||||||
|
[data-slot="input"],
|
||||||
|
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
||||||
|
textarea {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
background-color: color-mix(in oklch, white 74%, transparent) !important;
|
||||||
|
border-color: color-mix(in oklch, oklch(0.5 0.04 285) 35%, transparent);
|
||||||
|
}
|
||||||
|
.dark [data-slot="select-trigger"],
|
||||||
|
.dark [data-slot="input"],
|
||||||
|
.dark input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
||||||
|
.dark textarea {
|
||||||
|
background-color: color-mix(in oklch, oklch(0.32 0.05 286) 82%, transparent) !important;
|
||||||
|
border-color: color-mix(in oklch, white 16%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pills: frosted for neutral variants, gradient for the primary one. */
|
||||||
|
[data-slot="badge"][data-variant="secondary"],
|
||||||
|
[data-slot="badge"][data-variant="outline"] {
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
background-color: color-mix(in oklch, white 58%, transparent);
|
||||||
|
border-color: color-mix(in oklch, white 55%, transparent);
|
||||||
|
}
|
||||||
|
.dark [data-slot="badge"][data-variant="secondary"],
|
||||||
|
.dark [data-slot="badge"][data-variant="outline"] {
|
||||||
|
background-color: color-mix(in oklch, white 12%, transparent);
|
||||||
|
border-color: color-mix(in oklch, white 16%, transparent);
|
||||||
|
}
|
||||||
|
[data-slot="badge"][data-variant="default"] {
|
||||||
|
background-image: linear-gradient(135deg, oklch(0.58 0.24 277), oklch(0.56 0.25 305));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { ChevronDown, ChevronRight, ShieldAlert } from 'lucide-react'
|
import { ChevronDown, ChevronRight, Clock, Cpu, ScrollText, ShieldAlert, Wrench } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { AppShell } from '@/components/AppShell'
|
import { AppShell } from '@/components/AppShell'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -30,6 +30,43 @@ interface ReviewItem {
|
|||||||
createdAtUtc: string
|
createdAtUtc: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RunDetail {
|
||||||
|
status: string
|
||||||
|
output: string | null
|
||||||
|
prompt: string | null
|
||||||
|
error: string | null
|
||||||
|
trace: string | null
|
||||||
|
resultJson: string | null
|
||||||
|
latencyMs: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunTrace {
|
||||||
|
agent?: string
|
||||||
|
autonomy?: string
|
||||||
|
skills?: string[]
|
||||||
|
tools?: string[]
|
||||||
|
docs?: string[]
|
||||||
|
memories?: number
|
||||||
|
product?: { productId?: string | null; identity?: boolean }
|
||||||
|
task?: { taskType?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunResult {
|
||||||
|
action?: string
|
||||||
|
risk?: string
|
||||||
|
skill?: string
|
||||||
|
toolCalls?: { tool: string; server?: string | null; ok: boolean }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJson<T>(value: string | null | undefined): T | null {
|
||||||
|
if (!value) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as T
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ReviewsPage() {
|
export function ReviewsPage() {
|
||||||
const organizationId = useAuth((s) => s.organizationId)
|
const organizationId = useAuth((s) => s.organizationId)
|
||||||
const [items, setItems] = useState<ReviewItem[] | null>(null)
|
const [items, setItems] = useState<ReviewItem[] | null>(null)
|
||||||
@@ -54,7 +91,8 @@ export function ReviewsPage() {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Review inbox</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">Review inbox</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Held agent actions awaiting your decision. Edit before approving — your edits feed the metric.
|
Held agent actions awaiting your decision. See the action, the result, and the run log before
|
||||||
|
you approve. Your edits feed the metric.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,19 +128,32 @@ export function ReviewsPage() {
|
|||||||
function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: string) => void }) {
|
function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: string) => void }) {
|
||||||
const [content, setContent] = useState(item.content)
|
const [content, setContent] = useState(item.content)
|
||||||
const [childrenText, setChildrenText] = useState(item.childTitles.join('\n'))
|
const [childrenText, setChildrenText] = useState(item.childTitles.join('\n'))
|
||||||
const [showTrace, setShowTrace] = useState(false)
|
const [showLog, setShowLog] = useState(false)
|
||||||
|
const [run, setRun] = useState<RunDetail | null>(null)
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
|
|
||||||
const destructive = item.risk.toLowerCase() === 'destructive'
|
const destructive = item.risk.toLowerCase() === 'destructive'
|
||||||
|
const childCount = childrenText.split('\n').map((l) => l.trim()).filter(Boolean).length
|
||||||
|
|
||||||
|
// Lazily pull the full run (latency, tool calls, raw output, assembled prompt) the first time the
|
||||||
|
// approver opens the log.
|
||||||
|
const toggleLog = async () => {
|
||||||
|
const next = !showLog
|
||||||
|
setShowLog(next)
|
||||||
|
if (next && !run) {
|
||||||
|
try {
|
||||||
|
setRun(await api.get<RunDetail>(`/api/assembler/runs/${item.agentRunId}`))
|
||||||
|
} catch {
|
||||||
|
// The run row may be gone; the assembly trace on the item still renders below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function decide(action: 'approve' | 'sendback') {
|
async function decide(action: 'approve' | 'sendback') {
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
if (action === 'approve') {
|
if (action === 'approve') {
|
||||||
const childTitles = childrenText
|
const childTitles = childrenText.split('\n').map((line) => line.trim()).filter(Boolean)
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
const result = await api.post<{ editDistance: number | null; decision: string }>(
|
const result = await api.post<{ editDistance: number | null; decision: string }>(
|
||||||
`/api/governance/reviews/${item.id}/approve`,
|
`/api/governance/reviews/${item.id}/approve`,
|
||||||
{ content, childTitles },
|
{ content, childTitles },
|
||||||
@@ -138,23 +189,28 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str
|
|||||||
{destructive && <ShieldAlert />}
|
{destructive && <ShieldAlert />}
|
||||||
{item.risk}
|
{item.risk}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">{new Date(item.createdAtUtc).toLocaleString()}</span>
|
||||||
{new Date(item.createdAtUtc).toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{/* Action — what approving will actually do. */}
|
||||||
|
<div
|
||||||
|
className={`rounded-lg border px-3 py-2 text-sm ${
|
||||||
|
destructive ? 'border-destructive/40 bg-destructive/10 text-destructive' : 'border-primary/30 bg-primary/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="font-medium capitalize">{item.actionKind.replace(/-/g, ' ')}</span> ·{' '}
|
||||||
|
{destructive
|
||||||
|
? 'Destructive — always held for a human.'
|
||||||
|
: `On approve, this artifact is written to the board${childCount ? ` and ${childCount} child task${childCount === 1 ? '' : 's'} created` : ''}.`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Result — the proposed artifact + child tasks (both editable). */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor={`content-${item.id}`}>Proposed artifact</Label>
|
<Label htmlFor={`content-${item.id}`}>Result · proposed artifact</Label>
|
||||||
<MarkdownEditor
|
<MarkdownEditor id={`content-${item.id}`} value={content} onChange={setContent} rows={6} mono />
|
||||||
id={`content-${item.id}`}
|
|
||||||
value={content}
|
|
||||||
onChange={setContent}
|
|
||||||
rows={6}
|
|
||||||
mono
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{content !== item.content && (
|
{content !== item.content && (
|
||||||
@@ -165,13 +221,9 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str
|
|||||||
segment.kind === 'same' ? (
|
segment.kind === 'same' ? (
|
||||||
<span key={i}>{segment.text}</span>
|
<span key={i}>{segment.text}</span>
|
||||||
) : segment.kind === 'removed' ? (
|
) : segment.kind === 'removed' ? (
|
||||||
<del key={i} className="rounded bg-destructive/15 text-destructive">
|
<del key={i} className="rounded bg-destructive/15 text-destructive">{segment.text}</del>
|
||||||
{segment.text}
|
|
||||||
</del>
|
|
||||||
) : (
|
) : (
|
||||||
<ins key={i} className="rounded bg-seat-ai/15 font-medium text-primary no-underline">
|
<ins key={i} className="rounded bg-seat-ai/15 font-medium text-primary no-underline">{segment.text}</ins>
|
||||||
{segment.text}
|
|
||||||
</ins>
|
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -189,36 +241,104 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Run log — how the agent got here. */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowTrace((v) => !v)}
|
onClick={toggleLog}
|
||||||
className="flex items-center gap-1 self-start text-xs font-medium text-primary hover:underline"
|
className="flex items-center gap-1.5 self-start text-xs font-medium text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{showTrace ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />}
|
{showLog ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />}
|
||||||
Reasoning trace
|
<ScrollText className="size-3.5" /> Run log
|
||||||
</button>
|
</button>
|
||||||
{showTrace && (
|
{showLog && <RunLog item={item} run={run} />}
|
||||||
<pre className="max-h-48 overflow-auto rounded-lg bg-muted p-3 text-xs">{formatTrace(item.trace)}</pre>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Button variant="outline" disabled={busy} onClick={() => decide('sendback')}>
|
<Button variant="outline" disabled={busy} onClick={() => decide('sendback')}>Send back</Button>
|
||||||
Send back
|
<Button disabled={busy} onClick={() => decide('approve')}>{busy ? 'Working…' : 'Approve'}</Button>
|
||||||
</Button>
|
|
||||||
<Button disabled={busy} onClick={() => decide('approve')}>
|
|
||||||
{busy ? 'Working…' : 'Approve'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTrace(trace: string | null): string {
|
function RunLog({ item, run }: { item: ReviewItem; run: RunDetail | null }) {
|
||||||
if (!trace) return 'No trace captured.'
|
// The assembly trace is on the review item; the run adds latency, tool-call outcomes, and raw output.
|
||||||
try {
|
const trace = parseJson<RunTrace>(run?.trace ?? item.trace)
|
||||||
return JSON.stringify(JSON.parse(trace), null, 2)
|
const result = parseJson<RunResult>(run?.resultJson)
|
||||||
} catch {
|
const [showRaw, setShowRaw] = useState(false)
|
||||||
return trace
|
const [showPrompt, setShowPrompt] = useState(false)
|
||||||
|
const toolCalls = result?.toolCalls ?? []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 rounded-lg border bg-muted/30 p-3 text-xs">
|
||||||
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-muted-foreground">
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<Cpu className="size-3.5" /> {trace?.agent ?? 'agent'} · {trace?.autonomy ?? '—'}
|
||||||
|
</span>
|
||||||
|
{run?.latencyMs != null && (
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<Clock className="size-3.5" /> {(run.latencyMs / 1000).toFixed(1)}s
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{trace?.product?.identity && <span>· product identity included</span>}
|
||||||
|
{typeof trace?.memories === 'number' && <span>· {trace.memories} memory hit{trace.memories === 1 ? '' : 's'}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LogRow label="Skills applied" value={trace?.skills?.length ? trace.skills.join(', ') : 'none'} />
|
||||||
|
{trace?.tools?.length ? <LogRow label="Tools available" value={trace.tools.join(', ')} /> : null}
|
||||||
|
{trace?.docs?.length ? <LogRow label="Docs" value={trace.docs.join(', ')} /> : null}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span className="w-28 shrink-0 font-medium text-foreground">Tools called</span>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
{toolCalls.length === 0 ? (
|
||||||
|
<span className="text-muted-foreground">none</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex flex-col gap-0.5">
|
||||||
|
{toolCalls.map((t, i) => (
|
||||||
|
<span key={i} className="inline-flex items-center gap-1">
|
||||||
|
<Wrench className="size-3" /> {t.tool}
|
||||||
|
{t.server ? ` · ${t.server}` : ''}
|
||||||
|
<Badge variant={t.ok ? 'outline' : 'destructive'} className="ml-1 h-4 px-1.5 text-[10px]">
|
||||||
|
{t.ok ? 'ok' : 'failed'}
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{run?.error && <LogRow label="Error" value={run.error} />}
|
||||||
|
|
||||||
|
{run?.output && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<button type="button" onClick={() => setShowRaw((v) => !v)} className="flex items-center gap-1 self-start font-medium text-foreground hover:underline">
|
||||||
|
{showRaw ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />} Raw model output
|
||||||
|
</button>
|
||||||
|
{showRaw && <pre className="max-h-48 overflow-auto rounded bg-background/60 p-2 whitespace-pre-wrap">{run.output}</pre>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{run?.prompt && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<button type="button" onClick={() => setShowPrompt((v) => !v)} className="flex items-center gap-1 self-start font-medium text-foreground hover:underline">
|
||||||
|
{showPrompt ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />} Assembled prompt
|
||||||
|
</button>
|
||||||
|
{showPrompt && <pre className="max-h-56 overflow-auto rounded bg-background/60 p-2 whitespace-pre-wrap">{run.prompt}</pre>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!run && !trace && <span className="text-muted-foreground">Loading run…</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LogRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span className="w-28 shrink-0 font-medium text-foreground">{label}</span>
|
||||||
|
<span className="min-w-0 flex-1 break-words">{value}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ internal sealed record RunResponse(
|
|||||||
string? ActionRisk,
|
string? ActionRisk,
|
||||||
string? Prompt,
|
string? Prompt,
|
||||||
string? Output,
|
string? Output,
|
||||||
string? Error);
|
string? Error,
|
||||||
|
string? Trace,
|
||||||
|
string? ResultJson,
|
||||||
|
long? LatencyMs,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
DateTimeOffset? CompletedAtUtc);
|
||||||
|
|
||||||
internal sealed record AgentActivityResponse(
|
internal sealed record AgentActivityResponse(
|
||||||
Guid AgentId,
|
Guid AgentId,
|
||||||
|
|||||||
@@ -85,5 +85,6 @@ internal static class AssemblerEndpoints
|
|||||||
|
|
||||||
private static RunResponse ToResponse(AgentRun run) => new(
|
private static RunResponse ToResponse(AgentRun run) => new(
|
||||||
run.Id, run.SeatId, run.WorkItemId, run.AgentId, run.Status.ToString(),
|
run.Id, run.SeatId, run.WorkItemId, run.AgentId, run.Status.ToString(),
|
||||||
run.ActionType, run.ActionRisk, run.Prompt, run.Output, run.Error);
|
run.ActionType, run.ActionRisk, run.Prompt, run.Output, run.Error,
|
||||||
|
run.Trace, run.ResultJson, run.LatencyMs, run.CreatedAtUtc, run.CompletedAtUtc);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user