Skip to content
Merged
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
8 changes: 6 additions & 2 deletions apps/api/src/browserbase/browserbase.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const BROWSER_WIDTH = 1440;
const BROWSER_HEIGHT = 900;

/** Stagehand v3 requires 'provider/model' format. */
const STAGEHAND_MODEL = 'anthropic/claude-sonnet-4-6';
const STAGEHAND_CUA_MODEL = 'anthropic/claude-sonnet-4-6';

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const PENDING_CONTEXT_ID = '__PENDING__';
Expand Down Expand Up @@ -205,7 +209,7 @@ export class BrowserbaseService {
projectId: this.getProjectId(),
browserbaseSessionID: sessionId,
model: {
modelName: 'claude-3-7-sonnet-latest',
modelName: STAGEHAND_MODEL,
apiKey: process.env.ANTHROPIC_API_KEY,
},
verbose: 1,
Expand Down Expand Up @@ -784,7 +788,7 @@ export class BrowserbaseService {
.agent({
cua: true,
model: {
modelName: 'anthropic/claude-3-7-sonnet-latest',
modelName: STAGEHAND_CUA_MODEL,
apiKey: process.env.ANTHROPIC_API_KEY,
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export function BrowserAutomations({ taskId, isManualTask = false }: BrowserAuto
onRun={execution.runAutomation}
onCreateClick={isManualTask ? undefined : () => setDialogState({ open: true, mode: 'create' })}
onEditClick={(automation) => setDialogState({ open: true, mode: 'edit', automation })}
onDelete={automations.deleteAutomation}
onToggleEnabled={automations.toggleAutomation}
/>
<BrowserAutomationConfigDialog
isOpen={dialogState.open}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@

import { cn } from '@/lib/utils';
import { Button } from '@trycompai/ui/button';
import { ChevronDown, Loader2, MonitorPlay, Settings } from 'lucide-react';
import {
ChevronDown,
Loader2,
MonitorPlay,
Pencil,
Power,
PowerOff,
Trash2,
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { useState } from 'react';
import type { BrowserAutomation, BrowserAutomationRun } from '../../hooks/types';
import { RunHistory } from './RunHistory';

Expand All @@ -15,6 +24,8 @@ interface AutomationItemProps {
onToggleExpand: () => void;
onRun: () => void;
onEdit: () => void;
onDelete: () => void;
onToggleEnabled: (enabled: boolean) => void;
}

export function AutomationItem({
Expand All @@ -25,23 +36,30 @@ export function AutomationItem({
onToggleExpand,
onRun,
onEdit,
onDelete,
onToggleEnabled,
}: AutomationItemProps) {
const [confirmDelete, setConfirmDelete] = useState(false);
const runs: BrowserAutomationRun[] = automation.runs || [];
const latestRun = runs[0];

// status dot
const hasFailed = latestRun?.status === 'failed';
const isCompleted = latestRun?.status === 'completed';
const dotColor = hasFailed
? 'bg-destructive shadow-[0_0_8px_rgba(255,0,0,0.3)]'
: isCompleted
? 'bg-primary shadow-[0_0_8px_rgba(0,77,64,0.4)]'
: 'bg-muted-foreground';
const isDisabled = !automation.isEnabled;
const dotColor = isDisabled
? 'bg-muted-foreground/40'
: hasFailed
? 'bg-destructive shadow-[0_0_8px_rgba(255,0,0,0.3)]'
: isCompleted
? 'bg-primary shadow-[0_0_8px_rgba(0,77,64,0.4)]'
: 'bg-muted-foreground';

return (
<div
className={cn(
'rounded-lg border transition-all duration-300',
isDisabled && 'opacity-60',
isExpanded
? 'border-primary/30 shadow-sm bg-primary/2'
: 'border-border/50 hover:border-border hover:shadow-sm',
Expand All @@ -51,9 +69,16 @@ export function AutomationItem({
<div className={cn('h-2.5 w-2.5 rounded-full shrink-0', dotColor)} />

<div className="flex-1 min-w-0">
<p className="font-semibold text-foreground text-sm tracking-tight">
{automation.name}
</p>
<div className="flex items-center gap-2">
<p className="font-semibold text-foreground text-sm tracking-tight">
{automation.name}
</p>
{isDisabled && (
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
Paused
</span>
)}
</div>
{latestRun ? (
<p className="text-xs text-muted-foreground mt-0.5">
Last ran {formatDistanceToNow(new Date(latestRun.createdAt), { addSuffix: true })}
Expand All @@ -63,15 +88,75 @@ export function AutomationItem({
)}
</div>

<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5">
{!readOnly && (
<Button variant="ghost" size="icon" onClick={onEdit} aria-label="Edit automation">
<Settings className="h-4 w-4" />
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => onToggleEnabled(!automation.isEnabled)}
aria-label={automation.isEnabled ? 'Pause automation' : 'Enable automation'}
>
{automation.isEnabled ? (
<Power className="h-3.5 w-3.5 text-primary" />
) : (
<PowerOff className="h-3.5 w-3.5 text-muted-foreground" />
)}
</Button>
)}

{!readOnly && (
<Button variant="outline" size="sm" onClick={onRun} disabled={isRunning}>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={onEdit}
aria-label="Edit automation"
>
<Pencil className="h-3.5 w-3.5" />
</Button>
)}

{!readOnly && (
confirmDelete ? (
<div className="flex items-center gap-1">
<Button
variant="destructive"
size="sm"
className="h-7 text-xs"
onClick={() => { onDelete(); setConfirmDelete(false); }}
>
Confirm
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={() => setConfirmDelete(false)}
>
Cancel
</Button>
</div>
) : (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setConfirmDelete(true)}
aria-label="Delete automation"
>
<Trash2 className="h-3.5 w-3.5 text-muted-foreground hover:text-destructive" />
</Button>
)
)}

{!readOnly && (
<Button
variant="outline"
size="sm"
onClick={onRun}
disabled={isRunning || isDisabled}
>
{isRunning ? (
<>
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const defaultProps = {
onRun: vi.fn(),
onCreateClick: vi.fn(),
onEditClick: vi.fn(),
onDelete: vi.fn(),
onToggleEnabled: vi.fn(),
};

describe('BrowserAutomationsList permission gating', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ interface BrowserAutomationsListProps {
/** When undefined, the create button is hidden (e.g., for manual tasks) */
onCreateClick?: () => void;
onEditClick: (automation: BrowserAutomation) => void;
onDelete: (automationId: string) => void;
onToggleEnabled: (automationId: string, enabled: boolean) => void;
}

export function BrowserAutomationsList({
Expand All @@ -42,13 +44,16 @@ export function BrowserAutomationsList({
onRun,
onCreateClick,
onEditClick,
onDelete,
onToggleEnabled,
}: BrowserAutomationsListProps) {
const [expandedId, setExpandedId] = useState<string | null>(null);
const { hasPermission } = usePermissions();
const canCreateIntegration = hasPermission('integration', 'create');
const canUpdateIntegration = hasPermission('integration', 'update');

const nextRun = automations.length > 0 ? getNextScheduledRun() : null;
const hasEnabledAutomations = automations.some((a) => a.isEnabled);
const nextRun = hasEnabledAutomations ? getNextScheduledRun() : null;

return (
<div className="rounded-lg border border-border bg-card overflow-hidden">
Expand Down Expand Up @@ -102,6 +107,8 @@ export function BrowserAutomationsList({
}
onRun={() => onRun(automation.id)}
onEdit={() => onEditClick(automation)}
onDelete={() => onDelete(automation.id)}
onToggleEnabled={(enabled) => onToggleEnabled(automation.id, enabled)}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,45 @@ export function useBrowserAutomations({ taskId }: UseBrowserAutomationsOptions)
[fetchAutomations],
);

const deleteAutomation = useCallback(
async (automationId: string) => {
try {
const res = await apiClient.delete(`/v1/browserbase/automations/${automationId}`);
if (res.error) throw new Error(res.error);
toast.success('Browser automation deleted');
await fetchAutomations();
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to delete automation');
}
},
[fetchAutomations],
);

const toggleAutomation = useCallback(
async (automationId: string, isEnabled: boolean) => {
try {
const res = await apiClient.patch<BrowserAutomation>(
`/v1/browserbase/automations/${automationId}`,
{ isEnabled },
);
if (res.error) throw new Error(res.error);
toast.success(isEnabled ? 'Automation enabled' : 'Automation disabled');
await fetchAutomations();
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to update automation');
}
},
[fetchAutomations],
);

return {
automations,
isLoading,
isSaving,
fetchAutomations,
createAutomation,
updateAutomation,
deleteAutomation,
toggleAutomation,
};
}
Loading