Imported from japer-technology/forgejo-society (
FORGEJO-SOCIETY-INTRODUCTION/precursors/github-intelligence-overwatch/repo/githubification-OpenHands-CLI/AGENTS.md). Install upstream withnpx skills add japer-technology/forgejo-society --skill githubification-OpenHands-CLI. Copyright stays with the author.
Repository Guidelines
Repository Purpose
OpenHands CLI is a standalone terminal interface (Textual TUI) for interacting with the OpenHands agent.
This repo contains the current CLI UX, including the Textual TUI and a browser-served view via openhands web.
References
- Agent-sdk example: https://github.com/All-Hands-AI/agent-sdk/blob/main/examples/hello_world.py
- If you need to compare with upstream OpenHands code, use
$GITHUB_TOKENfor access.
Project Structure & Module Organization
openhands_cli/: Core CLI/TUI code (openhands_cli/entrypoint.py,openhands_cli/tui/,openhands_cli/auth/,openhands_cli/mcp/,openhands_cli/cloud/,openhands_cli/user_actions/,openhands_cli/conversations/,openhands_cli/theme.py, helpers inopenhands_cli/utils.py). Keep new modules snake_case and colocate tests.tests/: Pytest suite covering units, integration, and snapshot tests; mirrors source layout.tui_e2e/: tests for the PyInstaller-built executable.scripts/acp/: JSON-RPC and debug helpers for ACP development;hooks/: PyInstaller/runtime hooks.- Tooling & packaging:
Makefilefor common tasks,build.sh/build.pyfor PyInstaller artifacts,openhands-cli.specfor the frozen binary,uv.lockfor resolved deps. .agents/skills/: agent guidance for this repo.
Setup, Build, and Development Commands
This repository uses uv for dependency management and running tooling (such as in Makefile, CI workflows, and uv.lock). Avoid using pip install ... directly if possible.
-
install dependencies:
make install(runsuv sync) -
install dev dependencies:
make install-dev(runsuv sync --group dev) -
install pre-commit hooks:
uv run pre-commit install(included inmake build) -
build (sync + install hooks):
make build -
lint (all pre-commit hooks):
make lint -
format:
make format -
run the Textual TUI (interactive; prefer running inside tmux so you can detach with
Ctrl+b d):make run(oruv run openhands) -
run the Textual TUI (automation-friendly; use for agent-driven runs):
uv run openhands --exit-without-confirmation(quit withCtrl+Q;Ctrl+Cdoes not work once the TUI is running) -
run the browser-served web app (Textual
textual-serve):openhands web -
run the Docker-based OpenHands GUI server:
openhands serve -
run the ACP entrypoint:
uv run openhands-acp -
run unit/integration tests:
make test(for faster runs:uv run pytest -m "not integration" --ignore=tests/snapshots) -
run snapshot tests (Textual UI):
make test-snapshots(oruv run pytest tests/snapshots -v; use--snapshot-updatewhen updating snapshots) -
run binary tests:
make test-binary(oruv run pytest tui_e2e) -
run unit/integration + snapshot tests together:
make test-all -
build PyInstaller binaries:
./build.sh --install-pyinstaller
Development Guidelines
Linting Requirements
Before any commit, run make lint and only commit after it passes. Use make lint to run all pre-commit hooks on all files, and do it before every commit (not after) to avoid CI failures.
Typing Requirements
Prefer modern typing syntax (X | None over Optional[X]) in new code.
Documentation Guidelines
- Don’t add new root-level
.mdfiles or “summary updates” toREADME.mdunless explicitly requested (use thisAGENTS.mdfor repo guidance).
Coding Style & Naming Conventions
- Python 3.12, ruff formatting (88-char line limit, double quotes).
- Ruff enforced rules: pycodestyle, pyflakes, isort, pyupgrade, unused-arg checks (tests allow fixture-style args), and guards against mutable defaults.
- Keep modules/dirs snake_case; classes in CapWords; user-facing commands/flags kebab-case as in existing entrypoints.
- Type checking via
pyright(uv run pyright); prefer type hints on new functions and public interfaces.
Testing Guidelines
- Unit/integration tests live under
tests/(excludingtests/snapshots) and run viamake test. - Snapshot tests live under
tests/snapshots/and run viamake test-snapshots. - Binary tests live under
tui_e2e/and run viamake test-binary. - Pytest discovery: files
test_*.py, classesTest*, functionstest_*. Use@pytest.mark.integrationfor costly flows. - Match test locations to implementation (
tests/mirrorsopenhands_cli/); add fixtures intests/conftest.pywhen shared. - Run
make testbefore PRs; run snapshot/binary tests when relevant to the change.
Binary Tests with Mock LLM
- Binary tests in
tui_e2e/can usemock_llm_server.pyfor deterministic testing without real LLM calls. - The mock LLM server provides OpenAI-compatible endpoints with proper tool call format.
- Use
openai/gpt-4o-mockas the model name (litellm requires a provider prefix).
Snapshot Testing with pytest-textual-snapshot
The CLI uses pytest-textual-snapshot for visual regression testing of Textual UI components. Snapshots are SVG screenshots that capture the exact visual state of the application.
Running Snapshot Tests
# Run all snapshot tests
make test-snapshots
# or: uv run pytest tests/snapshots/ -v
# Update snapshots when intentional UI changes are made
uv run pytest tests/snapshots/ --snapshot-update
Snapshot Test Location
- Test files:
tests/snapshots/test_app_snapshots.py,tests/snapshots/test_visualizer_snapshots.py - Generated snapshots:
tests/snapshots/__snapshots__/test_app_snapshots/*.svg,tests/snapshots/__snapshots__/test_visualizer_snapshots/*.svg
Writing Snapshot Tests
Snapshot tests must be synchronous (not async). The snap_compare fixture handles async internally:
from textual.app import App, ComposeResult
from textual.widgets import Static, Footer
def test_my_widget(snap_compare):
"""Snapshot test for my widget."""
class MyTestApp(App):
def compose(self) -> ComposeResult:
yield Static("Content")
yield Footer()
assert snap_compare(MyTestApp(), terminal_size=(80, 24))
Using run_before for Setup
To interact with the app before taking a screenshot:
def test_with_interaction(snap_compare):
class MyApp(App):
def compose(self) -> ComposeResult:
yield InputField(id="input")
async def setup(pilot):
input_field = pilot.app.query_one(InputField)
input_field.input_widget.value = "Hello!"
await pilot.pause()
assert snap_compare(MyApp(), terminal_size=(80, 24), run_before=setup)
Using press for Key Simulation
def test_with_focus(snap_compare):
assert snap_compare(
MyApp(),
terminal_size=(80, 24),
press=["tab", "tab"], # Press tab twice to move focus
)
Viewing Snapshots Visually
To view the generated SVG snapshots in a browser:
-
Start a local HTTP server in the snapshots directory:
cd tests/snapshots/__snapshots__/test_app_snapshots python -m http.server 12000 -
Open in browser using the work host URL:
https://work-1-<id>.prod-runtime.all-hands.dev/<snapshot-name>.svgExample snapshot names:
TestExitModalSnapshots.test_exit_modal_initial_state.svgTestVisualizerSnapshots.test_multiple_actions_alignment.svg
-
Stop the server when done:
pkill -f "python -m http.server 12000"
Snapshot Best Practices
- Mock external dependencies so snapshots are deterministic.
- Always pass a fixed
terminal_size=(width, height). - Commit SVG snapshots.
- Review snapshot diffs carefully.
Commit & Pull Request Guidelines
- Follow the repo’s pattern:
<scope>: <concise message> (#NNN)(seegit log), where scope is the touched area (e.g.,auth,tui,fix). - Keep commits focused; include tests and formatting in the same change when practical.
- PRs should describe behavior changes, list key commands run (e.g., tests/build), link related issues, and include before/after notes or screenshots for UI/TUI updates.
- Check in
uv.lockchanges when dependency versions move; avoid committing secrets or local config.
Contribution standards (agents-first)
- Keep PRs minimally scoped; prefer multiple PRs over one large PR when it reduces risk and review load.
- Include tests for behavior changes (unit/integration/e2e as appropriate). If you can’t add tests, explain why and what manual verification you performed.
- For UI/TUI changes, snapshot tests are the preferred evidence. If snapshots aren't available/appropriate, include screenshots (and note the terminal size used).
- Before opening a PR, run this verification flow (and include the exact commands run in the PR description):
make lintmake test- If you touched ACP / binary executable code (e.g.,
tui_e2e/,openhands_cli/acp_impl/,openhands_cli/mcp/, auth/connection flow):make test-binary - If you touched TUI code (e.g.,
openhands_cli/tui/, widgets, styles, layout):make test-snapshots(use--snapshot-updateonly for intentional UI changes)
PR submission checklist
- Scope is minimal and focused on one change
- Tests added/updated for behavior changes (or PR explains why not)
-
make lint -
make test - (If ACP/binary executable touched)
make test-binary - (If TUI touched)
make test-snapshotsrun and snapshots updated/reviewed - PR description includes: what changed, why, commands run, and UI evidence (snapshots/screenshots)
Security & Configuration Tips
- Do not embed API keys or endpoints in code; rely on runtime configuration/env vars when integrating new services.
- When packaging, verify no sensitive files are included in
dist/; adjustopenhands-cli.specif new assets are added.
TUI State Management Architecture
The TUI uses a reactive state management pattern with clear separation of concerns. Key files are in openhands_cli/tui/core/.
Core Components
ConversationContainer (state.py) - Reactive state holder
- A Textual
Containerwidget that owns all conversation-related reactive properties - Properties include:
running,conversation_id,conversation_title,confirmation_policy,pending_action_count,elapsed_seconds,metrics - UI widgets bind to these properties via
data_bind()and auto-update when state changes - Provides thread-safe state update methods (e.g.,
set_running(),set_conversation_id()) - Composes the main UI hierarchy:
ScrollableContent+InputAreaContainer
ConversationManager (conversation_manager.py) - Message router
- A thin Textual
Containerthat listens to messages and delegates to controllers - Owns:
RunnerRegistry,ConfirmationPolicyService, and all controllers - Message handlers (
@on(MessageType)) route to appropriate controllers - Provides public API methods that post messages internally
Controllers - Single-responsibility business logic
UserMessageController- Handles user input, renders messages, queues/processes with runnerConversationCrudController- Creates new conversations, resets stateConversationSwitchController- Orchestrates switching (pause current, prepare new)ConfirmationFlowController- Shows confirmation panel, handles user decisions
RunnerFactory + RunnerRegistry - Runner lifecycle
RunnerFactory- CreatesConversationRunnerinstances with dependenciesRunnerRegistry- Caches runners by conversation_id, tracks current runner
Widget Hierarchy
OpenHandsApp
└── ConversationManager(Container) ← message router
└── ConversationContainer(#conversation_state) ← reactive state
├── ScrollableContent(#scroll_view) ← binds to conversation_id, pending_action_count
│ ├── SplashContent(#splash_content) ← binds to conversation_id
│ └── ... dynamically added conversation widgets
└── InputAreaContainer(#input_area) ← handles slash commands
├── WorkingStatusLine ← binds to running, elapsed_seconds
├── InputField ← binds to conversation_id, pending_action_count
└── InfoStatusLine ← binds to running, metrics
Data Flow
- User input →
InputFieldpostsUserInputSubmitted→ bubbles toConversationManager→UserMessageController.handle_user_message() - Slash commands →
InputFieldpostsSlashCommandSubmitted→InputAreaContainerroutes to command handlers → posts operation messages (e.g.,CreateConversation) - State changes → Controllers call
ConversationContainer.set_*()methods → reactive properties update → bound widgets auto-refresh - Cross-thread updates →
ConversationContainer._schedule_update()usescall_from_thread()for thread safety
Key Design Principles
- Reactive state: UI components bind to
ConversationContainerproperties viadata_bind(), auto-update on changes - Single source of truth:
ConversationContainerowns all conversation state - Thread safety: State updates use
call_from_thread()when called from background threads - Message-based communication: Components communicate via Textual messages that bubble up the widget tree
- Controller pattern: Business logic split into focused controllers,
ConversationManageris just a router