diff --git a/apps/web/src/app/admin/gastown/towns/[townId]/beads/[beadId]/BeadInspectorDashboard.tsx b/apps/web/src/app/admin/gastown/towns/[townId]/beads/[beadId]/BeadInspectorDashboard.tsx index 36a0a5f83d..5806e29d96 100644 --- a/apps/web/src/app/admin/gastown/towns/[townId]/beads/[beadId]/BeadInspectorDashboard.tsx +++ b/apps/web/src/app/admin/gastown/towns/[townId]/beads/[beadId]/BeadInspectorDashboard.tsx @@ -19,7 +19,7 @@ import { } from '@/components/ui/alert-dialog'; import Link from 'next/link'; import { formatDistanceToNow, format } from 'date-fns'; -import { ArrowLeft } from 'lucide-react'; +import { ArrowLeft, Eye } from 'lucide-react'; const STATUS_COLORS: Record = { open: 'bg-blue-500/10 text-blue-400 border-blue-500/20', @@ -28,6 +28,17 @@ const STATUS_COLORS: Record = { failed: 'bg-red-500/10 text-red-400 border-red-500/20', }; +const EVENT_LABELS: Record = { + babysit_started: 'Babysit Started', + pr_feedback_detected: 'PR Feedback', + pr_conflict_detected: 'PR Conflict', + pr_auto_merge: 'Auto-merge', + pr_status_changed: 'PR Status', + pr_created: 'PR Created', + pr_creation_failed: 'PR Creation Failed', + rework_requested: 'Rework Requested', +}; + type ConfirmActionType = 'close' | 'fail' | 'reset_agent'; type ConfirmAction = { @@ -247,13 +258,37 @@ export function BeadInspectorDashboard({ townId, beadId }: { townId: string; bea
{bead.labels.map(label => ( - + + {label === 'gt:babysit' && } {label} ))}
)} + {bead.labels.includes('gt:babysit') && + typeof bead.metadata?.head_sha === 'string' && ( +
+
+ Head SHA +
+
{bead.metadata.head_sha.slice(0, 12)}…
+
+ )} + {bead.labels.includes('gt:babysit') && ( +
+
+ Force-push +
+
+ {bead.metadata?.force_push_allowed === true ? 'Allowed' : 'Blocked'} +
+
+ )} {bead.title && (
@@ -353,7 +388,7 @@ export function BeadInspectorDashboard({ townId, beadId }: { townId: string; bea
- {event.event_type} + {EVENT_LABELS[event.event_type] ?? event.event_type} {event.agent_id && ( - {event.event_type} + {EVENT_LABELS[event.event_type] ?? event.event_type} = { created: PlayCircle, @@ -31,6 +37,11 @@ const EVENT_ICONS: Record = { mail_sent: Mail, agent_status: MessageSquare, triage_resolved: ShieldCheck, + babysit_started: Eye, + pr_feedback_detected: MessageCircle, + pr_conflict_detected: AlertTriangle, + pr_auto_merge: GitMergeIcon, + pr_status_changed: RefreshCw, }; const EVENT_COLORS: Record = { @@ -45,70 +56,16 @@ const EVENT_COLORS: Record = { mail_sent: 'text-sky-500', agent_status: 'text-white/50', triage_resolved: 'text-amber-500', + babysit_started: 'text-sky-500', + pr_feedback_detected: 'text-amber-500', + pr_conflict_detected: 'text-red-500', + pr_auto_merge: 'text-emerald-500', + pr_status_changed: 'text-purple-500', }; type TownEvent = GastownOutputs['gastown']['getTownEvents'][number]; type BeadEvent = GastownOutputs['gastown']['getBeadEvents'][number]; -function eventDescription(event: { - event_type: string; - old_value: string | null; - new_value: string | null; - metadata: Record; - rig_name?: string; -}): string { - const rigPrefix = event.rig_name ? `[${event.rig_name}] ` : ''; - switch (event.event_type) { - case 'created': { - const title = event.metadata?.title; - return `${rigPrefix}Bead created: ${typeof title === 'string' ? title : (event.new_value ?? 'unknown')}`; - } - case 'hooked': - return `${rigPrefix}Agent hooked to bead`; - case 'unhooked': - return `${rigPrefix}Agent unhooked from bead`; - case 'status_changed': { - const desc = `${rigPrefix}Status: ${event.old_value ?? '?'} → ${event.new_value ?? '?'}`; - if (event.new_value === 'failed') { - const fr = event.metadata?.failure_reason; - if (typeof fr === 'object' && fr !== null && 'message' in fr) { - const msg = (fr as Record).message; - if (typeof msg === 'string') return `${desc} — ${msg}`; - } - } - return desc; - } - case 'closed': - return `${rigPrefix}Bead closed`; - case 'escalated': - return `${rigPrefix}Escalation created`; - case 'review_submitted': - return `${rigPrefix}Submitted for review: ${event.new_value ?? ''}`; - case 'review_completed': - return `${rigPrefix}Review ${event.new_value ?? 'completed'}`; - case 'mail_sent': - return `${rigPrefix}Mail sent`; - case 'triage_resolved': { - const action = event.new_value ?? (event.metadata?.action as string | undefined) ?? 'unknown'; - const notes = event.metadata?.resolution_notes as string | undefined; - const desc = `${rigPrefix}Triage: ${action}`; - return notes ? `${desc} — ${notes}` : desc; - } - case 'agent_status': { - const msg = event.new_value ?? (event.metadata?.message as string | undefined); - const agentName = event.metadata?.agent_name as string | undefined; - const rigName = event.metadata?.rig_name as string | undefined; - const body = msg ?? 'Agent status update'; - // Prefer metadata rig_name over the top-level rig_name (which is - // never populated for bead_events rows). - const prefix = rigName ? `[${rigName}] ` : rigPrefix; - return agentName ? `${prefix}${agentName}: ${body}` : `${prefix}${body}`; - } - default: - return `${rigPrefix}${event.event_type}`; - } -} - function toEventDescriptionInput(event: TownEvent | BeadEvent) { return { event_type: event.event_type, diff --git a/apps/web/src/components/gastown/babysit-ui.test.ts b/apps/web/src/components/gastown/babysit-ui.test.ts new file mode 100644 index 0000000000..80c10c0cf1 --- /dev/null +++ b/apps/web/src/components/gastown/babysit-ui.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from '@jest/globals'; +import { eventDescription } from '@/components/gastown/event-description'; + +describe('eventDescription — babysit event types', () => { + it('formats babysit_started with branch and pr_url', () => { + const desc = eventDescription({ + event_type: 'babysit_started', + old_value: null, + new_value: 'https://github.com/Org/repo/pull/42', + metadata: { + branch: 'feature/x', + pr_url: 'https://github.com/Org/repo/pull/42', + }, + }); + expect(desc).toContain('Babysit started'); + expect(desc).toContain('feature/x'); + expect(desc).toContain('https://github.com/Org/repo/pull/42'); + }); + + it('formats babysit_started without branch or pr_url', () => { + const desc = eventDescription({ + event_type: 'babysit_started', + old_value: null, + new_value: null, + metadata: {}, + }); + expect(desc).toContain('Babysit started'); + }); + + it('formats pr_feedback_detected', () => { + const desc = eventDescription({ + event_type: 'pr_feedback_detected', + old_value: null, + new_value: 'CI failure', + metadata: {}, + }); + expect(desc).toContain('PR feedback detected'); + expect(desc).toContain('CI failure'); + }); + + it('formats pr_conflict_detected', () => { + const desc = eventDescription({ + event_type: 'pr_conflict_detected', + old_value: null, + new_value: null, + metadata: {}, + }); + expect(desc).toContain('PR merge conflict detected'); + }); + + it('formats pr_auto_merge', () => { + const desc = eventDescription({ + event_type: 'pr_auto_merge', + old_value: null, + new_value: null, + metadata: {}, + }); + expect(desc).toContain('PR auto-merge triggered'); + }); + + it('formats pr_status_changed with state from metadata', () => { + const desc = eventDescription({ + event_type: 'pr_status_changed', + old_value: null, + new_value: null, + metadata: { state: 'merged' }, + }); + expect(desc).toContain('PR status changed'); + expect(desc).toContain('merged'); + }); + + it('formats pr_status_changed falling back to new_value', () => { + const desc = eventDescription({ + event_type: 'pr_status_changed', + old_value: null, + new_value: 'closed', + metadata: {}, + }); + expect(desc).toContain('PR status changed'); + expect(desc).toContain('closed'); + }); + + it('includes rig prefix when rig_name is present', () => { + const desc = eventDescription({ + event_type: 'babysit_started', + old_value: null, + new_value: null, + metadata: {}, + rig_name: 'my-rig', + }); + expect(desc).toContain('[my-rig]'); + expect(desc).toContain('Babysit started'); + }); +}); diff --git a/apps/web/src/components/gastown/drawer-panels/BeadPanel.tsx b/apps/web/src/components/gastown/drawer-panels/BeadPanel.tsx index 65446c0177..7be8b3c41b 100644 --- a/apps/web/src/components/gastown/drawer-panels/BeadPanel.tsx +++ b/apps/web/src/components/gastown/drawer-panels/BeadPanel.tsx @@ -37,6 +37,9 @@ import { Loader2, Play, MessageCircle, + Lock, + Unlock, + Eye, } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -249,6 +252,7 @@ export function BeadPanel({ // Held bead detection const isHeld = bead.labels.includes('gt:held'); + const isBabysit = bead.labels.includes('gt:babysit'); // Mayor responses: message-type child beads const mayorResponses = allBeads.filter( @@ -335,6 +339,12 @@ export function BeadPanel({ {bead.priority} + {isBabysit && ( + + + Babysat external PR + + )} {isHeld && (