From 5688c77a479a29fefb6abb60d8527ad03a7eb0e2 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 28 Jan 2026 09:56:50 -0500 Subject: [PATCH 1/3] Add a diagnostics viewer --- scripts/diagnostics-viewer.py | 754 ++++++++++++++++++++++++++++++++++ 1 file changed, 754 insertions(+) create mode 100755 scripts/diagnostics-viewer.py diff --git a/scripts/diagnostics-viewer.py b/scripts/diagnostics-viewer.py new file mode 100755 index 000000000000..25569e2856b4 --- /dev/null +++ b/scripts/diagnostics-viewer.py @@ -0,0 +1,754 @@ +#!/usr/bin/env -S uv run --quiet --script +# /// script +# dependencies = ["textual>=0.87.0"] +# /// +""" +Diagnostics Viewer - Browse and inspect Goose diagnostics bundles. + +Scans for diagnostics zip files, displays their sessions, and provides +an interactive viewer for examining session data, logs, and other files. +""" +import json +import re +import sys +import zipfile +from pathlib import Path +from typing import Optional, Any + +from textual.app import App, ComposeResult +from textual.widgets import Header, Footer, Static, Tree, ListView, ListItem, Label, Input +from textual.containers import Horizontal, Vertical, VerticalScroll, Container +from textual.binding import Binding +from textual.reactive import reactive +from textual.message import Message +from textual.screen import ModalScreen + + +def truncate_string(s: str, max_len: int = 100, edge_len: int = 35) -> str: + """Truncate a string if it's longer than max_len.""" + if len(s) <= max_len: + return s + + omitted = len(s) - (2 * edge_len) + return f"{s[:edge_len]}[{omitted} more]{s[-edge_len:]}" + + +class JsonTreeView(Tree): + """A tree widget for displaying collapsible JSON.""" + + BINDINGS = [ + Binding("ctrl+o", "toggle_all", "Toggle All", show=True), + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.json_data = None + self.show_root = False + self.all_expanded = False + + def load_json(self, data: Any, label: str = "JSON"): + """Load JSON data into the tree.""" + self.json_data = data + self.clear() + self.root.label = label + self._build_tree(self.root, data) + # Expand all nodes by default + self.root.expand_all() + + def action_toggle_all(self): + """Toggle expansion of all nodes.""" + self.all_expanded = not self.all_expanded + if self.all_expanded: + self.root.expand_all() + else: + self.root.collapse_all() + self.root.expand() # Keep root expanded + + def on_tree_node_selected(self, event: Tree.NodeSelected): + """Handle node selection - show modal for truncated strings.""" + node = event.node + + # Check if this is a truncated string node + if node.data and isinstance(node.data, dict) and node.data.get("truncated"): + key = node.data["key"] + value = node.data["value"] + + # Show the full string in a modal + title = f"Full String Value for '{key}'" + self.app.push_screen(TextViewerModal(title, value)) + + # Prevent default tree expansion behavior + event.stop() + + def _build_tree(self, node, data, max_depth=10, current_depth=0): + """Recursively build the tree from JSON data.""" + if current_depth > max_depth: + node.add_leaf("[dim]...[/dim]") + return + + if isinstance(data, dict): + for key, value in data.items(): + if isinstance(value, (dict, list)) and value: + # Expand first level by default + child = node.add(f"[cyan]{key}[/cyan]: {{...}}" if isinstance(value, dict) else f"[cyan]{key}[/cyan]: [...]", expand=(current_depth == 0)) + child.data = {"key": key, "value": value, "type": type(value).__name__, "expandable": False} + self._build_tree(child, value, max_depth, current_depth + 1) + elif isinstance(value, str): + truncated = truncate_string(value) + if truncated != value: + # Make truncated strings expandable + child = node.add(f"[cyan]{key}[/cyan]: [green]\"{truncated}\"[/green]", expand=False) + child.data = {"key": key, "value": value, "type": "str", "truncated": True, "expandable": True} + child.allow_expand = False # Don't show expand icon initially + else: + node.add_leaf(f"[cyan]{key}[/cyan]: [green]\"{value}\"[/green]") + elif isinstance(value, (int, float)): + node.add_leaf(f"[cyan]{key}[/cyan]: [yellow]{value}[/yellow]") + elif isinstance(value, bool): + node.add_leaf(f"[cyan]{key}[/cyan]: [magenta]{str(value).lower()}[/magenta]") + elif value is None: + node.add_leaf(f"[cyan]{key}[/cyan]: [dim]null[/dim]") + else: + node.add_leaf(f"[cyan]{key}[/cyan]: {value}") + + elif isinstance(data, list): + for i, item in enumerate(data): + if isinstance(item, (dict, list)) and item: + # Expand first level by default + child = node.add(f"[yellow]{i}[/yellow]: {{...}}" if isinstance(item, dict) else f"[yellow]{i}[/yellow]: [...]", expand=(current_depth == 0)) + child.data = {"key": i, "value": item, "type": type(item).__name__, "expandable": False} + self._build_tree(child, item, max_depth, current_depth + 1) + elif isinstance(item, str): + truncated = truncate_string(item) + if truncated != item: + # Make truncated strings expandable + child = node.add(f"[yellow]{i}[/yellow]: [green]\"{truncated}\"[/green]", expand=False) + child.data = {"key": i, "value": item, "type": "str", "truncated": True, "expandable": True} + child.allow_expand = False # Don't show expand icon initially + else: + node.add_leaf(f"[yellow]{i}[/yellow]: [green]\"{item}\"[/green]") + elif isinstance(item, (int, float)): + node.add_leaf(f"[yellow]{i}[/yellow]: [yellow]{item}[/yellow]") + elif isinstance(item, bool): + node.add_leaf(f"[yellow]{i}[/yellow]: [magenta]{str(item).lower()}[/magenta]") + elif item is None: + node.add_leaf(f"[yellow]{i}[/yellow]: [dim]null[/dim]") + else: + node.add_leaf(f"[yellow]{i}[/yellow]: {item}") + + +class TextViewerModal(ModalScreen): + """Modal screen for viewing long text strings.""" + + BINDINGS = [ + Binding("escape,q,enter", "dismiss", "Close", show=True), + ] + + def __init__(self, title: str, text: str): + super().__init__() + self.title = title + self.text = text + + def compose(self) -> ComposeResult: + """Compose the modal content.""" + with Vertical(id="modal-container"): + yield Static(f"[bold]{self.title}[/bold]", id="modal-title") + with VerticalScroll(id="modal-scroll"): + yield Static(self.text, id="modal-text") + yield Static("[dim]Press Escape, Q, or Enter to close[/dim]", id="modal-footer") + + def action_dismiss(self): + """Dismiss the modal.""" + self.app.pop_screen() + + +class SearchOverlay(Container): + """Search overlay widget.""" + + def __init__(self): + super().__init__() + self.display = False + + def compose(self) -> ComposeResult: + with Horizontal(id="search-container"): + yield Static("Search: ", id="search-label") + yield Input(placeholder="Type to search...", id="search-input") + yield Static("", id="search-results") + + +class DiagnosticsSession: + """Represents a diagnostics bundle.""" + + def __init__(self, zip_path: Path): + self.zip_path = zip_path + self.name = "Unknown Session" + self.session_id = zip_path.stem + self.created_at = zip_path.stat().st_mtime + self._load_session_name() + + def _load_session_name(self): + """Extract session name from session.json.""" + try: + with zipfile.ZipFile(self.zip_path, 'r') as zf: + # Find session.json + for name in zf.namelist(): + if name.endswith('session.json'): + with zf.open(name) as f: + data = json.load(f) + self.name = data.get('name', 'Unknown Session') + self.session_id = data.get('id', self.zip_path.stem) + break + except Exception as e: + self.name = f"Error loading: {e}" + + def get_file_list(self) -> list[str]: + """Get list of files in the zip, sorted with system.txt first.""" + try: + with zipfile.ZipFile(self.zip_path, 'r') as zf: + files = zf.namelist() + + # Sort: system.txt first, then session.json, then alphabetically + def sort_key(f): + if f.endswith('system.txt'): + return (0, f) + elif f.endswith('session.json'): + return (1, f) + elif f.endswith('config.yaml'): + return (2, f) + else: + return (3, f) + + return sorted(files, key=sort_key) + except Exception: + return [] + + def read_file(self, filename: str) -> Optional[str]: + """Read a file from the zip.""" + try: + with zipfile.ZipFile(self.zip_path, 'r') as zf: + with zf.open(filename) as f: + return f.read().decode('utf-8', errors='replace') + except Exception as e: + return f"Error reading file: {e}" + + +class FileContentPane(Vertical): + """A pane that shows either JSON tree or plain text.""" + + def __init__(self, title: str): + super().__init__() + self.title = title + self.content_type = "empty" + self.json_data = None + self.text_content = "" + + def compose(self) -> ComposeResult: + """Compose the pane content.""" + if self.content_type == "json": + tree = JsonTreeView(self.title) + if self.json_data is not None: + tree.load_json(self.json_data, self.title) + yield tree + elif self.content_type == "text": + with VerticalScroll(): + yield Static(self.text_content) + else: + yield Static("[dim]No content[/dim]") + + def set_json(self, data: Any): + """Set JSON content.""" + self.content_type = "json" + self.json_data = data + + def set_text(self, text: str): + """Set text content.""" + self.content_type = "text" + self.text_content = text + + +class FileViewer(Vertical): + """Widget for viewing file contents.""" + + def __init__(self): + super().__init__() + self.current_session = None + + def compose(self) -> ComposeResult: + """Create child widgets.""" + with Vertical(id="content-area"): + yield Static("[dim]Select a file to view[/dim]") + + yield SearchOverlay() + + def update_content(self, session: DiagnosticsSession, filename: str, part: str = None): + """Update the viewer with new file content. + + Args: + session: The diagnostics session + filename: The file to display + part: For JSONL files, either "request" or "responses" + """ + self.current_session = session + + content = session.read_file(filename) + if content is None: + self._show_plain(filename, "File not found") + return + + # Check if this is a JSONL file + if filename.endswith('.jsonl') and part: + self._show_jsonl(filename, content, part) + elif filename.endswith('.json'): + self._show_json(filename, content) + else: + self._show_plain(filename, content) + + # Auto-focus the content + self.post_message(self.ContentReady()) + + def _show_jsonl(self, filename: str, content: str, part: str): + """Show JSONL file - either request or responses part.""" + lines = [line.strip() for line in content.strip().split('\n') if line.strip()] + + # Parse lines + request_data = None + responses = [] + + if len(lines) > 0: + try: + request_data = json.loads(lines[0]) + except json.JSONDecodeError: + pass + + for i in range(1, len(lines)): + try: + responses.append(json.loads(lines[i])) + except json.JSONDecodeError: + pass + + # Show content + content_area = self.query_one("#content-area", Vertical) + content_area.remove_children() + + if part == "request" and request_data: + tree = JsonTreeView(f"{filename} - request") + tree.load_json(request_data, f"{filename} - request") + content_area.mount(tree) + elif part == "responses" and responses: + tree = JsonTreeView(f"{filename} - responses") + if len(responses) == 1: + tree.load_json(responses[0], f"{filename} - response") + else: + tree.load_json(responses, f"{filename} - responses") + content_area.mount(tree) + else: + content_area.mount(Static("[red]No data available for this part[/red]")) + + def _show_json(self, filename: str, content: str): + """Show JSON file with collapsible tree.""" + # Show content + content_area = self.query_one("#content-area", Vertical) + content_area.remove_children() + + tree = JsonTreeView(filename) + try: + data = json.loads(content) + tree.load_json(data, filename) + except json.JSONDecodeError as e: + tree.root.add_leaf(f"[red]Error parsing JSON: {e}[/red]") + + content_area.mount(tree) + + def _show_plain(self, filename: str, content: str): + """Show plain text content.""" + # Show content + content_area = self.query_one("#content-area", Vertical) + content_area.remove_children() + + # Create and mount the scroll container with the content + scroll = VerticalScroll() + content_area.mount(scroll) + scroll.mount(Static(content)) + + def focus_content(self): + """Focus the content area.""" + try: + # Try to focus a tree if present + tree = self.query_one(JsonTreeView) + tree.focus() + except: + pass + + def action_search(self): + """Show search overlay.""" + overlay = self.query_one(SearchOverlay) + overlay.display = not overlay.display + if overlay.display: + search_input = overlay.query_one("#search-input", Input) + search_input.focus() + + class ContentReady(Message): + """Message sent when content is ready to be focused.""" + pass + + +class SessionViewer(Vertical): + """Widget for viewing a diagnostics session.""" + + BINDINGS = [ + Binding("ctrl+f,cmd+f", "search", "Search", show=True), + ] + + def __init__(self, session: DiagnosticsSession): + super().__init__() + self.session = session + + def compose(self) -> ComposeResult: + """Create child widgets.""" + yield Static(f"[bold yellow]Session: {self.session.name}[/bold yellow]", id="session-title") + + with Horizontal(id="main-content"): + # Left side: File browser + with Vertical(id="file-browser"): + yield Static("[bold]Files:[/bold]") + tree = Tree("Files", id="file-tree") + tree.show_root = False + + # Build file tree + files = self.session.get_file_list() + + # Group by directory + dirs = {} + for file in files: + parts = file.split('/') + is_jsonl = file.endswith('.jsonl') + + if len(parts) == 1: + # Root file + if is_jsonl: + # Add two entries for JSONL files + tree.root.add_leaf(f"{file} - request", data={"file": file, "part": "request"}) + tree.root.add_leaf(f"{file} - responses", data={"file": file, "part": "responses"}) + else: + tree.root.add_leaf(file, data={"file": file, "part": None}) + else: + # File in directory + dir_name = parts[0] + if dir_name not in dirs: + dirs[dir_name] = tree.root.add(dir_name, expand=True) + + file_name = '/'.join(parts[1:]) + if is_jsonl: + # Add two entries for JSONL files + dirs[dir_name].add_leaf(f"{file_name} - request", data={"file": file, "part": "request"}) + dirs[dir_name].add_leaf(f"{file_name} - responses", data={"file": file, "part": "responses"}) + else: + dirs[dir_name].add_leaf(file_name, data={"file": file, "part": None}) + + yield tree + + # Right side: File viewer + yield FileViewer() + + def on_mount(self): + """Handle mount event.""" + # Show system.txt by default and select it in tree + files = self.session.get_file_list() + system_file = next((f for f in files if f.endswith('system.txt')), None) + if system_file: + viewer = self.query_one(FileViewer) + viewer.update_content(self.session, system_file) + + # Select the first node in the tree + tree = self.query_one("#file-tree", Tree) + if tree.root.children: + tree.select_node(tree.root.children[0]) + + # Focus the tree initially + tree = self.query_one("#file-tree", Tree) + tree.focus() + + def on_tree_node_selected(self, event: Tree.NodeSelected): + """Handle file selection.""" + # Only handle selections from the file tree, not the JSON tree + if event.control.id != "file-tree": + return + + # Make sure it's a file (has dict data), not a directory + if event.node.data and isinstance(event.node.data, dict) and event.node.parent: + viewer = self.query_one(FileViewer) + file_path = event.node.data["file"] + part = event.node.data["part"] + viewer.update_content(self.session, file_path, part) + + def on_file_viewer_content_ready(self, event: FileViewer.ContentReady): + """Handle content ready event by focusing the viewer.""" + viewer = self.query_one(FileViewer) + viewer.focus_content() + + def action_search(self): + """Toggle search in the file viewer.""" + viewer = self.query_one(FileViewer) + viewer.action_search() + + def on_key(self, event): + """Handle left/right navigation between panels.""" + if event.key == "left": + tree = self.query_one("#file-tree", Tree) + tree.focus() + elif event.key == "right": + viewer = self.query_one(FileViewer) + viewer.focus_content() + + +class SessionList(Vertical): + """Widget for listing available sessions.""" + + def __init__(self, sessions: list[DiagnosticsSession]): + super().__init__() + self.sessions = sessions + + def compose(self) -> ComposeResult: + """Create child widgets.""" + yield Static("[bold yellow]Available Diagnostics Sessions[/bold yellow]\n") + + if not self.sessions: + yield Static("[red]No diagnostics files found[/red]") + else: + yield Static(f"[dim]Found {len(self.sessions)} session(s)[/dim]\n") + yield ListView(id="session-list") + + def on_mount(self): + """Populate the list after mounting.""" + list_view = self.query_one(ListView) + for session in self.sessions: + item = ListItem( + Label(f"{session.name}\n[dim]{session.zip_path.name}[/dim]"), + name=session.zip_path.name + ) + list_view.append(item) + + +class DiagnosticsApp(App): + """Diagnostics viewer application.""" + + # Disable command palette (Ctrl+\) + ENABLE_COMMAND_PALETTE = False + + CSS = """ + Screen { + background: $surface; + } + + /* Modal styles */ + TextViewerModal { + align: center middle; + } + + #modal-container { + width: 80%; + height: 80%; + background: $surface; + border: thick $primary; + padding: 1; + } + + #modal-title { + background: $primary; + color: $text; + padding: 1; + text-align: center; + dock: top; + } + + #modal-scroll { + height: 1fr; + border: solid $accent; + padding: 1; + margin: 1 0; + } + + #modal-text { + width: 100%; + } + + #modal-footer { + text-align: center; + dock: bottom; + } + + #session-title { + padding: 1; + background: $primary; + color: $text; + text-align: center; + height: 3; + } + + #main-content { + height: 100%; + } + + #file-browser { + width: 30%; + border-right: solid $primary; + padding: 1; + } + + FileViewer { + width: 70%; + height: 100%; + } + + #content-area { + height: 100%; + padding: 1; + } + + JsonTreeView { + height: 100%; + scrollbar-gutter: stable; + } + + #search-container { + height: 3; + background: $panel; + padding: 1; + border-top: solid $primary; + } + + #search-label { + width: auto; + margin-right: 1; + } + + #search-input { + width: 1fr; + margin-right: 1; + } + + #search-results { + width: auto; + } + + SearchOverlay { + height: auto; + } + + #session-list { + height: 100%; + } + + ListView { + background: $surface; + } + + ListItem { + padding: 1; + } + + ListItem:hover { + background: $primary 30%; + } + + Tree { + height: 100%; + } + + Tree:focus { + border: solid $accent; + } + """ + + BINDINGS = [ + Binding("q", "quit", "Quit"), + Binding("escape", "back", "Back to list"), + Binding("ctrl+f,cmd+f", "search", "Search", show=False), + ] + + def __init__(self, diagnostics_dir: Path): + super().__init__() + self.diagnostics_dir = diagnostics_dir + self.sessions = [] + self.current_view = None + + def compose(self) -> ComposeResult: + """Create child widgets.""" + yield Header() + yield Footer() + + def on_mount(self): + """Handle mount event.""" + self.title = "Goose Diagnostics Viewer" + self.scan_diagnostics() + self.show_session_list() + + def scan_diagnostics(self): + """Scan for diagnostics zip files.""" + self.sessions = [] + + # Find all diagnostics zip files + for zip_path in self.diagnostics_dir.glob("diagnostics*.zip"): + session = DiagnosticsSession(zip_path) + self.sessions.append(session) + + # Sort by creation time (newest first) + self.sessions.sort(key=lambda s: s.created_at, reverse=True) + + def show_session_list(self): + """Show the session list view.""" + if self.current_view: + self.current_view.remove() + + self.current_view = SessionList(self.sessions) + self.mount(self.current_view) + + def show_session_viewer(self, session: DiagnosticsSession): + """Show the session viewer.""" + if self.current_view: + self.current_view.remove() + + self.current_view = SessionViewer(session) + self.mount(self.current_view) + + def on_list_view_selected(self, event: ListView.Selected): + """Handle session selection.""" + # Find the session by zip name + session_name = event.item.name + session = next((s for s in self.sessions if s.zip_path.name == session_name), None) + if session: + self.show_session_viewer(session) + + def action_back(self): + """Go back to session list.""" + if isinstance(self.current_view, SessionViewer): + self.show_session_list() + + def action_quit(self): + """Quit the application.""" + self.exit() + + def action_search(self): + """Toggle search.""" + if isinstance(self.current_view, SessionViewer): + self.current_view.action_search() + + +def main(): + """Main entry point.""" + # Get diagnostics directory from args or use default + if len(sys.argv) > 1: + diagnostics_dir = Path(sys.argv[1]).expanduser() + else: + diagnostics_dir = Path.home() / "Downloads" + + if not diagnostics_dir.exists(): + print(f"Error: Directory '{diagnostics_dir}' not found", file=sys.stderr) + sys.exit(1) + + app = DiagnosticsApp(diagnostics_dir) + app.run() + + +if __name__ == "__main__": + main() From 7df8911ff24b704d83a97a768c028a7acb906cc7 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 28 Jan 2026 09:57:23 -0500 Subject: [PATCH 2/3] Add warning --- scripts/diagnostics-viewer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/diagnostics-viewer.py b/scripts/diagnostics-viewer.py index 25569e2856b4..115ecb327328 100755 --- a/scripts/diagnostics-viewer.py +++ b/scripts/diagnostics-viewer.py @@ -3,6 +3,9 @@ # dependencies = ["textual>=0.87.0"] # /// """ +WARNING: entirely vibe coded. use as a throwaway tool + + Diagnostics Viewer - Browse and inspect Goose diagnostics bundles. Scans for diagnostics zip files, displays their sessions, and provides From 42b3367717bc90ceeb5b0ff8724d31da85e5de4f Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 28 Jan 2026 10:25:12 -0500 Subject: [PATCH 3/3] What copilot said --- scripts/diagnostics-viewer.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/scripts/diagnostics-viewer.py b/scripts/diagnostics-viewer.py index 115ecb327328..95aec894a100 100755 --- a/scripts/diagnostics-viewer.py +++ b/scripts/diagnostics-viewer.py @@ -12,7 +12,6 @@ an interactive viewer for examining session data, logs, and other files. """ import json -import re import sys import zipfile from pathlib import Path @@ -22,7 +21,6 @@ from textual.widgets import Header, Footer, Static, Tree, ListView, ListItem, Label, Input from textual.containers import Horizontal, Vertical, VerticalScroll, Container from textual.binding import Binding -from textual.reactive import reactive from textual.message import Message from textual.screen import ModalScreen @@ -105,10 +103,11 @@ def _build_tree(self, node, data, max_depth=10, current_depth=0): child.allow_expand = False # Don't show expand icon initially else: node.add_leaf(f"[cyan]{key}[/cyan]: [green]\"{value}\"[/green]") - elif isinstance(value, (int, float)): - node.add_leaf(f"[cyan]{key}[/cyan]: [yellow]{value}[/yellow]") elif isinstance(value, bool): + # Check bool before int/float since bool is a subclass of int node.add_leaf(f"[cyan]{key}[/cyan]: [magenta]{str(value).lower()}[/magenta]") + elif isinstance(value, (int, float)): + node.add_leaf(f"[cyan]{key}[/cyan]: [yellow]{value}[/yellow]") elif value is None: node.add_leaf(f"[cyan]{key}[/cyan]: [dim]null[/dim]") else: @@ -130,10 +129,11 @@ def _build_tree(self, node, data, max_depth=10, current_depth=0): child.allow_expand = False # Don't show expand icon initially else: node.add_leaf(f"[yellow]{i}[/yellow]: [green]\"{item}\"[/green]") - elif isinstance(item, (int, float)): - node.add_leaf(f"[yellow]{i}[/yellow]: [yellow]{item}[/yellow]") elif isinstance(item, bool): + # Check bool before int/float since bool is a subclass of int node.add_leaf(f"[yellow]{i}[/yellow]: [magenta]{str(item).lower()}[/magenta]") + elif isinstance(item, (int, float)): + node.add_leaf(f"[yellow]{i}[/yellow]: [yellow]{item}[/yellow]") elif item is None: node.add_leaf(f"[yellow]{i}[/yellow]: [dim]null[/dim]") else: @@ -226,13 +226,18 @@ def sort_key(f): return [] def read_file(self, filename: str) -> Optional[str]: - """Read a file from the zip.""" + """Read a file from the zip. + + Returns: + File content as string, or None if file cannot be read. + """ try: with zipfile.ZipFile(self.zip_path, 'r') as zf: with zf.open(filename) as f: return f.read().decode('utf-8', errors='replace') - except Exception as e: - return f"Error reading file: {e}" + except Exception: + # File not found or cannot be read + return None class FileContentPane(Vertical): @@ -295,7 +300,7 @@ def update_content(self, session: DiagnosticsSession, filename: str, part: str = content = session.read_file(filename) if content is None: - self._show_plain(filename, "File not found") + self._show_plain(filename, f"[red]Error: Could not read file '{filename}'[/red]") return # Check if this is a JSONL file @@ -321,12 +326,14 @@ def _show_jsonl(self, filename: str, content: str, part: str): try: request_data = json.loads(lines[0]) except json.JSONDecodeError: + # Skip malformed request line; diagnostics may be truncated or corrupted pass for i in range(1, len(lines)): try: responses.append(json.loads(lines[i])) except json.JSONDecodeError: + # Skip individual malformed response lines; show only valid JSON entries pass # Show content @@ -379,11 +386,15 @@ def focus_content(self): # Try to focus a tree if present tree = self.query_one(JsonTreeView) tree.focus() - except: + except Exception: + # No JsonTreeView present (e.g., showing plain text), which is fine pass def action_search(self): - """Show search overlay.""" + """Show search overlay. + + TODO: Implement actual search functionality - currently just shows UI. + """ overlay = self.query_one(SearchOverlay) overlay.display = not overlay.display if overlay.display: