import {
ArrowRight,
Bot,
CheckCircle,
Loader2,
MessageSquare,
Send,
Settings,
Sparkles,
Square,
XCircle,
} from 'lucide-react'
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from '@/components/ui/input-group'
import { cn } from '@/lib/utils'
import { subscribeToEvents } from '@/messaging/events'
import { agentCommands } from '@/messaging/protocol'
import type { AgentActivity, AgentState, AgentStatus, HistoricalEvent } from '@/messaging/protocol'
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/utils/constants'
// Configuration panel component
function ConfigPanel({ onClose }: { onClose: () => void }) {
const [apiKey, setApiKey] = useState(DEMO_API_KEY)
const [baseURL, setBaseURL] = useState(DEMO_BASE_URL)
const [model, setModel] = useState(DEMO_MODEL)
const [saving, setSaving] = useState(false)
useEffect(() => {
chrome.storage.local.get('llmConfig').then((result) => {
const config = result.llmConfig as
| { apiKey?: string; baseURL?: string; model?: string }
| undefined
if (config) {
setApiKey(config.apiKey || DEMO_API_KEY)
setBaseURL(config.baseURL || DEMO_BASE_URL)
setModel(config.model || DEMO_MODEL)
}
})
}, [])
const handleSave = async () => {
setSaving(true)
try {
await agentCommands.sendMessage('agent:configure', { apiKey, baseURL, model })
onClose()
} finally {
setSaving(false)
}
}
return (
)
}
// Result card for done action
function ResultCard({ success, text }: { success: boolean; text: string }) {
return (
{success ? (
) : (
)}
Result: {success ? 'Success' : 'Failed'}
{text}
)
}
// Reflection section in step card
function ReflectionSection({
reflection,
}: {
reflection: {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
}
}) {
const items = [
{ icon: '✅', label: 'eval', value: reflection.evaluation_previous_goal },
{ icon: '💾', label: 'memory', value: reflection.memory },
{ icon: '🎯', label: 'goal', value: reflection.next_goal },
].filter((item) => item.value)
if (items.length === 0) return null
return (
Reflection
{items.map((item) => (
{item.icon}
{item.value}
))}
)
}
// History event card component
function EventCard({ event }: { event: HistoricalEvent }) {
// Done action - show as result card
if (event.type === 'step' && event.action?.name === 'done') {
const input = event.action.input as { text?: string; success?: boolean }
return (
)
}
if (event.type === 'step') {
return (
{/* Reflection */}
{event.reflection &&
}
{/* Action */}
{event.action && (
{event.action.name}
{JSON.stringify(event.action.input)}
→ {event.action.output}
)}
)
}
if (event.type === 'observation') {
return (
{event.content}
)
}
if (event.type === 'error') {
return (
{event.message}
)
}
return null
}
// Activity card with animation
function ActivityCard({ activity }: { activity: AgentActivity }) {
const getActivityInfo = () => {
switch (activity.type) {
case 'thinking':
return { text: 'Thinking...', color: 'text-blue-500' }
case 'executing':
return { text: `Executing ${activity.tool}...`, color: 'text-amber-500' }
case 'executed':
return { text: `Done: ${activity.tool}`, color: 'text-green-500' }
case 'retrying':
return {
text: `Retrying (${activity.attempt}/${activity.maxAttempts})...`,
color: 'text-amber-500',
}
case 'error':
return { text: activity.message, color: 'text-destructive' }
}
}
const info = getActivityInfo()
return (
)
}
// Status dot indicator
function StatusDot({ status }: { status: AgentStatus }) {
const colorClass = {
idle: 'bg-muted-foreground',
running: 'bg-blue-500',
completed: 'bg-green-500',
error: 'bg-destructive',
}[status]
const label = {
idle: 'Ready',
running: 'Running',
completed: 'Done',
error: 'Error',
}[status]
return (
{label}
)
}
// Logo component (Bot icon as placeholder until real logo is added)
function Logo({ className }: { className?: string }) {
return
}
// Empty state with logo
function EmptyState() {
return (
Page Agent Ext
Enter a task to automate this page
)
}
export default function App() {
const [showConfig, setShowConfig] = useState(false)
const [task, setTask] = useState('')
const [status, setStatus] = useState('idle')
const [history, setHistory] = useState([])
const [activity, setActivity] = useState(null)
const [currentTask, setCurrentTask] = useState('')
const historyRef = useRef(null)
const textareaRef = useRef(null)
// Subscribe to agent events
useEffect(() => {
// Initialize with demo config if not set
chrome.storage.local.get('llmConfig').then((result) => {
if (!result.llmConfig) {
chrome.storage.local.set({
llmConfig: { apiKey: DEMO_API_KEY, baseURL: DEMO_BASE_URL, model: DEMO_MODEL },
})
}
})
const unsubscribe = subscribeToEvents({
onStatus: (newStatus) => {
setStatus(newStatus)
if (newStatus === 'idle' || newStatus === 'completed' || newStatus === 'error') {
setActivity(null)
}
},
onHistory: (newHistory) => {
setHistory(newHistory)
},
onActivity: (newActivity) => {
setActivity(newActivity)
},
onStateSnapshot: (state) => {
setStatus(state.status)
setHistory(state.history)
setCurrentTask(state.task)
},
})
// Get initial state
agentCommands.sendMessage('agent:getState', undefined).then((state: AgentState) => {
setStatus(state.status)
setHistory(state.history)
setCurrentTask(state.task)
})
return unsubscribe
}, [])
// Auto-scroll to bottom on new events
useEffect(() => {
if (historyRef.current) {
historyRef.current.scrollTop = historyRef.current.scrollHeight
}
}, [history, activity])
const handleSubmit = useCallback(
async (e?: React.FormEvent) => {
e?.preventDefault()
if (!task.trim() || status === 'running') return
setCurrentTask(task)
setHistory([])
await agentCommands.sendMessage('agent:execute', task)
setTask('')
},
[task, status]
)
const handleStop = useCallback(async () => {
await agentCommands.sendMessage('agent:stop', undefined)
}, [])
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}
if (showConfig) {
return setShowConfig(false)} />
}
const isRunning = status === 'running'
const showEmptyState = !currentTask && history.length === 0 && !isRunning
return (
{/* Header */}
Page Agent Ext
setShowConfig(true)}>
{/* Content */}
{/* Current task */}
{currentTask && (
)}
{/* History */}
{showEmptyState &&
}
{history.map((event, index) => (
))}
{/* Activity indicator at bottom */}
{activity &&
}
{/* Input */}
setTask(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isRunning}
rows={2}
className="text-xs pr-12 min-h-[60px]"
/>
{isRunning ? (
) : (
handleSubmit()}
disabled={!task.trim()}
className="size-7"
>
)}
)
}