mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-09 19:45:17 +02:00
better ui tool call
This commit is contained in:
parent
d61c88fb43
commit
ff7bbd9b52
2 changed files with 169 additions and 22 deletions
|
|
@ -98,24 +98,33 @@ export const ToolHeader = ({
|
|||
type,
|
||||
state,
|
||||
...props
|
||||
}: ToolHeaderProps) => (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-4 p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<WrenchIcon className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium text-sm">
|
||||
{title ?? type.split("-").slice(1).join("-")}
|
||||
</span>
|
||||
{getStatusBadge(state)}
|
||||
</div>
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
}: ToolHeaderProps) => {
|
||||
const displayTitle = title ?? type.split("-").slice(1).join("-")
|
||||
|
||||
return (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-4 p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<WrenchIcon className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-left font-medium text-sm"
|
||||
title={displayTitle}
|
||||
>
|
||||
{displayTitle}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
{getStatusBadge(state)}
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
)
|
||||
};
|
||||
|
||||
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||
|
||||
|
|
@ -215,4 +224,3 @@ export const ToolTabbedContent = ({
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -249,6 +249,143 @@ const BROWSER_PENDING_LABELS: Record<string, string> = {
|
|||
wait: 'Waiting for page...',
|
||||
}
|
||||
|
||||
const truncateLabel = (value: string, max = 72): string => {
|
||||
const normalized = value.replace(/\s+/g, ' ').trim()
|
||||
if (normalized.length <= max) return normalized
|
||||
return `${normalized.slice(0, Math.max(0, max - 3)).trim()}...`
|
||||
}
|
||||
|
||||
const safeBrowserString = (value: unknown): string | null => {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null
|
||||
}
|
||||
|
||||
const parseBrowserUrl = (value: string | null): URL | null => {
|
||||
if (!value) return null
|
||||
try {
|
||||
return new URL(value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getGoogleSearchQuery = (value: string | null): string | null => {
|
||||
const parsed = parseBrowserUrl(value)
|
||||
if (!parsed) return null
|
||||
const hostname = parsed.hostname.replace(/^www\./, '')
|
||||
if (hostname !== 'google.com' && !hostname.endsWith('.google.com')) return null
|
||||
if (parsed.pathname !== '/search') return null
|
||||
const query = parsed.searchParams.get('q')?.trim()
|
||||
return query ? truncateLabel(query, 56) : null
|
||||
}
|
||||
|
||||
const formatBrowserTarget = (value: string | null): string | null => {
|
||||
const parsed = parseBrowserUrl(value)
|
||||
if (!parsed) {
|
||||
return value ? truncateLabel(value, 56) : null
|
||||
}
|
||||
|
||||
const hostname = parsed.hostname.replace(/^www\./, '')
|
||||
const path = parsed.pathname === '/' ? '' : parsed.pathname
|
||||
const suffix = parsed.search ? `${path}${parsed.search}` : path
|
||||
return truncateLabel(`${hostname}${suffix}`, 56)
|
||||
}
|
||||
|
||||
const sanitizeBrowserDescription = (value: string | null): string | null => {
|
||||
if (!value) return null
|
||||
|
||||
let text = value
|
||||
.replace(/^(clicked|typed into|pressed)\s+/i, '')
|
||||
.replace(/\.$/, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
if (!text) return null
|
||||
|
||||
const looksLikeCssNoise =
|
||||
/(^|[\s"])(body|html)\b/i.test(text)
|
||||
|| /display:|position:|background-color|align-items|justify-content|z-index|var\(--|left:|top:/i.test(text)
|
||||
|| /\.[A-Za-z0-9_-]+\{/.test(text)
|
||||
|
||||
if (looksLikeCssNoise || text.length > 88) {
|
||||
const quoted = Array.from(text.matchAll(/"([^"]+)"/g))
|
||||
.map((match) => match[1]?.trim())
|
||||
.find((candidate) => candidate && !/display:|position:|background-color|var\(--/i.test(candidate))
|
||||
|
||||
if (!quoted) return null
|
||||
text = `"${truncateLabel(quoted, 44)}"`
|
||||
}
|
||||
|
||||
if (/^(body|html)\b/i.test(text)) return null
|
||||
return truncateLabel(text, 64)
|
||||
}
|
||||
|
||||
const getBrowserSuccessLabel = (
|
||||
action: string,
|
||||
input: Record<string, unknown> | undefined,
|
||||
result: Record<string, unknown> | undefined,
|
||||
): string | null => {
|
||||
const page = result?.page as Record<string, unknown> | undefined
|
||||
const pageUrl = safeBrowserString(page?.url)
|
||||
const resultMessage = safeBrowserString(result?.message)
|
||||
|
||||
switch (action) {
|
||||
case 'open':
|
||||
return 'Opened browser'
|
||||
case 'get-state':
|
||||
return 'Read browser state'
|
||||
case 'new-tab': {
|
||||
const query = getGoogleSearchQuery(pageUrl)
|
||||
if (query) return `Opened search for "${query}"`
|
||||
const target = formatBrowserTarget(pageUrl) || safeBrowserString(input?.target)
|
||||
return target ? `Opened ${target}` : 'Opened new tab'
|
||||
}
|
||||
case 'switch-tab':
|
||||
return 'Switched browser tab'
|
||||
case 'close-tab':
|
||||
return 'Closed browser tab'
|
||||
case 'navigate': {
|
||||
const query = getGoogleSearchQuery(pageUrl)
|
||||
if (query) return `Searched Google for "${query}"`
|
||||
const target = formatBrowserTarget(pageUrl) || formatBrowserTarget(safeBrowserString(input?.target))
|
||||
return target ? `Opened ${target}` : 'Navigated browser'
|
||||
}
|
||||
case 'back':
|
||||
return 'Went back'
|
||||
case 'forward':
|
||||
return 'Went forward'
|
||||
case 'reload':
|
||||
return 'Reloaded page'
|
||||
case 'read-page': {
|
||||
const title = safeBrowserString(page?.title)
|
||||
return title ? `Read ${truncateLabel(title, 52)}` : 'Read page'
|
||||
}
|
||||
case 'click': {
|
||||
const detail = sanitizeBrowserDescription(resultMessage)
|
||||
if (detail) return `Clicked ${detail}`
|
||||
if (typeof input?.index === 'number') return `Clicked element ${input.index}`
|
||||
return 'Clicked page element'
|
||||
}
|
||||
case 'type': {
|
||||
const detail = sanitizeBrowserDescription(resultMessage)
|
||||
if (detail) return `Typed into ${detail}`
|
||||
if (typeof input?.index === 'number') return `Typed into element ${input.index}`
|
||||
return 'Typed into page'
|
||||
}
|
||||
case 'press': {
|
||||
const key = safeBrowserString(input?.key)
|
||||
return key ? `Pressed ${truncateLabel(key, 20)}` : 'Sent key press'
|
||||
}
|
||||
case 'scroll':
|
||||
return `Scrolled ${input?.direction === 'up' ? 'up' : 'down'}`
|
||||
case 'wait': {
|
||||
const ms = typeof input?.ms === 'number' ? input.ms : 1000
|
||||
return `Waited ${ms}ms`
|
||||
}
|
||||
default:
|
||||
return resultMessage ? truncateLabel(resultMessage, 72) : 'Controlled browser'
|
||||
}
|
||||
}
|
||||
|
||||
export const getBrowserControlLabel = (tool: ToolCall): string | null => {
|
||||
if (tool.name !== 'browser-control') return null
|
||||
|
||||
|
|
@ -270,11 +407,13 @@ export const getBrowserControlLabel = (tool: ToolCall): string | null => {
|
|||
}
|
||||
|
||||
if (result?.success === false) {
|
||||
return typeof result.error === 'string' ? `Browser error: ${result.error}` : 'Browser action failed'
|
||||
const error = safeBrowserString(result.error)
|
||||
return error ? `Browser error: ${truncateLabel(error, 84)}` : 'Browser action failed'
|
||||
}
|
||||
|
||||
if (typeof result?.message === 'string' && result.message.trim()) {
|
||||
return result.message
|
||||
const label = getBrowserSuccessLabel(action, input, result)
|
||||
if (label) {
|
||||
return label
|
||||
}
|
||||
|
||||
return 'Controlled browser'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue