mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-25 04:46:03 -04:00
9d463f52fb
Improved responsiveness during streaming and rendering (faster tool-call streaming, cached theme/charset lookups). --- - `execute_task_textual`: streamed `tool_call_chunk` fragments were re-joined into one string and `json.loads`-ed on *every* fragment, which is O(n²) on the UI event loop and worst for large `edit_file` argument blobs. Now fragments accumulate in a list and the buffer is joined + parsed only when it looks like complete JSON (or on the final chunk). - `get_theme_colors`: derived built-in `ThemeColors` are now memoized per `(theme name, dark)` instead of re-running ~16 hex validations on every widget render; the cache is cleared on `reload_registry`. - `_detect_charset_mode`: cached for the process like `get_glyphs` already is, since charset mode can't change at runtime. Reset alongside the glyph cache. - `AssistantMessage`: streamed text accumulates in a list and the full string is materialized lazily, avoiding per-token concatenation. - `_compose_diff_content`: the hunk-header regex is compiled once at module level, and the stats/max-line scans are folded into a single pass over the diff lines. Made by [Open SWE](https://openswe.vercel.app) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
253 lines
7.6 KiB
Python
253 lines
7.6 KiB
Python
"""Enhanced diff widget for displaying unified diffs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from textual.containers import Vertical
|
|
from textual.content import Content
|
|
from textual.widgets import Static
|
|
|
|
from deepagents_code import theme
|
|
from deepagents_code.config import get_glyphs, is_ascii_mode
|
|
|
|
if TYPE_CHECKING:
|
|
from textual.app import ComposeResult
|
|
|
|
_HUNK_RE = re.compile(r"@@ -(\d+)(?:,\d+)? \+(\d+)")
|
|
"""Matches a unified-diff hunk header, capturing the old and new start lines."""
|
|
|
|
|
|
def compose_diff_lines(
|
|
diff: str,
|
|
max_lines: int | None = 100,
|
|
) -> ComposeResult:
|
|
"""Yield per-line Static widgets for a unified diff.
|
|
|
|
Each added/removed line gets a CSS class (`.diff-line-added`,
|
|
`.diff-line-removed`) so background colors are driven by CSS variables
|
|
and update automatically on theme change.
|
|
|
|
Args:
|
|
diff: Unified diff string.
|
|
max_lines: Maximum number of diff lines to show (None for unlimited).
|
|
|
|
Yields:
|
|
Static widgets — one per diff line — with appropriate CSS classes.
|
|
"""
|
|
if not diff:
|
|
yield Static(Content.styled("No changes detected", "dim"))
|
|
else:
|
|
yield from _compose_diff_content(diff, max_lines)
|
|
|
|
|
|
def _compose_diff_content(
|
|
diff: str,
|
|
max_lines: int | None,
|
|
) -> ComposeResult:
|
|
"""Yield styled diff line widgets for non-empty diff content.
|
|
|
|
Args:
|
|
diff: Non-empty unified diff string.
|
|
max_lines: Maximum number of diff lines to show (None for unlimited).
|
|
|
|
Yields:
|
|
Static widgets for stats header and individual diff lines.
|
|
"""
|
|
colors = theme.get_theme_colors()
|
|
glyphs = get_glyphs()
|
|
lines = diff.splitlines()
|
|
|
|
# Single pass for stats and max line number (width calculation).
|
|
additions = 0
|
|
deletions = 0
|
|
max_line = 0
|
|
for ln in lines:
|
|
if ln.startswith("+"):
|
|
if not ln.startswith("+++"):
|
|
additions += 1
|
|
elif ln.startswith("-"):
|
|
if not ln.startswith("---"):
|
|
deletions += 1
|
|
elif m := _HUNK_RE.match(ln):
|
|
max_line = max(max_line, int(m.group(1)), int(m.group(2)))
|
|
|
|
# Stats header
|
|
stats_parts: list[str | tuple[str, str] | Content] = []
|
|
if additions:
|
|
stats_parts.append((f"+{additions}", colors.success))
|
|
if deletions:
|
|
if stats_parts:
|
|
stats_parts.append(" ")
|
|
stats_parts.append((f"-{deletions}", colors.error))
|
|
if stats_parts:
|
|
yield Static(Content.assemble(*stats_parts))
|
|
|
|
width = max(3, len(str(max_line + len(lines))))
|
|
|
|
old_num = new_num = 0
|
|
line_count = 0
|
|
|
|
for line in lines:
|
|
if max_lines and line_count >= max_lines:
|
|
yield Static(
|
|
Content.styled(f"\n... ({len(lines) - line_count} more lines)", "dim")
|
|
)
|
|
break
|
|
|
|
# Skip file headers (--- and +++)
|
|
if line.startswith(("---", "+++")):
|
|
continue
|
|
|
|
# Handle hunk headers - just update line numbers, don't display
|
|
if m := _HUNK_RE.match(line):
|
|
old_num, new_num = int(m.group(1)), int(m.group(2))
|
|
continue
|
|
|
|
# Handle diff lines - use gutter bar instead of +/- prefix
|
|
content = line[1:] if line else ""
|
|
|
|
if line.startswith("-"):
|
|
# Deletion — red gutter bar, background via CSS
|
|
yield Static(
|
|
Content.assemble(
|
|
(f"{glyphs.gutter_bar}", f"{colors.error} bold"),
|
|
(f"{old_num:>{width}}", "dim"),
|
|
f" {content}",
|
|
),
|
|
classes="diff-line-removed",
|
|
)
|
|
old_num += 1
|
|
line_count += 1
|
|
elif line.startswith("+"):
|
|
# Addition — green gutter bar, background via CSS
|
|
yield Static(
|
|
Content.assemble(
|
|
(f"{glyphs.gutter_bar}", f"{colors.success} bold"),
|
|
(f"{new_num:>{width}}", "dim"),
|
|
f" {content}",
|
|
),
|
|
classes="diff-line-added",
|
|
)
|
|
new_num += 1
|
|
line_count += 1
|
|
elif line.startswith(" "):
|
|
# Context line — dim gutter
|
|
yield Static(
|
|
Content.assemble(
|
|
(f"{glyphs.box_vertical}{old_num:>{width}}", "dim"),
|
|
f" {content}",
|
|
),
|
|
)
|
|
old_num += 1
|
|
new_num += 1
|
|
line_count += 1
|
|
elif line.strip() == "...":
|
|
# Truncation marker
|
|
yield Static(Content.styled("...", "dim"))
|
|
line_count += 1
|
|
else:
|
|
# Unrecognized diff line (e.g., "\ No newline at end of file")
|
|
yield Static(Content.styled(line, "dim"))
|
|
line_count += 1
|
|
|
|
|
|
class EnhancedDiff(Vertical):
|
|
"""Widget for displaying a unified diff with syntax highlighting."""
|
|
|
|
DEFAULT_CSS = """
|
|
EnhancedDiff {
|
|
height: auto;
|
|
padding: 1;
|
|
background: $surface-darken-1;
|
|
border: round $primary;
|
|
}
|
|
|
|
EnhancedDiff .diff-title {
|
|
color: $primary;
|
|
text-style: bold;
|
|
margin-bottom: 1;
|
|
}
|
|
|
|
EnhancedDiff .diff-content {
|
|
height: auto;
|
|
}
|
|
|
|
EnhancedDiff .diff-stats {
|
|
color: $text-muted;
|
|
margin-top: 1;
|
|
}
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
diff: str,
|
|
title: str = "Diff",
|
|
max_lines: int | None = 100,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Initialize the diff widget.
|
|
|
|
Args:
|
|
diff: Unified diff string
|
|
title: Title to display above the diff
|
|
max_lines: Maximum number of diff lines to show
|
|
**kwargs: Additional arguments passed to parent
|
|
"""
|
|
super().__init__(**kwargs)
|
|
self._diff = diff
|
|
self._title = title
|
|
self._max_lines = max_lines
|
|
self._stats = self._compute_stats()
|
|
|
|
def _compute_stats(self) -> tuple[int, int]:
|
|
"""Compute additions and deletions count.
|
|
|
|
Returns:
|
|
Tuple of (additions count, deletions count).
|
|
"""
|
|
additions = 0
|
|
deletions = 0
|
|
for line in self._diff.splitlines():
|
|
if line.startswith("+") and not line.startswith("+++"):
|
|
additions += 1
|
|
elif line.startswith("-") and not line.startswith("---"):
|
|
deletions += 1
|
|
return additions, deletions
|
|
|
|
def on_mount(self) -> None:
|
|
"""Set border style based on charset mode."""
|
|
if is_ascii_mode():
|
|
colors = theme.get_theme_colors(self)
|
|
self.styles.border = ("ascii", colors.primary)
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Compose the diff widget layout.
|
|
|
|
Yields:
|
|
Widgets for title, formatted diff content, and stats.
|
|
"""
|
|
colors = theme.get_theme_colors(self)
|
|
glyphs = get_glyphs()
|
|
h = glyphs.box_double_horizontal
|
|
yield Static(
|
|
Content.styled(
|
|
f"{h}{h}{h} {self._title} {h}{h}{h}", f"bold {colors.primary}"
|
|
),
|
|
classes="diff-title",
|
|
)
|
|
|
|
yield from compose_diff_lines(self._diff, self._max_lines)
|
|
|
|
additions, deletions = self._stats
|
|
if additions or deletions:
|
|
content_parts: list[str | tuple[str, str]] = []
|
|
if additions:
|
|
content_parts.append((f"+{additions}", colors.success))
|
|
if deletions:
|
|
if content_parts:
|
|
content_parts.append(" ")
|
|
content_parts.append((f"-{deletions}", colors.error))
|
|
yield Static(Content.assemble(*content_parts), classes="diff-stats")
|