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.
Files changed (233) hide show
  1. {hx_cli-0.1.9 → hx_cli-0.2.0}/CHANGELOG.md +68 -1
  2. hx_cli-0.1.9/README.md → hx_cli-0.2.0/PKG-INFO +85 -9
  3. hx_cli-0.1.9/PKG-INFO → hx_cli-0.2.0/README.md +50 -44
  4. {hx_cli-0.1.9 → hx_cli-0.2.0}/install.sh +71 -28
  5. {hx_cli-0.1.9 → hx_cli-0.2.0}/pyproject.toml +11 -5
  6. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/__init__.py +1 -1
  7. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/config.py +10 -1
  8. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/loop.py +15 -7
  9. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/keys.py +13 -32
  10. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/permissions/engine.py +10 -1
  11. hx_cli-0.2.0/src/hx/term/__init__.py +18 -0
  12. hx_cli-0.2.0/src/hx/term/ansi.py +391 -0
  13. hx_cli-0.2.0/src/hx/term/buffer.py +145 -0
  14. hx_cli-0.2.0/src/hx/term/component.py +141 -0
  15. hx_cli-0.2.0/src/hx/term/demo.py +150 -0
  16. hx_cli-0.2.0/src/hx/term/editor.py +283 -0
  17. hx_cli-0.2.0/src/hx/term/keydecode.py +296 -0
  18. hx_cli-0.2.0/src/hx/term/loop.py +247 -0
  19. hx_cli-0.2.0/src/hx/term/markdown.py +291 -0
  20. hx_cli-0.2.0/src/hx/term/primitives.py +315 -0
  21. hx_cli-0.2.0/src/hx/term/sanitize.py +88 -0
  22. hx_cli-0.2.0/src/hx/term/screen.py +525 -0
  23. hx_cli-0.2.0/src/hx/term/syntax.py +102 -0
  24. hx_cli-0.2.0/src/hx/term/terminal.py +457 -0
  25. hx_cli-0.2.0/src/hx/term/undo.py +63 -0
  26. hx_cli-0.2.0/src/hx/term/width.py +199 -0
  27. hx_cli-0.2.0/src/hx/term/word_nav.py +33 -0
  28. hx_cli-0.2.0/src/hx/tui/__init__.py +7 -0
  29. hx_cli-0.2.0/src/hx/tui/app.py +56 -0
  30. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/commands.py +125 -39
  31. hx_cli-0.2.0/src/hx/tui/format.py +70 -0
  32. hx_cli-0.2.0/src/hx/tui/glyphs.py +63 -0
  33. hx_cli-0.2.0/src/hx/tui/limits.py +40 -0
  34. hx_cli-0.2.0/src/hx/tui/paint.py +109 -0
  35. hx_cli-0.2.0/src/hx/tui/renderers.py +658 -0
  36. hx_cli-0.2.0/src/hx/tui/runtime.py +1148 -0
  37. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/theme.py +6 -46
  38. hx_cli-0.2.0/src/hx/tui/views/__init__.py +14 -0
  39. hx_cli-0.2.0/src/hx/tui/views/blocks.py +267 -0
  40. hx_cli-0.2.0/src/hx/tui/views/dialog.py +183 -0
  41. hx_cli-0.2.0/src/hx/tui/views/login.py +259 -0
  42. hx_cli-0.2.0/src/hx/tui/views/permission.py +286 -0
  43. hx_cli-0.2.0/src/hx/tui/views/pickers.py +419 -0
  44. hx_cli-0.2.0/src/hx/tui/views/prompt.py +497 -0
  45. hx_cli-0.2.0/src/hx/tui/views/status.py +326 -0
  46. hx_cli-0.2.0/src/hx/tui/views/transcript.py +342 -0
  47. hx_cli-0.2.0/tests/term/conftest.py +59 -0
  48. hx_cli-0.2.0/tests/term/test_ansi.py +190 -0
  49. hx_cli-0.2.0/tests/term/test_keydecode.py +233 -0
  50. hx_cli-0.2.0/tests/term/test_loop.py +147 -0
  51. hx_cli-0.2.0/tests/term/test_markdown.py +198 -0
  52. hx_cli-0.2.0/tests/term/test_primitives.py +206 -0
  53. hx_cli-0.2.0/tests/term/test_sanitize.py +77 -0
  54. hx_cli-0.2.0/tests/term/test_screen.py +673 -0
  55. hx_cli-0.2.0/tests/term/test_terminal.py +235 -0
  56. hx_cli-0.2.0/tests/term/test_width.py +109 -0
  57. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_git.py +6 -1
  58. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_install.py +3 -1
  59. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_readme.py +58 -8
  60. hx_cli-0.2.0/tests/tui/__init__.py +0 -0
  61. hx_cli-0.2.0/tests/tui/support.py +146 -0
  62. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_autocomplete.py +37 -28
  63. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_effort.py +40 -28
  64. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_keys.py +13 -14
  65. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_login.py +95 -71
  66. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_renderers.py +52 -30
  67. hx_cli-0.2.0/tests/tui/test_session.py +1123 -0
  68. hx_cli-0.2.0/tests/tui/views/__init__.py +0 -0
  69. hx_cli-0.2.0/tests/tui/views/test_blocks.py +247 -0
  70. hx_cli-0.2.0/tests/tui/views/test_permission.py +328 -0
  71. hx_cli-0.2.0/tests/tui/views/test_pickers.py +314 -0
  72. hx_cli-0.2.0/tests/tui/views/test_prompt.py +374 -0
  73. hx_cli-0.2.0/tests/tui/views/test_runtime.py +492 -0
  74. hx_cli-0.2.0/tests/tui/views/test_status.py +197 -0
  75. hx_cli-0.2.0/tests/tui/views/test_transcript.py +211 -0
  76. hx_cli-0.2.0/uv.lock +946 -0
  77. hx_cli-0.1.9/src/hx/tui/__init__.py +0 -1
  78. hx_cli-0.1.9/src/hx/tui/app.py +0 -1072
  79. hx_cli-0.1.9/src/hx/tui/hx.tcss +0 -240
  80. hx_cli-0.1.9/src/hx/tui/renderers.py +0 -579
  81. hx_cli-0.1.9/src/hx/tui/widgets/__init__.py +0 -1
  82. hx_cli-0.1.9/src/hx/tui/widgets/autocomplete.py +0 -105
  83. hx_cli-0.1.9/src/hx/tui/widgets/configure.py +0 -95
  84. hx_cli-0.1.9/src/hx/tui/widgets/diff.py +0 -25
  85. hx_cli-0.1.9/src/hx/tui/widgets/frame.py +0 -61
  86. hx_cli-0.1.9/src/hx/tui/widgets/header.py +0 -74
  87. hx_cli-0.1.9/src/hx/tui/widgets/hints.py +0 -75
  88. hx_cli-0.1.9/src/hx/tui/widgets/input.py +0 -460
  89. hx_cli-0.1.9/src/hx/tui/widgets/login.py +0 -175
  90. hx_cli-0.1.9/src/hx/tui/widgets/palette.py +0 -346
  91. hx_cli-0.1.9/src/hx/tui/widgets/permission.py +0 -243
  92. hx_cli-0.1.9/src/hx/tui/widgets/rule.py +0 -26
  93. hx_cli-0.1.9/src/hx/tui/widgets/selectable.py +0 -69
  94. hx_cli-0.1.9/src/hx/tui/widgets/statusbar.py +0 -259
  95. hx_cli-0.1.9/src/hx/tui/widgets/todos.py +0 -116
  96. hx_cli-0.1.9/src/hx/tui/widgets/transcript.py +0 -487
  97. hx_cli-0.1.9/src/hx/tui/widgets/working.py +0 -151
  98. hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_a_pending_permission_prompt.raw +0 -198
  99. hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_a_running_turn.raw +0 -192
  100. hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_a_turn_with_tool_blocks.raw +0 -194
  101. hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_the_completion_popup.raw +0 -191
  102. hx_cli-0.1.9/tests/tui/__snapshots__/test_snapshots/test_the_idle_screen.raw +0 -190
  103. hx_cli-0.1.9/tests/tui/test_app.py +0 -1573
  104. hx_cli-0.1.9/tests/tui/test_frame.py +0 -82
  105. hx_cli-0.1.9/tests/tui/test_permission.py +0 -132
  106. hx_cli-0.1.9/tests/tui/test_selection.py +0 -189
  107. hx_cli-0.1.9/tests/tui/test_snapshots.py +0 -159
  108. hx_cli-0.1.9/tests/tui/test_statusbar.py +0 -162
  109. hx_cli-0.1.9/tests/tui/test_transcript.py +0 -129
  110. hx_cli-0.1.9/uv.lock +0 -1816
  111. {hx_cli-0.1.9 → hx_cli-0.2.0}/.github/workflows/ci.yml +0 -0
  112. {hx_cli-0.1.9 → hx_cli-0.2.0}/.gitignore +0 -0
  113. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/agents/__init__.py +0 -0
  114. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/agents/definitions.py +0 -0
  115. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/agents/subagent.py +0 -0
  116. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/__init__.py +0 -0
  117. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/oauth/__init__.py +0 -0
  118. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/oauth/callback.py +0 -0
  119. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/oauth/codex.py +0 -0
  120. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/oauth/pkce.py +0 -0
  121. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/resolve.py +0 -0
  122. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/auth/store.py +0 -0
  123. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/cli.py +0 -0
  124. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/__init__.py +0 -0
  125. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/checkpoints.py +0 -0
  126. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/compaction.py +0 -0
  127. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/context.py +0 -0
  128. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/events.py +0 -0
  129. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/lateinject.py +0 -0
  130. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/messages.py +0 -0
  131. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/session.py +0 -0
  132. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/title.py +0 -0
  133. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/core/usage.py +0 -0
  134. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/docs.py +0 -0
  135. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/frontmatter.py +0 -0
  136. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/git.py +0 -0
  137. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/mcp/__init__.py +0 -0
  138. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/mcp/client.py +0 -0
  139. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/mcp/manager.py +0 -0
  140. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/net.py +0 -0
  141. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/paths.py +0 -0
  142. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/permissions/__init__.py +0 -0
  143. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/permissions/parser.py +0 -0
  144. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/permissions/sandbox.py +0 -0
  145. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/__init__.py +0 -0
  146. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/base.py +0 -0
  147. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/codex.py +0 -0
  148. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/codex_catalogue.py +0 -0
  149. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/fake.py +0 -0
  150. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/models.py +0 -0
  151. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/openrouter.py +0 -0
  152. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/registry.py +0 -0
  153. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/providers/responses_codec.py +0 -0
  154. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/skills/__init__.py +0 -0
  155. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/skills/loader.py +0 -0
  156. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/skills/runtime.py +0 -0
  157. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/__init__.py +0 -0
  158. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/base.py +0 -0
  159. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/bash.py +0 -0
  160. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/edit.py +0 -0
  161. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/glob.py +0 -0
  162. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/grep.py +0 -0
  163. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/output.py +0 -0
  164. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/read.py +0 -0
  165. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/registry.py +0 -0
  166. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/task.py +0 -0
  167. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/todo.py +0 -0
  168. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/websearch.py +0 -0
  169. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tools/write.py +0 -0
  170. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/clipboard.py +0 -0
  171. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/fuzzy.py +0 -0
  172. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/killring.py +0 -0
  173. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/roles.py +0 -0
  174. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/theme_json.py +0 -0
  175. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/themes/ansi.json +0 -0
  176. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/themes/dark.json +0 -0
  177. {hx_cli-0.1.9 → hx_cli-0.2.0}/src/hx/tui/themes/light.json +0 -0
  178. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/__init__.py +0 -0
  179. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/agents/__init__.py +0 -0
  180. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/agents/test_subagent.py +0 -0
  181. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/__init__.py +0 -0
  182. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/test_codex_oauth.py +0 -0
  183. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/test_pkce.py +0 -0
  184. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/test_resolve.py +0 -0
  185. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/auth/test_store.py +0 -0
  186. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/conftest.py +0 -0
  187. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/__init__.py +0 -0
  188. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_checkpoints.py +0 -0
  189. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_compaction.py +0 -0
  190. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_context.py +0 -0
  191. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_lateinject.py +0 -0
  192. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_loop.py +0 -0
  193. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_rewind.py +0 -0
  194. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_session.py +0 -0
  195. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_settings_layers.py +0 -0
  196. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_settings_migration.py +0 -0
  197. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_steering.py +0 -0
  198. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_title.py +0 -0
  199. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/core/test_usage.py +0 -0
  200. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/mcp/__init__.py +0 -0
  201. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/mcp/fixtures/echo_server.py +0 -0
  202. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/mcp/test_manager.py +0 -0
  203. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/permissions/__init__.py +0 -0
  204. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/permissions/test_engine.py +0 -0
  205. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/permissions/test_parser.py +0 -0
  206. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/permissions/test_sandbox.py +0 -0
  207. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/__init__.py +0 -0
  208. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_catalogue.py +0 -0
  209. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_codex.py +0 -0
  210. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_codex_catalogue.py +0 -0
  211. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_openrouter.py +0 -0
  212. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_registry.py +0 -0
  213. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/providers/test_responses_codec.py +0 -0
  214. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/skills/__init__.py +0 -0
  215. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/skills/test_loader.py +0 -0
  216. {hx_cli-0.1.9/tests/tools → hx_cli-0.2.0/tests/term}/__init__.py +0 -0
  217. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_cli.py +0 -0
  218. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_docs.py +0 -0
  219. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_integration.py +0 -0
  220. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_live.py +0 -0
  221. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_live_codex.py +0 -0
  222. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_live_tavily.py +0 -0
  223. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_net.py +0 -0
  224. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/test_paths.py +0 -0
  225. {hx_cli-0.1.9/tests/tui → hx_cli-0.2.0/tests/tools}/__init__.py +0 -0
  226. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_background.py +0 -0
  227. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_bash.py +0 -0
  228. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_files.py +0 -0
  229. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_output.py +0 -0
  230. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_todo.py +0 -0
  231. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tools/test_websearch.py +0 -0
  232. {hx_cli-0.1.9 → hx_cli-0.2.0}/tests/tui/test_clipboard.py +0 -0
  233. {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.1.8...HEAD
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, Textual TUI, OpenRouter for models.
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` | toggle the todo sidebar |
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 of the transcript |
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+z` undoes, `ctrl+shift+z` redoes.
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` | toggle the sidebar |
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" // queue | steer - what enter does mid-turn
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
- tui/ Textual app, commands, per-tool renderers, widgets
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, Textual TUI, OpenRouter for models.
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` | toggle the todo sidebar |
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 of the transcript |
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+z` undoes, `ctrl+shift+z` redoes.
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` | toggle the sidebar |
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" // queue | steer - what enter does mid-turn
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
- tui/ Textual app, commands, per-tool renderers, widgets
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
- padding=$1
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
- [ "${2:-}" = clear ] && clear_prefix='\033[2K\r'
35
-
36
- printf '\033[0;36m'
37
- printf '%b%s%s\n' "$clear_prefix" "$padding" ' _ _ __ __'
38
- printf '%b%s%s\n' "$clear_prefix" "$padding" '| | | | \ \/ /'
39
- printf '%b%s%s\n' "$clear_prefix" "$padding" '| |_| | > <'
40
- printf '%b%s%s\n' "$clear_prefix" "$padding" "| _ | / /\\ \\"
41
- printf '%b%s%s\n' "$clear_prefix" "$padding" "|_| |_|/_/ \\_\\"
42
- printf '\033[0m'
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
- # Slide in, overshoot slightly, then settle into place.
52
- render_logo ' ' clear
53
- sleep 0.07
54
- printf '\033[5A'
55
- render_logo ' ' clear
56
- sleep 0.07
57
- printf '\033[5A'
58
- render_logo ' ' clear
59
- sleep 0.07
60
- printf '\033[5A'
61
- render_logo '' clear
62
- sleep 0.07
63
- printf '\033[5A'
64
- render_logo ' ' clear
65
- sleep 0.07
66
- printf '\033[5A'
67
- render_logo '' clear
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
- "pytest-textual-snapshot>=1.0",
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 = ["textual.*", "textual_dev.*"]
99
+ module = ["pyte.*", "wcwidth.*", "markdown_it.*"]
94
100
  ignore_missing_imports = true
95
101
 
96
102
  [tool.pytest.ini_options]
@@ -1,5 +1,5 @@
1
1
  """HX - a terminal coding agent."""
2
2
 
3
- __version__ = "0.1.9"
3
+ __version__ = "0.2.0"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -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=_permission_detail(call),
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 modal shows: the command, or the exact edit.
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
- return unified_diff(before, after, str(path)) or f"{path} (no change)"
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: