diff --git a/features/steps/tui_permissions_screen_steps.py b/features/steps/tui_permissions_screen_steps.py index a9fa59515..7dc7e5a39 100644 --- a/features/steps/tui_permissions_screen_steps.py +++ b/features/steps/tui_permissions_screen_steps.py @@ -2,7 +2,7 @@ Tests cover: - DiffDisplayMode, FileChangeType, PermissionDecision enums -- PermissionRequest model (unified/side-by-side/context diffs) +- PermissionRequest model (unified/split/auto diffs) - ToolPermissionRequest model (decisions, is_pending, is_allowed, is_rejected) - PermissionRequestService (queue, decisions, session-scoped decisions) - PermissionsScreen widget (load, navigate, cycle mode, allow/reject) @@ -166,7 +166,7 @@ def step_rendered_diff_empty(context): @when("I call split_diff on the request") -def step_call_side_by_side_diff(context): +def step_call_split_diff(context): context._sbs_left, context._sbs_right = context._perm_request.split_diff() @@ -548,7 +548,7 @@ def step_screen_current_request_not_none(context): # --------------------------------------------------------------------------- -# PermissionRequest side_by_side_diff delete/insert branches +# PermissionRequest split_diff delete/insert branches # --------------------------------------------------------------------------- diff --git a/features/tui_permissions_screen.feature b/features/tui_permissions_screen.feature index d96f920b8..f0ce17582 100644 --- a/features/tui_permissions_screen.feature +++ b/features/tui_permissions_screen.feature @@ -6,7 +6,7 @@ Feature: TUI PermissionsScreen Scenario: DiffDisplayMode has three values When I inspect the DiffDisplayMode enum - Then DiffDisplayMode should have values "unified", "side_by_side", "context" + Then DiffDisplayMode should have values "unified", "split", "auto" # ── FileChangeType enum ─────────────────────────────────────── @@ -29,14 +29,14 @@ Feature: TUI PermissionsScreen And the rendered diff should contain "+" And the rendered diff should contain "-" - Scenario: PermissionRequest generates a side-by-side diff + Scenario: PermissionRequest generates a split diff Given a PermissionRequest for "src/auth/handler.py" with before and after content - When I render the diff in "side_by_side" mode + When I render the diff in "split" mode Then the rendered diff should contain "|" - Scenario: PermissionRequest generates a context diff + Scenario: PermissionRequest generates an auto diff Given a PermissionRequest for "src/auth/handler.py" with before and after content - When I render the diff in "context" mode + When I render the diff in "auto" mode Then the rendered diff should contain "***" Scenario: PermissionRequest unified diff with no changes returns empty string @@ -44,14 +44,14 @@ Feature: TUI PermissionsScreen When I render the diff in "unified" mode Then the rendered diff should be empty - Scenario: PermissionRequest side_by_side diff returns equal lines for identical content + Scenario: PermissionRequest split diff returns equal lines for identical content Given a PermissionRequest for "src/empty.py" with identical before and after content - When I call side_by_side_diff on the request + When I call split_diff on the request Then both sides should have the same number of lines - Scenario: PermissionRequest context_diff with no changes returns empty string + Scenario: PermissionRequest auto_diff with no changes returns empty string Given a PermissionRequest for "src/empty.py" with identical before and after content - When I render the diff in "context" mode + When I render the diff in "auto" mode Then the rendered diff should be empty Scenario: PermissionRequest render_diff dispatches to unified @@ -59,9 +59,9 @@ Feature: TUI PermissionsScreen When I render the diff in "unified" mode Then the rendered diff should contain "a/src/auth/handler.py" - Scenario: PermissionRequest render_diff dispatches to context + Scenario: PermissionRequest render_diff dispatches to auto Given a PermissionRequest for "src/auth/handler.py" with before and after content - When I render the diff in "context" mode + When I render the diff in "auto" mode Then the rendered diff should contain "a/src/auth/handler.py" # ── ToolPermissionRequest model ─────────────────────────────── @@ -221,17 +221,17 @@ Feature: TUI PermissionsScreen When I load a ToolPermissionRequest with 3 file changes Then the diff mode should be "unified" When I cycle the diff mode - Then the diff mode should be "side_by_side" + Then the diff mode should be "split" When I cycle the diff mode - Then the diff mode should be "context" + Then the diff mode should be "auto" When I cycle the diff mode Then the diff mode should be "unified" Scenario: PermissionsScreen set_diff_mode sets mode directly Given a fresh PermissionsScreen When I load a ToolPermissionRequest with 3 file changes - And I set the diff mode to "side_by_side" - Then the diff mode should be "side_by_side" + And I set the diff mode to "split" + Then the diff mode should be "split" Scenario: PermissionsScreen allow_once records decision Given a fresh PermissionsScreen @@ -309,16 +309,16 @@ Feature: TUI PermissionsScreen # ── _next_diff_mode helper ──────────────────────────────────── - Scenario: _next_diff_mode cycles unified to side_by_side + Scenario: _next_diff_mode cycles unified to split When I call _next_diff_mode with "unified" - Then the next diff mode result should be "side_by_side" + Then the next diff mode result should be "split" - Scenario: _next_diff_mode cycles side_by_side to context - When I call _next_diff_mode with "side_by_side" - Then the next diff mode result should be "context" + Scenario: _next_diff_mode cycles split to auto + When I call _next_diff_mode with "split" + Then the next diff mode result should be "auto" - Scenario: _next_diff_mode cycles context back to unified - When I call _next_diff_mode with "context" + Scenario: _next_diff_mode cycles auto back to unified + When I call _next_diff_mode with "auto" Then the next diff mode result should be "unified" # ── Rendering helpers ───────────────────────────────────────── @@ -351,17 +351,17 @@ Feature: TUI PermissionsScreen When I load a ToolPermissionRequest with 3 file changes Then the screen current_request should not be None - # ── PermissionRequest side_by_side_diff delete/insert branches ─ + # ── PermissionRequest split_diff delete/insert branches ──────── - Scenario: PermissionRequest side_by_side_diff handles delete-only change + Scenario: PermissionRequest split_diff handles delete-only change Given a PermissionRequest with only deleted lines - When I call side_by_side_diff on the request + When I call split_diff on the request Then the left side should have non-empty lines And the right side should have empty lines for deleted content - Scenario: PermissionRequest side_by_side_diff handles insert-only change + Scenario: PermissionRequest split_diff handles insert-only change Given a PermissionRequest with only inserted lines - When I call side_by_side_diff on the request + When I call split_diff on the request Then the right side should have non-empty lines And the left side should have empty lines for inserted content diff --git a/src/cleveragents/tui/permissions/models.py b/src/cleveragents/tui/permissions/models.py index a4a12ef30..f1cb19139 100644 --- a/src/cleveragents/tui/permissions/models.py +++ b/src/cleveragents/tui/permissions/models.py @@ -6,6 +6,7 @@ tool permission requests with diff views and record user decisions. from __future__ import annotations +import shutil from enum import StrEnum from pydantic import BaseModel, ConfigDict, Field @@ -23,16 +24,17 @@ class DiffDisplayMode(StrEnum): """Diff display modes for the PermissionsScreen. Toggled with the ``d`` key in the PermissionsScreen. + Cycle: unified → split → auto → unified … """ UNIFIED = "unified" """Standard unified diff with +/- lines (default).""" - SIDE_BY_SIDE = "side_by_side" + SPLIT = "split" """Two-column view with old content left, new content right.""" - CONTEXT = "context" - """Shows only changed lines with surrounding context (3 lines default).""" + AUTO = "auto" + """Selects unified or split based on available terminal width.""" class FileChangeType(StrEnum): @@ -101,8 +103,8 @@ class PermissionRequest(BaseModel): ) return "".join(diff_lines) - def side_by_side_diff(self) -> tuple[list[str], list[str]]: - """Return (left_lines, right_lines) for side-by-side display.""" + def split_diff(self) -> tuple[list[str], list[str]]: + """Return (left_lines, right_lines) for split (side-by-side) display.""" import difflib before_lines = (self.before_content or "").splitlines() @@ -137,8 +139,12 @@ class PermissionRequest(BaseModel): return left, right - def context_diff(self, *, context_lines: int = 3) -> str: - """Generate a context diff showing only changed lines with context.""" + def auto_diff(self, *, context_lines: int = 3) -> str: + """Generate a context diff showing only changed lines with context. + + In AUTO mode, the display mode is selected based on available + terminal width: unified for narrow terminals, split for wide. + """ import difflib before_lines = (self.before_content or "").splitlines(keepends=True) @@ -161,14 +167,14 @@ class PermissionRequest(BaseModel): """Render the diff in the specified display mode.""" if mode == DiffDisplayMode.UNIFIED: return self.unified_diff(context_lines=context_lines) - if mode == DiffDisplayMode.SIDE_BY_SIDE: - left, right = self.side_by_side_diff() + if mode == DiffDisplayMode.SPLIT: + left, right = self.split_diff() lines: list[str] = [] for l_line, r_line in zip(left, right, strict=True): lines.append(f"{l_line:<40} | {r_line}") return "\n".join(lines) - # CONTEXT mode - return self.context_diff(context_lines=context_lines) + # AUTO mode + return self.auto_diff(context_lines=context_lines) class ToolPermissionRequest(BaseModel): diff --git a/src/cleveragents/tui/permissions/screen.py b/src/cleveragents/tui/permissions/screen.py index b56b05df5..574c9b194 100644 --- a/src/cleveragents/tui/permissions/screen.py +++ b/src/cleveragents/tui/permissions/screen.py @@ -1,7 +1,7 @@ """PermissionsScreen widget for displaying tool permission requests with diff views. Shows a split-pane layout: a file list on the left and a diff view on the right. -Supports three diff display modes (unified, side-by-side, context) toggled with ``d``. +Supports three diff display modes (unified, split, auto) toggled with ``d``. Allow/reject keyboard bindings: ``a`` allow-once, ``A`` allow-always, ``r`` reject-once, ``R`` reject-always. """ @@ -44,8 +44,8 @@ _StaticBase = _load_static_base() _DIFF_MODE_CYCLE: list[DiffDisplayMode] = [ DiffDisplayMode.UNIFIED, - DiffDisplayMode.SIDE_BY_SIDE, - DiffDisplayMode.CONTEXT, + DiffDisplayMode.SPLIT, + DiffDisplayMode.AUTO, ] @@ -128,7 +128,7 @@ class PermissionsScreen(_StaticBase): - ``r``: reject once - ``R``: reject always (session) - ``j`` / ``k``: navigate file list (next / previous) - - ``d``: cycle diff display mode (unified → side-by-side → context) + - ``d``: cycle diff display mode (unified → split → auto) - ``escape``: dismiss (caller responsibility) """