Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion libs/code/deepagents_code/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2637,6 +2637,14 @@ def __init__(
completion in the chat input.
"""

self._debug_console_cleared_upto = 0
"""Absolute emission index the Debug Console was last cleared up to.

Persists a `Ctrl+L` clear across close/reopen of the console for the
process lifetime; each newly opened `DebugConsoleScreen` is seeded from
it and reports a fresh clear back through its `on_clear` callback.
"""

self._lc_thread_id = thread_id
"""LangChain thread identifier.

Expand Down Expand Up @@ -17112,8 +17120,16 @@ def handle_result(_: None) -> None:
if self._chat_input:
self._chat_input.focus_input()

def persist_clear(cursor: int) -> None:
self._debug_console_cleared_upto = cursor

self.push_screen(
DebugConsoleScreen(self._build_debug_snapshot()), handle_result
DebugConsoleScreen(
self._build_debug_snapshot(),
cleared_upto=self._debug_console_cleared_upto,
on_clear=persist_clear,
),
handle_result,
)

def _build_debug_snapshot(self) -> list[SnapshotField]:
Expand Down
27 changes: 23 additions & 4 deletions libs/code/deepagents_code/tui/widgets/debug_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,17 +706,30 @@ class DebugConsoleScreen(ModalScreen[None]):
}
"""

def __init__(self, snapshot: Sequence[SnapshotField]) -> None:
def __init__(
self,
snapshot: Sequence[SnapshotField],
*,
cleared_upto: int = 0,
on_clear: Callable[[int], None] | None = None,
) -> None:
"""Initialize with a captured *snapshot* of session/runtime fields.

Args:
snapshot: Ordered `SnapshotField` rows rendered in the header.
cleared_upto: Absolute emission index a prior `Ctrl+L` cleared up to.
The console starts rendering from here so a clear persists across
close/reopen; records emitted after it still appear.
on_clear: Invoked with the new clear cursor whenever `Ctrl+L` clears
the view, letting the owner persist it for the next open.
"""
super().__init__()
self._snapshot = list(snapshot)
self._records: list[InMemoryLogRecord] = []
# Absolute index of the next unrendered log record (incremental writes).
self._rendered_upto = 0
# Absolute index of the next unrendered log record (incremental writes),
# seeded from any persisted clear so reopening honors the last Ctrl+L.
self._rendered_upto = cleared_upto
self._on_clear = on_clear
# One-shot guard so the "buffer unavailable" notice is written only once.
self._missing_notice_shown = False
self._level_filter: FilterValue = "all"
Expand Down Expand Up @@ -1025,12 +1038,18 @@ def _refresh_log_view(self, *, scroll_end: bool) -> None:
)

def action_clear_view(self) -> None:
"""Clear the on-screen log view; the in-memory buffer keeps accruing."""
"""Clear the on-screen log view; the in-memory buffer keeps accruing.

Advances the render cursor past everything emitted so far and reports it
via `on_clear` so the owner can persist the clear across close/reopen.
"""
self.query_one("#debug-log", _DebugLogView).clear_records()
self._records.clear()
buffer = get_log_buffer()
if buffer is not None:
self._rendered_upto = buffer.total_emitted
if self._on_clear is not None:
self._on_clear(self._rendered_upto)

def action_copy(self) -> None:
"""Copy visible retained log records since the last clear to the clipboard."""
Expand Down
84 changes: 84 additions & 0 deletions libs/code/tests/unit_tests/test_debug_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,48 @@ async def test_clear_view_key_empties_log_and_advances_pointer(self) -> None:
assert log.line_count == 0
assert screen._rendered_upto == buffer.total_emitted

async def test_clear_view_reports_cursor_via_on_clear(self) -> None:
logger.info("debug-console-on-clear-marker")
cleared: list[int] = []
app = _Harness()
async with app.run_test() as pilot:
screen = DebugConsoleScreen(_snapshot(), on_clear=cleared.append)
app.push_screen(screen)
await pilot.pause()
buffer = get_log_buffer()
assert buffer is not None

# Capture the cursor at clear time; the shared process-wide buffer
# may accrue records between the clear and a later re-read.
expected = buffer.total_emitted
await pilot.press("ctrl+l")
await pilot.pause()
assert cleared == [expected]

async def test_cleared_upto_seeds_render_cursor(self) -> None:
logger.info("debug-console-pre-clear-marker")
app = _Harness()
async with app.run_test() as pilot:
buffer = get_log_buffer()
assert buffer is not None
# Simulate a prior clear by seeding past everything emitted so far.
screen = DebugConsoleScreen(_snapshot(), cleared_upto=buffer.total_emitted)
app.push_screen(screen)
await pilot.pause()
log = screen.query_one("#debug-log", _DebugLogView)
assert not any(
"debug-console-pre-clear-marker" in record.message
for record in log.records
)

logger.info("debug-console-post-clear-marker")
screen._poll_logs()
await pilot.pause()
assert any(
"debug-console-post-clear-marker" in record.message
for record in log.records
)

async def test_copy_key_invokes_clipboard_with_retained_lines(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down Expand Up @@ -1186,6 +1228,48 @@ async def test_toggle_action_closes_open_console(self) -> None:
await pilot.pause()
assert not isinstance(app.screen, DebugConsoleScreen)

async def test_clear_persists_across_reopen(self) -> None:
logger.info("debug-console-persist-marker")
app = DeepAgentsApp(agent=MagicMock(), thread_id="thread-123")
async with app.run_test() as pilot:
await pilot.pause()
await pilot.press("ctrl+backslash")
await pilot.pause()
screen = cast("DebugConsoleScreen", app.screen)
log = screen.query_one("#debug-log", _DebugLogView)
assert any(
"debug-console-persist-marker" in record.message
for record in log.records
)

buffer = get_log_buffer()
assert buffer is not None
expected = buffer.total_emitted
await pilot.press("ctrl+l")
await pilot.pause()
assert app._debug_console_cleared_upto == expected

# A record emitted after the clear must survive the reopen; only the
# pre-clear tail is suppressed.
logger.info("debug-console-post-clear-marker")

# Close and reopen: the cleared records must not come back, but the
# post-clear record must appear.
await pilot.press("ctrl+backslash")
await pilot.pause()
await pilot.press("ctrl+backslash")
await pilot.pause()
reopened = cast("DebugConsoleScreen", app.screen)
reopened_log = reopened.query_one("#debug-log", _DebugLogView)
assert not any(
"debug-console-persist-marker" in record.message
for record in reopened_log.records
)
assert any(
"debug-console-post-clear-marker" in record.message
for record in reopened_log.records
)

async def test_debug_command_opens_console(self) -> None:
app = DeepAgentsApp(agent=MagicMock(), thread_id="thread-123")
async with app.run_test() as pilot:
Expand Down