Skip to content

CLI Refactor to Textual - #686

Merged
vivek (vtrivedy) merged 13 commits into
masterfrom
cli-push
Jan 8, 2026
Merged

CLI Refactor to Textual#686
vivek (vtrivedy) merged 13 commits into
masterfrom
cli-push

Conversation

@vtrivedy

Copy link
Copy Markdown
Collaborator

Summary

Full Textual-based terminal UI replacing the Rich console implementation, with persistent session support via SQLite.

Features

Multi-line input - Enter to send, Ctrl+J for newlines
Fuzzy file completion - Type @ to autocomplete files
Slash commands - /help, /clear, /quit with autocomplete
Bash mode - Type !ls to run shell commands directly
Tool approval UI - y/n/a quick keys, Escape to cancel
Auto-approve mode - Shift+Tab to toggle
Session persistence - Resume conversations with -r

Usage

deepagents # New session
deepagents -r # Resume most recent
deepagents -r abc123 # Resume specific thread
deepagents threads list # List sessions
deepagents threads delete ID # Delete session

Changes

Added app.py, textual_adapter.py, and widgets/ for Textual UI
Added sessions.py for SQLite-backed thread persistence
Removed commands.py, execution.py (legacy Rich CLI)
Trimmed ui.py (~1,200 lines of dead code removed)

Tests

139 tests passing

@vtrivedy
vivek (vtrivedy) merged commit 2826939 into master Jan 8, 2026
13 checks passed
Marcelo5444 pushed a commit to Marcelo5444/deepagents that referenced this pull request Jul 30, 2026
### Summary
Full Textual-based terminal UI replacing the Rich console
implementation, with persistent session support via SQLite.

### Features
Multi-line input - Enter to send, Ctrl+J for newlines
Fuzzy file completion - Type @ to autocomplete files
Slash commands - /help, /clear, /quit with autocomplete
Bash mode - Type !ls to run shell commands directly
Tool approval UI - y/n/a quick keys, Escape to cancel
Auto-approve mode - Shift+Tab to toggle
Session persistence - Resume conversations with -r

### Usage
deepagents                    # New session
deepagents -r                 # Resume most recent
deepagents -r abc123          # Resume specific thread
deepagents threads list       # List sessions
deepagents threads delete ID  # Delete session

### Changes
Added app.py, textual_adapter.py, and widgets/ for Textual UI
Added sessions.py for SQLite-backed thread persistence
Removed commands.py, execution.py (legacy Rich CLI)
Trimmed ui.py (~1,200 lines of dead code removed)

### Tests
139 tests passing
Mason Daugherty (mdrxy) added a commit that referenced this pull request Aug 4, 2026
Interrupting a turn, or exiting while a background thread-cache refresh
is in flight, no longer strands an unawaited coroutine or an open SQLite
handle.

---

The unit suite emitted three families of warnings, all at teardown. Two
are ours and are fixed here; the third is upstream and is filtered.

### An unawaited turn coroutine

`_send_to_agent` handed `run_worker` an already-built `_run_agent_task`
coroutine. Textual never runs the work of a worker cancelled before its
first event-loop step, so that coroutine was finalized unawaited — a
`RuntimeWarning`, and (once interpreter teardown had gone far enough to
break the import machinery its cleanup relies on) an unraisable
`KeyError: '__import__'`. Passing a callable means the coroutine only
exists if the worker actually runs, so there is nothing to strand.
Several tests were closing that coroutine by hand purely to silence the
warning; those workarounds are gone, and a new test pins the callable
contract.

**Where it started:** #5196. Handing `run_worker` a coroutine dates all
the way back to the original Textual REPL (#686), but it was harmless
until something cancelled a worker before its first step. #5196 added
the recovery path for exactly that situation along with the tests that
exercise it. Its parent commit runs the app test module with zero `never
awaited` warnings; #5196 itself produces three.

### An unclosed SQLite handle

`aiosqlite` opens the database on its worker thread and hands the raw
`sqlite3.Connection` back through a future, recording it on the
connection only when the awaiting coroutine resumes. A cancel landing
anywhere in that window left the handle unreachable from the cleanup
that follows, so the garbage collector reported `ResourceWarning:
unclosed database`. There are two halves to the window, and both are now
covered:

- Cancelled while the worker is still opening, the library has no handle
recorded yet, so the cleanup it queues closes nothing. The session
module now records the handle from the worker thread the moment the
connector returns.
- Cancelled after the handle is delivered but before the coroutine
resumes, the library clears its own reference before that queued cleanup
can run — so it again closes nothing. The guard now also queues an
explicit close ahead of the library's cleanup, while the handle is still
reachable.

Both closes run on the thread that opened the handle, and closing twice
is a no-op, so neither disturbs a normal shutdown. `get_checkpointer`
builds its connection through the same helper rather than
`AsyncSqliteSaver.from_conn_string`, so it gets the same guard.

**Where it started:** #5174. The prewarm that reads the session database
has existed since #1481, but it ran once at startup, so it had normally
finished before anything cancelled it. #5174 re-fires it after every
turn, which reliably leaves a session-DB read in flight when a test app
exits. Counting handles that `aiosqlite` opened and never closed across
the goal-command tests: zero on the parent commit, fourteen on #5174.

### A `typing` deprecation from `google-genai`

`google.genai.types` builds a union alias out of
`typing._UnionGenericAlias`, which CPython 3.14 deprecates, and it fires
at import before any of this package's code runs. This one is not a
regression from any change here — `deepagents-code` has been tested on
the 3.14 leg since the package was created in #3027, and the warning
appears wherever a test imports the Google integration. It is tracked
upstream as
[googleapis/python-genai#1640](googleapis/python-genai#1640)
and still unfixed as of `google-genai` 2.13.0, so it is filtered
narrowly (message, category, and module) rather than worked around.

Made by [Open
SWE](https://openswe.vercel.app/agents/f42590ef-0fe4-0b9f-6880-1c48b28446d5)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants