fix(tui/browser): scope console logs to browser cards

This commit is contained in:
James Peter
2026-01-06 05:23:13 +10:00
parent 9d5dbfc1df
commit e1d8f12155
2 changed files with 166 additions and 6 deletions
+159 -6
View File
@@ -13,6 +13,8 @@ use serde_json::Value;
use std::path::PathBuf;
use std::time::{Duration, Instant};
const BROWSER_CONSOLE_EVENT_PREFIX: &str = "[browser console]";
pub(super) struct BrowserSessionTracker {
pub slot: ToolCardSlot,
pub cell: BrowserSessionCell,
@@ -212,6 +214,11 @@ pub(super) fn handle_custom_tool_end(
summary.value.clone(),
summary.outcome.clone(),
);
if tool_name == "browser_console" {
for line in console_lines_from_result(result) {
tracker.cell.add_console_message(line);
}
}
if let Some(code) = summary.status_code {
tracker.cell.set_status_code(Some(code));
}
@@ -247,6 +254,14 @@ pub(super) fn handle_background_event(
return false;
}
let payload = match message.strip_prefix(BROWSER_CONSOLE_EVENT_PREFIX) {
Some(rest) => rest.trim(),
None => return false,
};
if payload.is_empty() {
return false;
}
let key = key_from_order_or_last(chat, order);
let Some(key) = key else { return false; };
@@ -260,12 +275,14 @@ pub(super) fn handle_background_event(
.unwrap_or(tracker.slot.order_key);
tracker.slot.set_order_key(order_key);
let console_line = if message.starts_with("⚠️") {
message.to_string()
} else {
format!("⚠️ {}", message)
};
tracker.cell.add_console_message(console_line);
let console_lines = parse_console_output(payload);
if console_lines.is_empty() {
chat.tools_state.browser_sessions.insert(key, tracker);
return false;
}
for line in console_lines {
tracker.cell.add_console_message(line);
}
ensure_cell_picker(chat, &tracker.cell);
if tracker.slot.has_order_change() && !tracker.anchor_inserted {
@@ -661,6 +678,142 @@ fn extract_leading_status_code(text: &str) -> Option<String> {
}
}
fn console_lines_from_result(result: &Result<String, String>) -> Vec<String> {
match result {
Ok(output) => parse_console_output(output),
Err(err) => {
let trimmed = err.trim();
if trimmed.is_empty() {
Vec::new()
} else {
vec![format_warning_line(trimmed)]
}
}
}
}
fn parse_console_output(output: &str) -> Vec<String> {
let trimmed = output.trim();
if trimmed.is_empty() {
return Vec::new();
}
if let Ok(Value::Array(entries)) = serde_json::from_str::<Value>(trimmed) {
return format_console_entries(&entries);
}
if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(trimmed) {
if let Some(Value::Array(entries)) = map.get("logs") {
return format_console_entries(entries);
}
}
let mut lines = Vec::new();
for line in trimmed.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if line.eq_ignore_ascii_case("console logs:") {
continue;
}
let normalized = normalize_console_line(line);
if !normalized.is_empty() {
lines.push(normalized);
}
}
lines
}
fn format_console_entries(entries: &[Value]) -> Vec<String> {
let mut lines = Vec::new();
for entry in entries {
let line = match entry {
Value::Object(map) => format_console_entry(map),
Value::String(text) => {
let trimmed = text.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
_ => None,
};
if let Some(line) = line {
let normalized = normalize_console_line(line.as_str());
if !normalized.is_empty() {
lines.push(normalized);
}
}
}
lines
}
fn format_console_entry(entry: &serde_json::Map<String, Value>) -> Option<String> {
let message = entry
.get("message")
.and_then(Value::as_str)
.unwrap_or("")
.trim();
if message.is_empty() {
return None;
}
let level = entry
.get("level")
.and_then(Value::as_str)
.unwrap_or("log")
.to_uppercase();
let timestamp = entry
.get("timestamp")
.and_then(Value::as_str)
.map(|s| s.to_string())
.or_else(|| {
entry.get("ts_unix_ms").and_then(|value| match value {
Value::Number(num) => Some(num.to_string()),
Value::String(s) => Some(s.clone()),
_ => None,
})
})
.unwrap_or_default();
if timestamp.is_empty() {
Some(format!("[{level}] {message}"))
} else {
Some(format!("[{timestamp}] [{level}] {message}"))
}
}
fn normalize_console_line(line: &str) -> String {
let trimmed = line.trim();
if trimmed.is_empty() {
return String::new();
}
let upper = trimmed.to_ascii_uppercase();
let is_warning = upper.contains("[WARN]")
|| upper.contains("[WARNING]")
|| upper.contains("[ERROR]")
|| upper.contains("[EXCEPTION]")
|| upper.contains("[UNHANDLEDREJECTION]");
if is_warning {
format_warning_line(trimmed)
} else {
trimmed.to_string()
}
}
fn format_warning_line(line: &str) -> String {
if line.contains('⚠') {
line.to_string()
} else {
format!("⚠️ {}", line)
}
}
fn truncate(input: &str, max: usize) -> String {
if input.chars().count() <= max {
input.to_string()
+7
View File
@@ -219,6 +219,13 @@ impl BrowserSessionCell {
}
pub(crate) fn add_console_message(&mut self, message: String) {
if self
.console_messages
.last()
.map_or(false, |last| last == &message)
{
return;
}
self.console_messages.push(message);
if self.console_messages.len() > MAX_CONSOLE {
let overflow = self.console_messages.len() - MAX_CONSOLE;