Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
Expand All @@ -28,6 +28,17 @@ const STATUS_COLORS: Record<string, string> = {
failed: 'bg-red-500/10 text-red-400 border-red-500/20',
};

const EVENT_LABELS: Record<string, string> = {
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 = {
Expand Down Expand Up @@ -247,13 +258,37 @@ export function BeadInspectorDashboard({ townId, beadId }: { townId: string; bea
</dt>
<dd className="mt-1 flex flex-wrap gap-1">
{bead.labels.map(label => (
<Badge key={label} variant="outline" className="font-mono text-xs">
<Badge
key={label}
variant="outline"
className={`font-mono text-xs ${label === 'gt:babysit' ? 'border-sky-500/30 bg-sky-500/10 text-sky-400' : ''}`}
>
{label === 'gt:babysit' && <Eye className="mr-1 size-3" />}
{label}
</Badge>
))}
</dd>
</div>
)}
{bead.labels.includes('gt:babysit') &&
typeof bead.metadata?.head_sha === 'string' && (
<div>
<dt className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Head SHA
</dt>
<dd className="font-mono text-xs">{bead.metadata.head_sha.slice(0, 12)}…</dd>
</div>
)}
{bead.labels.includes('gt:babysit') && (
<div>
<dt className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Force-push
</dt>
<dd className="text-xs">
{bead.metadata?.force_push_allowed === true ? 'Allowed' : 'Blocked'}
</dd>
</div>
)}
{bead.title && (
<div className="col-span-2 md:col-span-3">
<dt className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Expand Down Expand Up @@ -353,7 +388,7 @@ export function BeadInspectorDashboard({ townId, beadId }: { townId: string; bea
<div className="pb-4">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded bg-gray-500/10 px-1.5 py-0.5 font-mono text-xs">
{event.event_type}
{EVENT_LABELS[event.event_type] ?? event.event_type}
</span>
{event.agent_id && (
<Link
Expand Down Expand Up @@ -431,7 +466,7 @@ export function BeadInspectorDashboard({ townId, beadId }: { townId: string; bea
</td>
<td className="py-2 pr-4">
<span className="rounded bg-gray-500/10 px-1.5 py-0.5 font-mono text-xs">
{event.event_type}
{EVENT_LABELS[event.event_type] ?? event.event_type}
</span>
</td>
<td
Expand Down
75 changes: 16 additions & 59 deletions apps/web/src/components/gastown/ActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ import {
MessageSquare,
ChevronRight,
ShieldCheck,
Eye,
MessageCircle,
GitMergeIcon,
RefreshCw,
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
export { eventDescription } from './event-description';
import { eventDescription } from './event-description';

const EVENT_ICONS: Record<string, typeof Activity> = {
created: PlayCircle,
Expand All @@ -31,6 +37,11 @@ const EVENT_ICONS: Record<string, typeof Activity> = {
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<string, string> = {
Expand All @@ -45,70 +56,16 @@ const EVENT_COLORS: Record<string, string> = {
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<string, unknown>;
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<string, unknown>).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,
Expand Down
94 changes: 94 additions & 0 deletions apps/web/src/components/gastown/babysit-ui.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
21 changes: 21 additions & 0 deletions apps/web/src/components/gastown/drawer-panels/BeadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ import {
Loader2,
Play,
MessageCircle,
Lock,
Unlock,
Eye,
} from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -335,6 +339,12 @@ export function BeadPanel({
<Flag className="size-2.5" />
{bead.priority}
</span>
{isBabysit && (
<span className="inline-flex items-center gap-1 rounded-md border border-sky-500/30 bg-sky-500/10 px-2 py-0.5 text-[10px] font-medium text-sky-300">
<Eye className="size-2.5" />
Babysat external PR
</span>
)}
{isHeld && (
<Button
size="sm"
Expand Down Expand Up @@ -464,6 +474,17 @@ export function BeadPanel({
value={bead.labels.length ? bead.labels.join(', ') : 'None'}
/>

{isBabysit && typeof bead.metadata?.head_sha === 'string' && (
<MetaCell icon={Hash} label="Head SHA" value={bead.metadata.head_sha} mono />
)}
{isBabysit && (
<MetaCell
icon={bead.metadata?.force_push_allowed === true ? Unlock : Lock}
label="Force-push"
value={bead.metadata?.force_push_allowed === true ? 'Allowed' : 'Blocked'}
/>
)}

{/* Parent bead — clickable */}
{bead.parent_bead_id && (
<button
Expand Down
Loading