mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 17:57:43 +02:00
[eric] browser: show a failed step as failed in the live overlay, with per-step timestamps
This commit is contained in:
@@ -2970,7 +2970,12 @@ async def run_browser_agent(
|
||||
result_text = result.get("text", result.get("error", ""))
|
||||
result_msg = Message(
|
||||
role="tool_result",
|
||||
content={"text": result_text, "tool_name": tu.name, "elapsed_ms": elapsed_ms},
|
||||
# `ok` because a failure's text lands in the SAME field as a success's, so the
|
||||
# live overlay was drawing a click that errored exactly like one that worked.
|
||||
# A flag rather than sniffing the text: "not found" is a perfectly successful
|
||||
# read, and a string match would call it a failure forever.
|
||||
content={"text": result_text, "tool_name": tu.name, "elapsed_ms": elapsed_ms,
|
||||
"ok": "error" not in result},
|
||||
)
|
||||
session.messages.append(result_msg)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
|
||||
@@ -24,11 +24,23 @@ interface Props {
|
||||
browserHeight: number;
|
||||
}
|
||||
|
||||
function summarizeMessage(msg: AgentMessage): { type: 'thought' | 'action' | 'result' | 'skip'; text: string } {
|
||||
// Same red as this card's own error icon, so a failed step and a failed run read as one thing.
|
||||
const P_FAILED_COLOR = '#f87171';
|
||||
|
||||
interface LogEntry {
|
||||
type: 'thought' | 'action' | 'result' | 'skip';
|
||||
text: string;
|
||||
/** Wall-clock of the message, so a stalled run shows WHERE it stalled. */
|
||||
at?: string;
|
||||
/** Only on a result: did the tool that just ran actually work? */
|
||||
ok?: boolean;
|
||||
}
|
||||
|
||||
function summarizeMessage(msg: AgentMessage): LogEntry {
|
||||
if (msg.role === 'assistant' && typeof msg.content === 'string') {
|
||||
const trimmed = msg.content.trim();
|
||||
if (!trimmed) return { type: 'skip', text: '' };
|
||||
return { type: 'thought', text: trimmed };
|
||||
return { type: 'thought', text: trimmed, at: msg.timestamp };
|
||||
}
|
||||
|
||||
if (msg.role === 'tool_call') {
|
||||
@@ -46,11 +58,14 @@ function summarizeMessage(msg: AgentMessage): { type: 'thought' | 'action' | 're
|
||||
case 'BrowserEvaluate': brief = `Evaluate JS`; break;
|
||||
default: brief = tool;
|
||||
}
|
||||
return { type: 'action', text: brief };
|
||||
return { type: 'action', text: brief, at: msg.timestamp };
|
||||
}
|
||||
|
||||
if (msg.role === 'tool_result') {
|
||||
return { type: 'result', text: '' };
|
||||
const content = typeof msg.content === 'string' ? (() => { try { return JSON.parse(msg.content); } catch { return {}; } })() : msg.content;
|
||||
// Older results predate the `ok` flag; absent means "no reason to think it failed", which keeps
|
||||
// a resumed session from repainting its whole history red.
|
||||
return { type: 'result', text: '', ok: content?.ok !== false };
|
||||
}
|
||||
|
||||
return { type: 'skip', text: '' };
|
||||
@@ -145,9 +160,24 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
|
||||
|
||||
const accentColor = c.accent.primary;
|
||||
|
||||
const entries = session.messages
|
||||
.map(summarizeMessage)
|
||||
.filter((e) => e.type !== 'skip' && e.type !== 'result');
|
||||
// A result carries no text of its own; its job is to say whether the action just above it
|
||||
// worked. Fold it back onto that action so a failed click reads as failed instead of vanishing,
|
||||
// which is what happened before: the overlay dropped results entirely and drew every action the
|
||||
// same whether it succeeded or errored.
|
||||
const entries: LogEntry[] = [];
|
||||
for (const e of session.messages.map(summarizeMessage)) {
|
||||
if (e.type === 'skip') continue;
|
||||
if (e.type === 'result') {
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
if (entries[i].type === 'action') {
|
||||
if (e.ok === false) entries[i] = { ...entries[i], ok: false };
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
entries.push(e);
|
||||
}
|
||||
|
||||
if (streamingMessage && streamingMessage.role === 'assistant' && streamingMessage.content) {
|
||||
entries.push({ type: 'thought', text: streamingMessage.content });
|
||||
@@ -439,7 +469,7 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '1px',
|
||||
bgcolor: accentColor,
|
||||
bgcolor: entry.ok === false ? P_FAILED_COLOR : accentColor,
|
||||
flexShrink: 0,
|
||||
mt: '5px',
|
||||
transform: 'rotate(45deg)',
|
||||
@@ -449,17 +479,35 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
|
||||
sx={{
|
||||
fontSize: '0.6875rem',
|
||||
fontFamily: c.font.mono,
|
||||
color: accentColor,
|
||||
color: entry.ok === false ? P_FAILED_COLOR : accentColor,
|
||||
lineHeight: 1.4,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
// Collapsed stays one tidy line; expanded is where you go to actually READ a
|
||||
// long selector or URL, so it wraps there instead of ellipsing forever.
|
||||
...(expanded
|
||||
? { display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', wordBreak: 'break-all' }
|
||||
: { textOverflow: 'ellipsis', whiteSpace: 'nowrap' }),
|
||||
}}
|
||||
>
|
||||
{entry.text}
|
||||
{entry.ok === false ? `${entry.text} — failed` : entry.text}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{expanded && entry.at && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.625rem',
|
||||
fontFamily: c.font.mono,
|
||||
color: 'rgba(255,255,255,0.28)',
|
||||
flexShrink: 0,
|
||||
ml: 'auto',
|
||||
pl: 0.5,
|
||||
mt: '4px',
|
||||
}}
|
||||
>
|
||||
{new Date(entry.at).toLocaleTimeString([], { hour12: false })}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user