Skip to content
Open
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
4 changes: 4 additions & 0 deletions apps/mobile/src/components/agents/platform-filter-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { cn } from '@/lib/utils';
const PLATFORM_FILTERS = [
'cloud-agent',
'extension',
'gastown',
'cli',
'slack',
'github',
Expand Down Expand Up @@ -80,6 +81,9 @@ function platformFilterLabel(p: string): string {
case 'other': {
return 'Other';
}
case 'gastown': {
return 'Gastown';
}
default: {
return p;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import { BeadBoard } from '@/components/gastown/BeadBoard';
import { AgentCard } from '@/components/gastown/AgentCard';
import { ConvoyTimeline } from '@/components/gastown/ConvoyTimeline';
import { CreateBeadDrawer } from '@/components/gastown/CreateBeadDrawer';
import { SlingDialog } from '@/components/gastown/SlingDialog';
import { useDrawerStack } from '@/components/gastown/DrawerStack';
import {
Plus,
Zap,
GitBranch,
Hexagon,
Bot,
Expand Down Expand Up @@ -42,6 +44,7 @@ export function RigDetailPageClient({
const trpc = useGastownTRPC();
const confirm = useConfirm();
const [isCreateBeadOpen, setIsCreateBeadOpen] = useState(false);
const [isSlingOpen, setIsSlingOpen] = useState(false);
const [convoysCollapsed, setConvoysCollapsed] = useState(false);
const { open: openDrawer } = useDrawerStack();

Expand Down Expand Up @@ -165,6 +168,15 @@ export function RigDetailPageClient({
>
<Settings className="size-4" />
</Link>
<Button
variant="secondary"
size="sm"
onClick={() => setIsSlingOpen(true)}
className="gap-1.5"
>
<Zap className="size-3.5" />
Sling
</Button>
<Button
variant="primary"
size="sm"
Expand Down Expand Up @@ -322,6 +334,7 @@ export function RigDetailPageClient({
isOpen={isCreateBeadOpen}
onClose={() => setIsCreateBeadOpen(false)}
/>
<SlingDialog rigId={rigId} isOpen={isSlingOpen} onClose={() => setIsSlingOpen(false)} />
</div>
);
}
Expand Down
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, Lock, Unlock } from 'lucide-react';

const STATUS_COLORS: Record<string, string> = {
open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
Expand Down Expand Up @@ -167,6 +167,12 @@ export function BeadInspectorDashboard({ townId, beadId }: { townId: string; bea
{bead.status}
</Badge>
)}
{bead?.labels.includes('gt:babysit') && (
<Badge variant="outline" className="border-cyan-500/30 bg-cyan-500/10 text-cyan-400">
<Eye className="mr-1 h-3 w-3" />
Babysat external PR
</Badge>
)}
</div>

{beadQuery.isLoading && (
Expand Down Expand Up @@ -262,6 +268,54 @@ export function BeadInspectorDashboard({ townId, beadId }: { townId: string; bea
<dd>{bead.title}</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, 7)}</dd>
</div>
)}
<div>
<dt className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Force-push
</dt>
<dd className="flex items-center gap-1 text-xs">
{bead.metadata.force_push_allowed === true ? (
<>
<Unlock className="h-3 w-3 text-green-400" />
<span className="text-green-400">Allowed</span>
</>
) : (
<>
<Lock className="h-3 w-3 text-red-400" />
<span className="text-red-400">Blocked</span>
</>
)}
</dd>
</div>
{typeof bead.metadata.babysit_started_at === 'string' && (
<div>
<dt className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Babysit started
</dt>
<dd
title={format(
new Date(bead.metadata.babysit_started_at as string),
'PPpp'
)}
>
{formatDistanceToNow(
new Date(bead.metadata.babysit_started_at as string),
{ addSuffix: true }
)}
</dd>
</div>
)}
</>
)}
</dl>
</CardContent>
</Card>
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/cloud-agent-next/ChatSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ function SessionRow({
const PLATFORM_FILTERS = [
'cloud-agent',
'extension',
'gastown',
'cli',
'slack',
'github',
Expand All @@ -314,6 +315,8 @@ function platformFilterLabel(p: string): string {
return 'Linear';
case 'other':
return 'Other';
case 'gastown':
return 'Gastown';
default:
return p;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
if (platformFilter.length === 0) return undefined;
return platformFilter.flatMap(p => {
switch (p) {
// 'cloud-agent-web' is a variant of the cloud agent
case 'cloud-agent':
return ['cloud-agent', 'cloud-agent-web'];
// Extension sessions are created from VS Code or agent-manager
case 'extension':
return ['vscode', 'agent-manager'];
default:
// Handles platforms like 'gastown' by passing through unchanged
return [p];
}
});
Expand Down
95 changes: 95 additions & 0 deletions apps/web/src/components/gastown/ActivityFeed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, it, expect } from '@jest/globals';
import { eventDescription } from './eventDescription';

describe('eventDescription: babysit event types', () => {
const base = {
old_value: null,
new_value: null,
metadata: {},
rig_name: undefined,
};

it('renders babysit_started with default description', () => {
expect(eventDescription({ ...base, event_type: 'babysit_started' })).toBe(
'Babysit started — town is now monitoring this PR'
);
});

it('renders babysit_started with rig prefix', () => {
expect(eventDescription({ ...base, event_type: 'babysit_started', rig_name: 'my-rig' })).toBe(
'[my-rig] Babysit started — town is now monitoring this PR'
);
});

it('renders pr_feedback_detected without source', () => {
expect(eventDescription({ ...base, event_type: 'pr_feedback_detected' })).toBe(
'PR feedback detected'
);
});

it('renders pr_feedback_detected with source from metadata', () => {
expect(
eventDescription({
...base,
event_type: 'pr_feedback_detected',
metadata: { source: 'ci-failure' },
})
).toBe('PR feedback detected (ci-failure)');
});

it('renders pr_conflict_detected', () => {
expect(eventDescription({ ...base, event_type: 'pr_conflict_detected' })).toBe(
'PR merge conflict detected'
);
});

it('renders pr_auto_merge', () => {
expect(eventDescription({ ...base, event_type: 'pr_auto_merge' })).toBe(
'PR auto-merge triggered'
);
});

it('renders pr_status_changed with new_value', () => {
expect(
eventDescription({ ...base, event_type: 'pr_status_changed', new_value: 'merged' })
).toBe('PR status changed: merged');
});

it('renders pr_status_changed without new_value', () => {
expect(eventDescription({ ...base, event_type: 'pr_status_changed' })).toBe(
'PR status changed: unknown'
);
});
});

describe('eventDescription: existing event types still work', () => {
const base = {
old_value: null,
new_value: null,
metadata: {},
rig_name: undefined,
};

it('renders created event', () => {
expect(
eventDescription({ ...base, event_type: 'created', metadata: { title: 'My Bead' } })
).toBe('Bead created: My Bead');
});

it('renders status_changed event', () => {
expect(
eventDescription({
...base,
event_type: 'status_changed',
old_value: 'open',
new_value: 'in_progress',
})
).toBe('Status: open → in_progress');
});

it('falls through to raw event_type for unknown events', () => {
expect(eventDescription({ ...base, event_type: 'some_future_event' })).toBe(
'some_future_event'
);
});
});
74 changes: 15 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,13 @@ import {
MessageSquare,
ChevronRight,
ShieldCheck,
Eye,
GitPullRequest as GitPullRequestIcon,
AlertCircle,
Zap,
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { eventDescription } from './eventDescription';

const EVENT_ICONS: Record<string, typeof Activity> = {
created: PlayCircle,
Expand All @@ -31,6 +36,11 @@ const EVENT_ICONS: Record<string, typeof Activity> = {
mail_sent: Mail,
agent_status: MessageSquare,
triage_resolved: ShieldCheck,
babysit_started: Eye,
pr_feedback_detected: AlertCircle,
pr_conflict_detected: AlertTriangle,
pr_auto_merge: Zap,
pr_status_changed: GitPullRequestIcon,
};

const EVENT_COLORS: Record<string, string> = {
Expand All @@ -45,70 +55,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-cyan-500',
pr_feedback_detected: 'text-orange-500',
pr_conflict_detected: 'text-red-400',
pr_auto_merge: 'text-emerald-500',
pr_status_changed: 'text-purple-400',
};

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
Loading
Loading