hx-cli 0.1.9__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hx_cli-0.1.9 → hx_cli-0.2.0}/CHANGELOG.md +68 -1
- hx_cli-0.1.9/README.md → hx_cli-0.2.0/PKG-INFO +85 -9
- hx_cli-0.1.9/PKG-INFO → hx_cli-0.2.0/README.md +50 -44
- {hx_cli-0.1.9 → hx_cli-0.2.0}/install.sh +71 -28
- {hx_cli-0.1.9 → hx_cli-0.2.0}/pyproject.toml +11 -5
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/__init__.py +1 -1
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/config.py +10 -1
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/loop.py +15 -7
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/keys.py +13 -32
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/permissions/engine.py +10 -1
- hx_cli-0.2.0/src/hx/term/__init__.py +18 -0
- hx_cli-0.2.0/src/hx/term/ansi.py +391 -0
- hx_cli-0.2.0/src/hx/term/buffer.py +145 -0
- hx_cli-0.2.0/src/hx/term/component.py +141 -0
- hx_cli-0.2.0/src/hx/term/demo.py +150 -0
- hx_cli-0.2.0/src/hx/term/editor.py +283 -0
- hx_cli-0.2.0/src/hx/term/keydecode.py +296 -0
- hx_cli-0.2.0/src/hx/term/loop.py +247 -0
- hx_cli-0.2.0/src/hx/term/markdown.py +291 -0
- hx_cli-0.2.0/src/hx/term/primitives.py +315 -0
- hx_cli-0.2.0/src/hx/term/sanitize.py +88 -0
- hx_cli-0.2.0/src/hx/term/screen.py +525 -0
- hx_cli-0.2.0/src/hx/term/syntax.py +102 -0
- hx_cli-0.2.0/src/hx/term/terminal.py +457 -0
- hx_cli-0.2.0/src/hx/term/undo.py +63 -0
- hx_cli-0.2.0/src/hx/term/width.py +199 -0
- hx_cli-0.2.0/src/hx/term/word_nav.py +33 -0
- hx_cli-0.2.0/src/hx/tui/__init__.py +7 -0
- hx_cli-0.2.0/src/hx/tui/app.py +56 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/commands.py +125 -39
- hx_cli-0.2.0/src/hx/tui/format.py +70 -0
- hx_cli-0.2.0/src/hx/tui/glyphs.py +63 -0
- hx_cli-0.2.0/src/hx/tui/limits.py +40 -0
- hx_cli-0.2.0/src/hx/tui/paint.py +109 -0
- hx_cli-0.2.0/src/hx/tui/renderers.py +658 -0
- hx_cli-0.2.0/src/hx/tui/runtime.py +1148 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/theme.py +6 -46
- hx_cli-0.2.0/src/hx/tui/views/__init__.py +14 -0
- hx_cli-0.2.0/src/hx/tui/views/blocks.py +267 -0
- hx_cli-0.2.0/src/hx/tui/views/dialog.py +183 -0
- hx_cli-0.2.0/src/hx/tui/views/login.py +259 -0
- hx_cli-0.2.0/src/hx/tui/views/permission.py +286 -0
- hx_cli-0.2.0/src/hx/tui/views/pickers.py +419 -0
- hx_cli-0.2.0/src/hx/tui/views/prompt.py +497 -0
- hx_cli-0.2.0/src/hx/tui/views/status.py +326 -0
- hx_cli-0.2.0/src/hx/tui/views/transcript.py +342 -0
- hx_cli-0.2.0/tests/term/conftest.py +59 -0
- hx_cli-0.2.0/tests/term/test_ansi.py +190 -0
- hx_cli-0.2.0/tests/term/test_keydecode.py +233 -0
- hx_cli-0.2.0/tests/term/test_loop.py +147 -0
- hx_cli-0.2.0/tests/term/test_markdown.py +198 -0
- hx_cli-0.2.0/tests/term/test_primitives.py +206 -0
- hx_cli-0.2.0/tests/term/test_sanitize.py +77 -0
- hx_cli-0.2.0/tests/term/test_screen.py +673 -0
- hx_cli-0.2.0/tests/term/test_terminal.py +235 -0
- hx_cli-0.2.0/tests/term/test_width.py +109 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_git.py +6 -1
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_install.py +3 -1
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_readme.py +58 -8
- hx_cli-0.2.0/tests/tui/__init__.py +0 -0
- hx_cli-0.2.0/tests/tui/support.py +146 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_autocomplete.py +37 -28
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_effort.py +40 -28
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_keys.py +13 -14
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_login.py +95 -71
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_renderers.py +52 -30
- hx_cli-0.2.0/tests/tui/test_session.py +1123 -0
- hx_cli-0.2.0/tests/tui/views/__init__.py +0 -0
- hx_cli-0.2.0/tests/tui/views/test_blocks.py +247 -0
- hx_cli-0.2.0/tests/tui/views/test_permission.py +328 -0
- hx_cli-0.2.0/tests/tui/views/test_pickers.py +314 -0
- hx_cli-0.2.0/tests/tui/views/test_prompt.py +374 -0
- hx_cli-0.2.0/tests/tui/views/test_runtime.py +492 -0
- hx_cli-0.2.0/tests/tui/views/test_status.py +197 -0
- hx_cli-0.2.0/tests/tui/views/test_transcript.py +211 -0
- hx_cli-0.2.0/uv.lock +946 -0
- hx_cli-0.1.9/src/hx/tui/__init__.py +0 -1
- hx_cli-0.1.9/src/hx/tui/app.py +0 -1072
- hx_cli-0.1.9/src/hx/tui/hx.tcss +0 -240
- hx_cli-0.1.9/src/hx/tui/renderers.py +0 -579
- hx_cli-0.1.9/src/hx/tui/widgets/__init__.py +0 -1
- hx_cli-0.1.9/src/hx/tui/widgets/autocomplete.py +0 -105
- hx_cli-0.1.9/src/hx/tui/widgets/configure.py +0 -95
- hx_cli-0.1.9/src/hx/tui/widgets/diff.py +0 -25
- hx_cli-0.1.9/src/hx/tui/widgets/frame.py +0 -61
- hx_cli-0.1.9/src/hx/tui/widgets/header.py +0 -74
- hx_cli-0.1.9/src/hx/tui/widgets/hints.py +0 -75
- hx_cli-0.1.9/src/hx/tui/widgets/input.py +0 -460
- hx_cli-0.1.9/src/hx/tui/widgets/login.py +0 -175
- hx_cli-0.1.9/src/hx/tui/widgets/palette.py +0 -346
- hx_cli-0.1.9/src/hx/tui/widgets/permission.py +0 -243
- hx_cli-0.1.9/src/hx/tui/widgets/rule.py +0 -26
- hx_cli-0.1.9/src/hx/tui/widgets/selectable.py +0 -69
- hx_cli-0.1.9/src/hx/tui/widgets/statusbar.py +0 -259
- hx_cli-0.1.9/src/hx/tui/widgets/todos.py +0 -116
- hx_cli-0.1.9/src/hx/tui/widgets/transcript.py +0 -487
- hx_cli-0.1.9/src/hx/tui/widgets/working.py +0 -151
- hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_a_pending_permission_prompt.raw +0 -198
- hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_a_running_turn.raw +0 -192
- hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_a_turn_with_tool_blocks.raw +0 -194
- hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_the_completion_popup.raw +0 -191
- hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_the_idle_screen.raw +0 -190
- hx_cli-0.1.9/tests/tui/test_app.py +0 -1573
- hx_cli-0.1.9/tests/tui/test_frame.py +0 -82
- hx_cli-0.1.9/tests/tui/test_permission.py +0 -132
- hx_cli-0.1.9/tests/tui/test_selection.py +0 -189
- hx_cli-0.1.9/tests/tui/test_snapshots.py +0 -159
- hx_cli-0.1.9/tests/tui/test_statusbar.py +0 -162
- hx_cli-0.1.9/tests/tui/test_transcript.py +0 -129
- hx_cli-0.1.9/uv.lock +0 -1816
- {hx_cli-0.1.9 → hx_cli-0.2.0}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/.gitignore +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/agents/subagent.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/oauth/callback.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/store.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/cli.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/context.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/events.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/messages.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/session.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/title.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/usage.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/docs.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/git.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/mcp/client.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/net.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/paths.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/permissions/sandbox.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/base.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/models.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/openrouter.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/base.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/output.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/read.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/registry.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/task.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/websearch.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/write.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/agents/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/agents/test_subagent.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/test_codex_oauth.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/test_pkce.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/test_resolve.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/test_store.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/conftest.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_checkpoints.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_context.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_lateinject.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_loop.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_rewind.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_session.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_settings_layers.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_settings_migration.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_steering.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_title.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_usage.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/mcp/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/mcp/fixtures/echo_server.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/mcp/test_manager.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/permissions/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/permissions/test_engine.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/permissions/test_sandbox.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_catalogue.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_codex.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_codex_catalogue.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_openrouter.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_registry.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_responses_codec.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/skills/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.1.9/tests/tools → hx_cli-0.2.0/tests/term}/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_cli.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_docs.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_integration.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_live.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_live_codex.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_net.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_paths.py +0 -0
- {hx_cli-0.1.9/tests/tui → hx_cli-0.2.0/tests/tools}/__init__.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_background.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_bash.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_files.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_output.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_todo.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_websearch.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_theme_json.py +0 -0
|
@@ -28,6 +28,71 @@ the project follows [semantic versioning](https://semver.org/).
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## [0.2.0] - 2026-09-14
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- The interface is drawn into your terminal's own scrollback instead of taking
|
|
36
|
+
over the screen. Finished output belongs to the terminal: scroll it, select
|
|
37
|
+
it with the mouse, copy it, pipe it, and it is still there after HX exits.
|
|
38
|
+
`/fullscreen` takes the whole window back for anyone who preferred it that
|
|
39
|
+
way - the prompt pinned to the bottom row, the transcript scrolling above it
|
|
40
|
+
with `pgup`, `pgdn`, `ctrl+home` and `ctrl+end` - and writes the choice to
|
|
41
|
+
`tui.fullscreen` for the next session. On either screen the prompt and the
|
|
42
|
+
status bar sit on the bottom row from the first frame - a conversation that
|
|
43
|
+
does not fill the window is padded above the dock, not below it, and rows a
|
|
44
|
+
closing picker gives back open above it too. The slash-command and `@`-path
|
|
45
|
+
completion lists draw above the prompt for the same reason: under it, every
|
|
46
|
+
keystroke that changed the number of matches moved the prompt and the status
|
|
47
|
+
bar with it. Exiting erases the whole interface - banner, conversation and
|
|
48
|
+
dock - and puts the shell prompt back on the row HX started on, rather than
|
|
49
|
+
leaving a dead prompt box and a status bar on screen above it, from either
|
|
50
|
+
screen: quitting out of `/fullscreen` also takes away the frame that was
|
|
51
|
+
parked underneath it.
|
|
52
|
+
- Approving a tool call is now a framed prompt with one option per line, each
|
|
53
|
+
saying what it does - `always allow` states that it writes a rule to
|
|
54
|
+
`.hx/settings.local.json`. `y`/`s`/`a`/`n` still answer in one keystroke,
|
|
55
|
+
`esc` denies, and the arrow keys work for anyone who wants to read first.
|
|
56
|
+
The command shown is the one that will run, `$` and all. When two subagents
|
|
57
|
+
stop at an approval at once, the older one holds the keyboard and the other
|
|
58
|
+
says it is waiting rather than offering keys that would answer the first.
|
|
59
|
+
- Your theme reaches the whole interface. Links, list bullets, block quotes,
|
|
60
|
+
horizontal rules and inline code in the model's replies now follow the
|
|
61
|
+
palette instead of a hard-coded set of colours, and headings are visibly
|
|
62
|
+
different from each other.
|
|
63
|
+
- Multi-line output from `/help`, `/cost`, `/status` and the rest lines up
|
|
64
|
+
under its own first line instead of at two different indents.
|
|
65
|
+
- The todo list is a block in the conversation rather than a sidebar, and
|
|
66
|
+
`/todos` and `ctrl+t` show it. A scrollback-native interface has no second
|
|
67
|
+
column to put a sidebar in.
|
|
68
|
+
- Mouse selection is the terminal's, across the whole session including what
|
|
69
|
+
has scrolled off, rather than HX's own within the visible screen.
|
|
70
|
+
- Text HX did not write - a file a tool read, a command's output, the model's
|
|
71
|
+
reply, a paste into the prompt - is drawn as plain text. Escape sequences in
|
|
72
|
+
it are removed rather than sent on to your terminal, so a file cannot set
|
|
73
|
+
your clipboard, rename your window, hide a link behind different words, or
|
|
74
|
+
erase what is already on screen by being read.
|
|
75
|
+
- `/clear` now clears the screen and the scrollback above it, instead of
|
|
76
|
+
emptying the transcript and leaving the last session on screen. Terminals
|
|
77
|
+
that do not implement scrollback erasure - macOS Terminal.app - clear the
|
|
78
|
+
screen only.
|
|
79
|
+
- `/model`, the command palette and every other picker are sized to the
|
|
80
|
+
terminal rather than to a fixed eight rows, so a tall window shows a long
|
|
81
|
+
list instead of leaving two thirds of itself empty.
|
|
82
|
+
- `ctrl+z` suspends HX to the background and resuming redraws it; `ctrl+home`
|
|
83
|
+
and `ctrl+end` reach the ends of the conversation; `ctrl+_` undoes in the
|
|
84
|
+
prompt. All three were listed in `/help` and bound to nothing.
|
|
85
|
+
- `ctrl+p` and `ctrl+l` are named in the hints line under the prompt, which is
|
|
86
|
+
where the keys a session offers are supposed to be discoverable.
|
|
87
|
+
- A block drawn against the prompt no longer closes itself with a second rule,
|
|
88
|
+
which left a rule, a blank line and another rule under every picker and under
|
|
89
|
+
every approval. Whichever block ends up against the dock lets the dock's own
|
|
90
|
+
rule close it, and takes its own back as soon as something lands underneath.
|
|
91
|
+
- The installer now opens with a terminal boot animation that draws and lights
|
|
92
|
+
the HX mark, with a clean static version for CI and redirected output.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
31
96
|
## [0.1.9] - 2026-09-11
|
|
32
97
|
|
|
33
98
|
### Changed
|
|
@@ -325,7 +390,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
325
390
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
326
391
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
327
392
|
|
|
328
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.
|
|
393
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.0...HEAD
|
|
394
|
+
[0.2.0]: https://github.com/aletisunil/hx/compare/v0.1.9...v0.2.0
|
|
395
|
+
[0.1.9]: https://github.com/aletisunil/hx/compare/v0.1.8...v0.1.9
|
|
329
396
|
[0.1.8]: https://github.com/aletisunil/hx/compare/v0.1.7...v0.1.8
|
|
330
397
|
[0.1.7]: https://github.com/aletisunil/hx/compare/v0.1.6...v0.1.7
|
|
331
398
|
[0.1.6]: https://github.com/aletisunil/hx/compare/v0.1.5...v0.1.6
|
|
@@ -1,6 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hx-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: HX - a terminal coding agent
|
|
5
|
+
Project-URL: Homepage, https://github.com/aletisunil/hx
|
|
6
|
+
Project-URL: Issues, https://github.com/aletisunil/hx/issues
|
|
7
|
+
Author: Sunil Aleti
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: agent,cli,coding-agent,llm,openrouter,tui
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: click>=8.1
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
Requires-Dist: markdown-it-py>=3.0
|
|
20
|
+
Requires-Dist: pydantic>=2.7
|
|
21
|
+
Requires-Dist: pygments>=2.17
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: truststore>=0.10
|
|
24
|
+
Requires-Dist: wcwidth>=0.2.13
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
27
|
+
Requires-Dist: pyte>=0.8; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
32
|
+
Requires-Dist: types-pygments; extra == 'dev'
|
|
33
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
1
36
|
# HX
|
|
2
37
|
|
|
3
|
-
A terminal coding agent. Python core,
|
|
38
|
+
A terminal coding agent. Python core, its own terminal renderer, OpenRouter for
|
|
39
|
+
models.
|
|
4
40
|
|
|
5
41
|
HX runs in your project directory, reads and edits your code, runs commands in
|
|
6
42
|
a sandboxed shell, and shows you what every turn costs.
|
|
@@ -291,13 +327,13 @@ be rebound (see [Keybindings](#keybindings)).
|
|
|
291
327
|
| `ctrl+p` | command palette |
|
|
292
328
|
| `ctrl+l` | model picker |
|
|
293
329
|
| `ctrl+o` | expand tool output (also `ctrl+r`) |
|
|
294
|
-
| `ctrl+t` |
|
|
330
|
+
| `ctrl+t` | show the current plan |
|
|
295
331
|
| `ctrl+x` | copy the selected message to the clipboard |
|
|
296
332
|
| `ctrl+up` | previous message |
|
|
297
333
|
| `ctrl+down` | next message |
|
|
298
|
-
| `pgup` | scroll the transcript up |
|
|
299
|
-
| `pgdn` | scroll the transcript down |
|
|
300
|
-
| `ctrl+home` | jump to the start
|
|
334
|
+
| `pgup` | scroll the transcript up (fullscreen; otherwise your terminal scrolls) |
|
|
335
|
+
| `pgdn` | scroll the transcript down (fullscreen; otherwise your terminal scrolls) |
|
|
336
|
+
| `ctrl+home` | jump to the start: the viewport in fullscreen, the first message otherwise |
|
|
301
337
|
| `ctrl+end` | jump back to the newest output |
|
|
302
338
|
| `@path` | complete a file path |
|
|
303
339
|
| `!command` | run a shell command directly, no model turn |
|
|
@@ -309,7 +345,8 @@ The prompt is a readline-style editor: `ctrl+a`/`ctrl+e` for line start and end,
|
|
|
309
345
|
`ctrl+b`/`ctrl+f` by character, `alt+b`/`alt+f` by word, `ctrl+w` and `alt+d` to
|
|
310
346
|
kill a word, `ctrl+u` and `ctrl+k` to kill to the start or end of a line, then
|
|
311
347
|
`ctrl+y` to yank it back and `alt+y` to walk further down the kill ring.
|
|
312
|
-
`ctrl+
|
|
348
|
+
`ctrl+_` undoes, `ctrl+shift+z` redoes - `ctrl+z` belongs to the shell, and
|
|
349
|
+
suspends HX.
|
|
313
350
|
|
|
314
351
|
Typing `/` or `@` opens a completion list above the prompt; `tab` cycles it,
|
|
315
352
|
`enter` accepts, `esc` dismisses.
|
|
@@ -364,18 +401,55 @@ than being silently resolved.
|
|
|
364
401
|
| `/rewind` | go back to an earlier prompt, restoring the files HX changed |
|
|
365
402
|
| `/title [text]` | show or set this session's name |
|
|
366
403
|
| `/prompt` | the system prompt this session is running with |
|
|
367
|
-
| `/todos` |
|
|
404
|
+
| `/todos` | show the current plan |
|
|
368
405
|
| `/skills` | installed skills |
|
|
369
406
|
| `/agents` | subagent types |
|
|
370
407
|
| `/mcp` | server status |
|
|
371
408
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
372
409
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
373
410
|
| `/copy` | copy the last reply to the clipboard |
|
|
411
|
+
| `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
|
|
374
412
|
| `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
|
|
375
413
|
| `/init` | generate an `AGENTS.md` for the project |
|
|
376
414
|
| `/help` | list commands and keys |
|
|
377
415
|
| `/quit` | exit (also `/exit`, `/q`) |
|
|
378
416
|
|
|
417
|
+
### The two screens
|
|
418
|
+
|
|
419
|
+
By default HX draws into your terminal's own scrollback. Finished output belongs
|
|
420
|
+
to the terminal while the session runs: it scrolls it, selects it, copies it and
|
|
421
|
+
pipes it. Nothing is repainted above the fold, so a long session costs the same
|
|
422
|
+
to draw as a short one.
|
|
423
|
+
|
|
424
|
+
The prompt and the status bar still open on the bottom row of the window: a
|
|
425
|
+
conversation too short to fill the screen is padded above them rather than
|
|
426
|
+
below, so the dock stays where you last saw it instead of creeping down the
|
|
427
|
+
screen a block at a time. The padding is gone by the time the terminal starts
|
|
428
|
+
scrolling.
|
|
429
|
+
|
|
430
|
+
Leaving takes the whole interface off the screen: the banner, the conversation
|
|
431
|
+
and the dock are erased and your shell prompt comes back on the row HX started
|
|
432
|
+
on, the terminal as it was found. The erase stops at the top of the window -
|
|
433
|
+
lines that had already scrolled above it stay in your scrollback, because the
|
|
434
|
+
only sequence that reaches them takes everything the terminal remembers,
|
|
435
|
+
including what was there before HX ran. `/clear` is the command that asks for
|
|
436
|
+
that.
|
|
437
|
+
|
|
438
|
+
`/fullscreen` switches to the alternate screen instead - the whole window, the
|
|
439
|
+
prompt pinned to the bottom row, the transcript scrolling above it with `pgup`,
|
|
440
|
+
`pgdn`, `ctrl+home` and `ctrl+end`. The trade is the thing the default was
|
|
441
|
+
chosen for: the alternate screen is not scrollback, so your terminal cannot
|
|
442
|
+
scroll or select it, and it is gone when HX exits. `/fullscreen off` gives it
|
|
443
|
+
back, and either way the choice is written to `tui.fullscreen` for the next
|
|
444
|
+
session. Quitting from fullscreen leaves the same clean terminal as quitting
|
|
445
|
+
from the default screen: the alternate screen goes, and so does the frame HX
|
|
446
|
+
had drawn underneath it before the switch.
|
|
447
|
+
|
|
448
|
+
`/clear` wipes the screen and the scrollback above it. Terminals that do not
|
|
449
|
+
implement scrollback erasure - macOS Terminal.app is the one people hit - clear
|
|
450
|
+
the screen only, and the old conversation stays above it until you clear it the
|
|
451
|
+
terminal's own way (`cmd+k`).
|
|
452
|
+
|
|
379
453
|
### Selecting and copying text
|
|
380
454
|
|
|
381
455
|
Drag to select and `ctrl+c` to copy. HX copies through `pbcopy`, `wl-copy` or
|
|
@@ -489,7 +563,8 @@ along with the `.hx/.gitignore` it added to hide it.
|
|
|
489
563
|
},
|
|
490
564
|
|
|
491
565
|
"tui": {
|
|
492
|
-
"enterWhileBusy": "queue"
|
|
566
|
+
"enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
|
|
567
|
+
"fullscreen": false // true takes the whole window (see /fullscreen)
|
|
493
568
|
}
|
|
494
569
|
}
|
|
495
570
|
```
|
|
@@ -761,7 +836,8 @@ src/hx/
|
|
|
761
836
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
762
837
|
skills/ agents/ mcp/
|
|
763
838
|
keys.py keybinding registry: ids, defaults, descriptions, user overrides
|
|
764
|
-
|
|
839
|
+
term/ the renderer: raw mode, input decoding, the differ, the screen
|
|
840
|
+
tui/ views, commands, per-tool renderers, themes
|
|
765
841
|
themes/ the shipped palettes, as JSON
|
|
766
842
|
```
|
|
767
843
|
|
|
@@ -1,41 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: hx-cli
|
|
3
|
-
Version: 0.1.9
|
|
4
|
-
Summary: HX - a terminal coding agent
|
|
5
|
-
Project-URL: Homepage, https://github.com/aletisunil/hx
|
|
6
|
-
Project-URL: Issues, https://github.com/aletisunil/hx/issues
|
|
7
|
-
Author: Sunil Aleti
|
|
8
|
-
License: MIT
|
|
9
|
-
Keywords: agent,cli,coding-agent,llm,openrouter,tui
|
|
10
|
-
Classifier: Development Status :: 3 - Alpha
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
-
Requires-Python: >=3.11
|
|
17
|
-
Requires-Dist: click>=8.1
|
|
18
|
-
Requires-Dist: httpx>=0.27
|
|
19
|
-
Requires-Dist: pydantic>=2.7
|
|
20
|
-
Requires-Dist: pyyaml>=6.0
|
|
21
|
-
Requires-Dist: rich>=13.7
|
|
22
|
-
Requires-Dist: textual>=0.85
|
|
23
|
-
Requires-Dist: truststore>=0.10
|
|
24
|
-
Provides-Extra: dev
|
|
25
|
-
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
26
|
-
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
-
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
28
|
-
Requires-Dist: pytest-textual-snapshot>=1.0; extra == 'dev'
|
|
29
|
-
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
30
|
-
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
31
|
-
Requires-Dist: textual-dev>=1.5; extra == 'dev'
|
|
32
|
-
Requires-Dist: types-pygments; extra == 'dev'
|
|
33
|
-
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
34
|
-
Description-Content-Type: text/markdown
|
|
35
|
-
|
|
36
1
|
# HX
|
|
37
2
|
|
|
38
|
-
A terminal coding agent. Python core,
|
|
3
|
+
A terminal coding agent. Python core, its own terminal renderer, OpenRouter for
|
|
4
|
+
models.
|
|
39
5
|
|
|
40
6
|
HX runs in your project directory, reads and edits your code, runs commands in
|
|
41
7
|
a sandboxed shell, and shows you what every turn costs.
|
|
@@ -326,13 +292,13 @@ be rebound (see [Keybindings](#keybindings)).
|
|
|
326
292
|
| `ctrl+p` | command palette |
|
|
327
293
|
| `ctrl+l` | model picker |
|
|
328
294
|
| `ctrl+o` | expand tool output (also `ctrl+r`) |
|
|
329
|
-
| `ctrl+t` |
|
|
295
|
+
| `ctrl+t` | show the current plan |
|
|
330
296
|
| `ctrl+x` | copy the selected message to the clipboard |
|
|
331
297
|
| `ctrl+up` | previous message |
|
|
332
298
|
| `ctrl+down` | next message |
|
|
333
|
-
| `pgup` | scroll the transcript up |
|
|
334
|
-
| `pgdn` | scroll the transcript down |
|
|
335
|
-
| `ctrl+home` | jump to the start
|
|
299
|
+
| `pgup` | scroll the transcript up (fullscreen; otherwise your terminal scrolls) |
|
|
300
|
+
| `pgdn` | scroll the transcript down (fullscreen; otherwise your terminal scrolls) |
|
|
301
|
+
| `ctrl+home` | jump to the start: the viewport in fullscreen, the first message otherwise |
|
|
336
302
|
| `ctrl+end` | jump back to the newest output |
|
|
337
303
|
| `@path` | complete a file path |
|
|
338
304
|
| `!command` | run a shell command directly, no model turn |
|
|
@@ -344,7 +310,8 @@ The prompt is a readline-style editor: `ctrl+a`/`ctrl+e` for line start and end,
|
|
|
344
310
|
`ctrl+b`/`ctrl+f` by character, `alt+b`/`alt+f` by word, `ctrl+w` and `alt+d` to
|
|
345
311
|
kill a word, `ctrl+u` and `ctrl+k` to kill to the start or end of a line, then
|
|
346
312
|
`ctrl+y` to yank it back and `alt+y` to walk further down the kill ring.
|
|
347
|
-
`ctrl+
|
|
313
|
+
`ctrl+_` undoes, `ctrl+shift+z` redoes - `ctrl+z` belongs to the shell, and
|
|
314
|
+
suspends HX.
|
|
348
315
|
|
|
349
316
|
Typing `/` or `@` opens a completion list above the prompt; `tab` cycles it,
|
|
350
317
|
`enter` accepts, `esc` dismisses.
|
|
@@ -399,18 +366,55 @@ than being silently resolved.
|
|
|
399
366
|
| `/rewind` | go back to an earlier prompt, restoring the files HX changed |
|
|
400
367
|
| `/title [text]` | show or set this session's name |
|
|
401
368
|
| `/prompt` | the system prompt this session is running with |
|
|
402
|
-
| `/todos` |
|
|
369
|
+
| `/todos` | show the current plan |
|
|
403
370
|
| `/skills` | installed skills |
|
|
404
371
|
| `/agents` | subagent types |
|
|
405
372
|
| `/mcp` | server status |
|
|
406
373
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
407
374
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
408
375
|
| `/copy` | copy the last reply to the clipboard |
|
|
376
|
+
| `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
|
|
409
377
|
| `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
|
|
410
378
|
| `/init` | generate an `AGENTS.md` for the project |
|
|
411
379
|
| `/help` | list commands and keys |
|
|
412
380
|
| `/quit` | exit (also `/exit`, `/q`) |
|
|
413
381
|
|
|
382
|
+
### The two screens
|
|
383
|
+
|
|
384
|
+
By default HX draws into your terminal's own scrollback. Finished output belongs
|
|
385
|
+
to the terminal while the session runs: it scrolls it, selects it, copies it and
|
|
386
|
+
pipes it. Nothing is repainted above the fold, so a long session costs the same
|
|
387
|
+
to draw as a short one.
|
|
388
|
+
|
|
389
|
+
The prompt and the status bar still open on the bottom row of the window: a
|
|
390
|
+
conversation too short to fill the screen is padded above them rather than
|
|
391
|
+
below, so the dock stays where you last saw it instead of creeping down the
|
|
392
|
+
screen a block at a time. The padding is gone by the time the terminal starts
|
|
393
|
+
scrolling.
|
|
394
|
+
|
|
395
|
+
Leaving takes the whole interface off the screen: the banner, the conversation
|
|
396
|
+
and the dock are erased and your shell prompt comes back on the row HX started
|
|
397
|
+
on, the terminal as it was found. The erase stops at the top of the window -
|
|
398
|
+
lines that had already scrolled above it stay in your scrollback, because the
|
|
399
|
+
only sequence that reaches them takes everything the terminal remembers,
|
|
400
|
+
including what was there before HX ran. `/clear` is the command that asks for
|
|
401
|
+
that.
|
|
402
|
+
|
|
403
|
+
`/fullscreen` switches to the alternate screen instead - the whole window, the
|
|
404
|
+
prompt pinned to the bottom row, the transcript scrolling above it with `pgup`,
|
|
405
|
+
`pgdn`, `ctrl+home` and `ctrl+end`. The trade is the thing the default was
|
|
406
|
+
chosen for: the alternate screen is not scrollback, so your terminal cannot
|
|
407
|
+
scroll or select it, and it is gone when HX exits. `/fullscreen off` gives it
|
|
408
|
+
back, and either way the choice is written to `tui.fullscreen` for the next
|
|
409
|
+
session. Quitting from fullscreen leaves the same clean terminal as quitting
|
|
410
|
+
from the default screen: the alternate screen goes, and so does the frame HX
|
|
411
|
+
had drawn underneath it before the switch.
|
|
412
|
+
|
|
413
|
+
`/clear` wipes the screen and the scrollback above it. Terminals that do not
|
|
414
|
+
implement scrollback erasure - macOS Terminal.app is the one people hit - clear
|
|
415
|
+
the screen only, and the old conversation stays above it until you clear it the
|
|
416
|
+
terminal's own way (`cmd+k`).
|
|
417
|
+
|
|
414
418
|
### Selecting and copying text
|
|
415
419
|
|
|
416
420
|
Drag to select and `ctrl+c` to copy. HX copies through `pbcopy`, `wl-copy` or
|
|
@@ -524,7 +528,8 @@ along with the `.hx/.gitignore` it added to hide it.
|
|
|
524
528
|
},
|
|
525
529
|
|
|
526
530
|
"tui": {
|
|
527
|
-
"enterWhileBusy": "queue"
|
|
531
|
+
"enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
|
|
532
|
+
"fullscreen": false // true takes the whole window (see /fullscreen)
|
|
528
533
|
}
|
|
529
534
|
}
|
|
530
535
|
```
|
|
@@ -796,7 +801,8 @@ src/hx/
|
|
|
796
801
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
797
802
|
skills/ agents/ mcp/
|
|
798
803
|
keys.py keybinding registry: ids, defaults, descriptions, user overrides
|
|
799
|
-
|
|
804
|
+
term/ the renderer: raw mode, input decoding, the differ, the screen
|
|
805
|
+
tui/ views, commands, per-tool renderers, themes
|
|
800
806
|
themes/ the shipped palettes, as JSON
|
|
801
807
|
```
|
|
802
808
|
|
|
@@ -28,43 +28,86 @@ should_animate() {
|
|
|
28
28
|
[ -t 1 ] && [ -z "${HX_NO_ANIMATION:-}" ] && [ -z "${CI:-}" ]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
paint_logo_row() {
|
|
32
|
+
row=$1
|
|
33
|
+
text=$2
|
|
34
|
+
visible=$3
|
|
35
|
+
highlight=$4
|
|
36
|
+
clear_prefix=$5
|
|
37
|
+
|
|
38
|
+
if [ "$visible" -ne 0 ] && [ "$row" -gt "$visible" ]; then
|
|
39
|
+
text=
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if [ "$highlight" -eq 0 ]; then
|
|
43
|
+
colour='\033[0;36m'
|
|
44
|
+
elif [ "$row" -eq "$highlight" ]; then
|
|
45
|
+
colour='\033[1;96m'
|
|
46
|
+
else
|
|
47
|
+
colour='\033[2;36m'
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
printf '%b%b%s\033[0m\n' "$clear_prefix" "$colour" "$text"
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
render_logo() {
|
|
32
|
-
|
|
54
|
+
# visible=0 renders the whole logo. highlight=0 uses the settled colour.
|
|
55
|
+
visible=${1:-0}
|
|
56
|
+
highlight=${2:-0}
|
|
33
57
|
clear_prefix=
|
|
34
|
-
[ "${
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
if [ "${3:-}" = clear ]; then
|
|
59
|
+
clear_prefix='\033[2K\r'
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
paint_logo_row 1 ' __ __ _ __' "$visible" "$highlight" "$clear_prefix"
|
|
63
|
+
paint_logo_row 2 ' / / / / | |/ /' "$visible" "$highlight" "$clear_prefix"
|
|
64
|
+
paint_logo_row 3 ' / /_/ / | /' "$visible" "$highlight" "$clear_prefix"
|
|
65
|
+
paint_logo_row 4 ' / __ / / |' "$visible" "$highlight" "$clear_prefix"
|
|
66
|
+
paint_logo_row 5 '/_/ /_/ /_/|_|' "$visible" "$highlight" "$clear_prefix"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
render_boot_prompt() {
|
|
70
|
+
typed=$1
|
|
71
|
+
clear_prefix='\033[2K\r'
|
|
72
|
+
|
|
73
|
+
printf '%b\n' "$clear_prefix"
|
|
74
|
+
printf '%b\n' "$clear_prefix"
|
|
75
|
+
printf '%b\033[2;36m > \033[1;96m%s\033[0;36m_\033[0m\n' "$clear_prefix" "$typed"
|
|
76
|
+
printf '%b\n' "$clear_prefix"
|
|
77
|
+
printf '%b\n' "$clear_prefix"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
rewind_logo() {
|
|
81
|
+
printf '\033[5A'
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
animate_logo() {
|
|
46
85
|
if ! should_animate; then
|
|
47
|
-
render_logo
|
|
86
|
+
render_logo
|
|
48
87
|
return
|
|
49
88
|
fi
|
|
50
89
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
# Type the command, draw the mark one scanline at a time, then bounce a
|
|
91
|
+
# highlight back through it. Every frame is five rows so redraws never jump.
|
|
92
|
+
for typed in '' h hx; do
|
|
93
|
+
render_boot_prompt "$typed"
|
|
94
|
+
sleep 0.055
|
|
95
|
+
rewind_logo
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
for row in 1 2 3 4 5; do
|
|
99
|
+
render_logo "$row" "$row" clear
|
|
100
|
+
sleep 0.045
|
|
101
|
+
rewind_logo
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
for row in 4 3 2 1; do
|
|
105
|
+
render_logo 0 "$row" clear
|
|
106
|
+
sleep 0.035
|
|
107
|
+
rewind_logo
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
render_logo 0 0 clear
|
|
68
111
|
}
|
|
69
112
|
|
|
70
113
|
stream_text() {
|
|
@@ -20,8 +20,6 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.14",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"textual>=0.85",
|
|
24
|
-
"rich>=13.7",
|
|
25
23
|
"httpx>=0.27",
|
|
26
24
|
"pydantic>=2.7",
|
|
27
25
|
"pyyaml>=6.0",
|
|
@@ -29,6 +27,13 @@ dependencies = [
|
|
|
29
27
|
# System trust store, so a corporate proxy CA the machine already trusts is
|
|
30
28
|
# enough - certifi alone is why `hx` fails on networks where browsers work.
|
|
31
29
|
"truststore>=0.10",
|
|
30
|
+
# Cell widths for the terminal renderer. Pure Python, tables generated from
|
|
31
|
+
# the Unicode data files - the one thing hx.term does not hand-roll.
|
|
32
|
+
"wcwidth>=0.2.13",
|
|
33
|
+
# Syntax highlighting. Was transitive through rich; now asked for directly.
|
|
34
|
+
"pygments>=2.17",
|
|
35
|
+
# CommonMark parsing. The renderer is ours; the parser is not worth writing.
|
|
36
|
+
"markdown-it-py>=3.0",
|
|
32
37
|
]
|
|
33
38
|
|
|
34
39
|
[project.optional-dependencies]
|
|
@@ -38,10 +43,11 @@ dev = [
|
|
|
38
43
|
"pytest-cov>=5.0",
|
|
39
44
|
"ruff>=0.6",
|
|
40
45
|
"mypy>=1.11",
|
|
41
|
-
"textual-dev>=1.5",
|
|
42
46
|
"types-PyYAML",
|
|
43
47
|
"types-Pygments",
|
|
44
|
-
|
|
48
|
+
# A VT emulator, so tests can assert what the differ actually paints rather
|
|
49
|
+
# than what it claims to have written.
|
|
50
|
+
"pyte>=0.8",
|
|
45
51
|
]
|
|
46
52
|
|
|
47
53
|
[project.scripts]
|
|
@@ -90,7 +96,7 @@ disallow_untyped_defs = true
|
|
|
90
96
|
plugins = []
|
|
91
97
|
|
|
92
98
|
[[tool.mypy.overrides]]
|
|
93
|
-
module = ["
|
|
99
|
+
module = ["pyte.*", "wcwidth.*", "markdown_it.*"]
|
|
94
100
|
ignore_missing_imports = true
|
|
95
101
|
|
|
96
102
|
[tool.pytest.ini_options]
|
|
@@ -141,6 +141,14 @@ class TuiSettings:
|
|
|
141
141
|
"""Queueing is the default because it cannot surprise anyone: a steer cuts
|
|
142
142
|
off the model mid-sentence, which is worth asking for deliberately."""
|
|
143
143
|
|
|
144
|
+
fullscreen: bool = False
|
|
145
|
+
"""Draw into the alternate screen, with the prompt pinned to the bottom.
|
|
146
|
+
|
|
147
|
+
Off by default: in the normal screen the transcript is the terminal's own
|
|
148
|
+
scrollback, which it can scroll, select, copy and keep after HX exits.
|
|
149
|
+
``/fullscreen`` turns it on for a session and writes it here for the next
|
|
150
|
+
one, for anyone who would rather have the window back."""
|
|
151
|
+
|
|
144
152
|
|
|
145
153
|
@dataclass(frozen=True, slots=True)
|
|
146
154
|
class Settings:
|
|
@@ -397,6 +405,7 @@ def _build_settings(data: dict[str, Any], cwd: Path) -> Settings:
|
|
|
397
405
|
enter_while_busy = EnterWhileBusy(busy_raw)
|
|
398
406
|
except ValueError as exc:
|
|
399
407
|
raise ConfigError(f"unknown tui.enterWhileBusy: {busy_raw!r}") from exc
|
|
408
|
+
fullscreen = bool(tui.get("fullscreen", False))
|
|
400
409
|
|
|
401
410
|
return Settings(
|
|
402
411
|
cwd=cwd,
|
|
@@ -447,7 +456,7 @@ def _build_settings(data: dict[str, Any], cwd: Path) -> Settings:
|
|
|
447
456
|
system=prompt.get("system"),
|
|
448
457
|
append=tuple(_as_list(prompt.get("append", ()))),
|
|
449
458
|
),
|
|
450
|
-
tui=TuiSettings(enter_while_busy=enter_while_busy),
|
|
459
|
+
tui=TuiSettings(enter_while_busy=enter_while_busy, fullscreen=fullscreen),
|
|
451
460
|
theme=data.get("theme", "dark"),
|
|
452
461
|
quiet_startup=bool(data.get("quietStartup", data.get("quiet_startup", False))),
|
|
453
462
|
telemetry=bool(data.get("telemetry", False)),
|
|
@@ -609,13 +609,15 @@ class AgentLoop:
|
|
|
609
609
|
from hx.permissions.engine import PermissionRequest
|
|
610
610
|
|
|
611
611
|
tool = self.tools.get(call.name)
|
|
612
|
+
detail, detail_kind = _permission_detail(call)
|
|
612
613
|
request = PermissionRequest(
|
|
613
614
|
tool_name=call.name,
|
|
614
615
|
specifier=tool.permission_specifier(call.input),
|
|
615
616
|
params=call.input,
|
|
616
617
|
mutating=tool.mutating,
|
|
617
618
|
description=f"{call.name}({_brief(call.input)})",
|
|
618
|
-
detail=
|
|
619
|
+
detail=detail,
|
|
620
|
+
detail_kind=detail_kind,
|
|
619
621
|
origin=self.origin,
|
|
620
622
|
)
|
|
621
623
|
allowed, reason = await self.permissions.request(
|
|
@@ -711,14 +713,19 @@ def _parse_tool_input(raw: str | None) -> dict[str, Any]:
|
|
|
711
713
|
return parsed if isinstance(parsed, dict) else {}
|
|
712
714
|
|
|
713
715
|
|
|
714
|
-
def _permission_detail(call: ToolUseBlock) -> str:
|
|
715
|
-
"""What the approval
|
|
716
|
+
def _permission_detail(call: ToolUseBlock) -> tuple[str, str]:
|
|
717
|
+
"""What the approval shows, and what kind of thing it is.
|
|
716
718
|
|
|
717
719
|
For an edit this previews the change against the file on disk, so the user
|
|
718
720
|
approves a diff rather than a filename.
|
|
721
|
+
|
|
722
|
+
The kind is returned rather than left for the UI to infer. Guessing from
|
|
723
|
+
the text meant a file whose first line was ``---`` - YAML front matter, a
|
|
724
|
+
markdown rule - was treated as a diff and run through a painter that
|
|
725
|
+
strips exactly those lines, leaving the user approving a blank space.
|
|
719
726
|
"""
|
|
720
727
|
if call.name == "Bash":
|
|
721
|
-
return str(call.input.get("command", ""))
|
|
728
|
+
return str(call.input.get("command", "")), "command"
|
|
722
729
|
|
|
723
730
|
if call.name in {"Edit", "Write"} and (raw_path := call.input.get("file_path")):
|
|
724
731
|
from pathlib import Path
|
|
@@ -736,12 +743,13 @@ def _permission_detail(call: ToolUseBlock) -> str:
|
|
|
736
743
|
after = after.replace(
|
|
737
744
|
edit.old_string, edit.new_string, -1 if edit.replace_all else 1
|
|
738
745
|
)
|
|
739
|
-
|
|
746
|
+
diff = unified_diff(before, after, str(path))
|
|
747
|
+
return (diff, "diff") if diff else (f"{path} (no change)", "text")
|
|
740
748
|
except Exception:
|
|
741
749
|
# Previewing is best effort; never block the prompt on it.
|
|
742
|
-
return str(raw_path)
|
|
750
|
+
return str(raw_path), "text"
|
|
743
751
|
|
|
744
|
-
return _brief(call.input, limit=400)
|
|
752
|
+
return _brief(call.input, limit=400), "text"
|
|
745
753
|
|
|
746
754
|
|
|
747
755
|
def _brief(params: dict[str, Any], limit: int = 80) -> str:
|